8

I have following App component:

            <Route render={( { location } ) => (
                <TransitionGroup component="div" className="content">
                    <CSSTransition key={location.key} classNames="slide" timeout={{
                        enter: 1000,
                        exit: 300
                    }} appear>
                        <Switch location={location}>
                            <Route exact path='/' component={Intro}/>
                            <Route path="/history" component={History}/>
                            <Route path="/rules" component={Rules}/>
                            <Route path="/faq" component={Faq}/>
                            <Route path="/feedback" component={Feedback}/>
                            <Route path="/partners" component={Partners}/>
                        </Switch>
                    </CSSTransition>
                </TransitionGroup>
            )}/>

And it works fine, but every animation executes immediately. For example, if I go from /rules to /history, I got full animation on both components, but history component require data from the server, so animation applied on empty container.

How could I pause animation in react-transition-group components? I have Redux, so I could change loading variable anywhere in my app. Also, I don't want to preload all data in the store on app start.

Dracontis
  • 3,267
  • 6
  • 31
  • 48
  • When are fetching data for the new container, is it on componentWillMount ? – Rohit Garg Nov 28 '18 at 13:19
  • @RohitGarg yes, I've tried this. But this won't solve this issue, because fetching data is Promise (`axios` if this matters), so `render()` will be fired before finishing data load so it will trigger animation from `react-transition-group` and after some time data appear on the screen. – Dracontis Nov 28 '18 at 14:02

3 Answers3

2

I would make your component return null when it's loading and make the loading state determine the CSSTransition key like <CSSTransition key={location.key+loading?'-loading':''}

see example here: https://stackblitz.com/edit/react-anim-route-once

note that to make this work without duplication I had to make the component copy the loading prop and persist it in state, so that one of the copies of the component never displays (which would create a duplication of the component as seen here: https://stackblitz.com/edit/react-anim-route-twice)

    <Route render={({ location }) => (
      <TransitionGroup component="div" className="content">
        <CSSTransition key={location.key+(this.state.loading?'-loading':'-loaded')} classNames="crossFade" timeout={{
          enter: 3000,
          exit: 3000
        }} appear>
          <Switch location={location} >
            <Route exact path='/' component={Hello} />
            <Route exact path='/history' render={() =>
              <Delayed setLoading={this.handleSetLoading} loading={this.state.loading} />} />

          </Switch>

        </CSSTransition>
      </TransitionGroup>
    )} />

and in the component something like this:

export default class History extends React.Component {
  state={loading:this.props.loading}
  componentDidMount() {
    setTimeout(() => {
      this.props.setLoading(false);
    }, 2000);
  }
  render() {
    return !this.state.loading ? <div><h1>History! <Link to="/">Home</Link></h1></div> : null;
  }
}
Tiago Coelho
  • 4,262
  • 7
  • 15
  • Can't figure out how this could help me. Should I wrap each route with `CSSTransition` or have it inside each component? I've tried to return `null` in render for components, but it just renders animation for enter/exit on previous component and null. So when data is ready it just renders page. – Dracontis Dec 04 '18 at 20:23
  • It does work, because setting the CSSTransition key makes it assume another transition when the load is done, but it has a small flaw which makes the loaded route appear twice. I'm editing to include links to working code (with this flaw and also with that fixed) – Tiago Coelho Dec 05 '18 at 00:44
  • this loading behavior can then be extracted to a HOC to keep the real component and the routes clean – Tiago Coelho Dec 05 '18 at 00:56
  • I'm sorry, but small flaw is unacceptable in real applications, because there is no point in animation with such strange behaviour. – Dracontis Dec 05 '18 at 07:08
  • This just means you mount the component twice. I showed you how to fix the flaw, and provided a fully workibg example – Tiago Coelho Dec 05 '18 at 08:08
  • This solution means that you can also use a loading indicator instead of null – Tiago Coelho Dec 05 '18 at 08:09
  • If this is a no go for you I recomend a specific animation inside the component to handle the loading state – Tiago Coelho Dec 05 '18 at 08:16
  • 1
    I have no time to test it, as I went with redux preloading, though I think I should give you bounty, as it solves animation task with react-transition-group and react-router only. – Dracontis Dec 05 '18 at 09:48
  • 1
    Also, after it rendered it works a bit strange. If you press Home > History first time it will work flawlessly, but when you press History > Home it renders two components the same time. We've solved such issue in our project, but it still could be a small enhancement for this example. – Dracontis Dec 05 '18 at 09:55
  • that's only about the css. I did not spend any time crafting the animations themselves. (it is supposed to have two elements at the same time: the entering one and the exiting one) but I will update to slide animations and you will be able to see it still works fine :) – Tiago Coelho Dec 05 '18 at 10:24
  • Looks cool now. And I think I get the point of animation and overall composition. That's all I wanted in my question. – Dracontis Dec 05 '18 at 10:44
1

So my cases have been a bit different but they might help you think of a solution.

  1. You can delay the initial display easily by adding an if (this.state.isloaded == true) block around your whole router. Start loading when your component mounts, and when the async call completes, setState({isloaded: true}).
  2. You can make your own <Link> component, which launches a request, and only once it’s complete changes the page location. You can do whatever special loading spinners you like in the meantime.

Basically, keep the routing and transition components to one side. I find them to be brittle and painful with cases like this. Let me know if you want any more details or snippets.

  • 1
    First approach is not an option, because it will remove current page so it won't be visible during loading process. Second option is better, but instead of Link component I could create custom redirect methods in Redux, and I want something different. – Dracontis Nov 29 '18 at 13:29
  • Re 1, You could have the other page respond to a second flag in the first option. It's basically building your own state manager, which can be hell, but the CSS Transitions framework is very picky with React Router, so I think it's a good skill to have. The link option seems much easier tbh, and if you do make it a component you can nicely encapsulate it's state and data payload, and pass its existence up and down with props and state. But, I'm from iOS land, I have weird ideas about how to do things. – Olivier Butler Nov 30 '18 at 13:04
0

I've done peloading through redux and redux-saga. Maybe it's one and only way to achieve following with react-router and react-transition-group, because transition toggle animation anytime when render method is run, even if it return null.

I've implemented following actions:

const LoadingActions = {
    START_LOADING: 'START_LOADING',
    STOP_LOADING: 'STOP_LOADING',
    REDIRECT: 'REDIRECT',

    startLoading: () => ({
        type: LoadingActions.START_LOADING
    }),

    stopLoading: () => ({
        type: LoadingActions.STOP_LOADING
    }),

    redirect: ( url, token ) => ({
        type: LoadingActions.REDIRECT,
        payload: {
            url,
            token
        }
    })
};

export default LoadingActions;

In the reducers I've implemented simple loader reducer, that will toggle on and off loading variable:

import { LoadingActions } from "../actions";

const INITIAL_STATE = {
    loading: false
};

export default function ( state = INITIAL_STATE, { type } ) {
    switch ( type ) {
        case LoadingActions.START_LOADING:
            return { loading: true };
        case LoadingActions.STOP_LOADING:
            return { loading: false };
        default:
            return state;
    }
}

The most irritating thing is reducer chain - this.props.loader.loading. Too complex for such simple thing.

import { combineReducers } from "redux";
...
import LoadingReducer from './LoadingReducer';

export default combineReducers( {
    ...
    loader: LoadingReducer
} );

This most work goes in saga:

function* redirect ( action ) {
    yield put( LoadingActions.startLoading() );
    const { url } = action.payload;

    switch ( url ) {
        case MENU_URL.EXCHANGE:
            yield call( getExchangeData, action );
            break;
        ... other urls...
    }
    yield put( LoadingActions.stopLoading() );
    BrowserHistory.push( url );
}

... loaders ...


function* watchRedirect () {
    yield takeLatest( LoadingActions.REDIRECT, redirect );
}

const sagas = all( [
    ...
    fork( watchRedirect )
] );

export default sagas;

I put listener on redirect action, so it will call redirect generator. It will start loading and call data preloading yield call will await for preload to finish and after it will stop loading and redirect. Though it won't wait for positive result, so preloaders should handle errors themselves.

I hoped that I could avoid redux complexity with built-in feature of router or transition library, but it has no such tools to stop transition. So it is one of the best way to achieve transition with preloded data.

Dracontis
  • 3,267
  • 6
  • 31
  • 48