React Router transitions with Lazy Loading
By: Jørgen Lybeck Hansen
Tags: React, Animation, JavaScript, Router, Tutorial

This guide builds upon my previous guide, where I explain how to get route transitions/animations with react-router-dom and react-pose.

It’s a fairly simple setup. When you start scaling your application, and your code base starts growing, you might want to add some Lazy Loading to the components. With Create React App this is really easy. You just add a Suspense wrapper and import the component the Async method:

const LazyComponent = React.lazy(() => import("./Component")); return (

<React.Suspense fallback={<div />}>

<LazyComponent />

</React.Suspense>

)

However, this breaks the initial route animation when you refresh the page, because the async operation uptakes the single-threaded run time in JavaScript.

So what can we do to fix this? The solution is pre-loading components at an earlier step. This is actually the only solution I’ve found to handle this problem, so bear with me because this might look like a hack.

Before adding Pre-Loading

Click the “Two” page and see how it skips the animation, because the component needs to pre-load before actually animating in.

After adding Pre-Loading

We still get Lazy Loaded components, and the animation is intact.

Implementation

In essence, you need to load all your Lazy Loaded components in a hidden div early in your render prioritization. In this case it’s wise to have your route paths and Components in objects of an array. This way, you can export them to both your actual React Router <Switch> component, and the PreloadLazyComponents.js file.

The timeout of this implementation is for removing the components from the DOM as soon as we know they’ve all been implemented. Also, we’re passing the preload prop to all components being pre-loaded. This is to control that it doesn’t perform any API requests in the sub-components, e.g. Page1.

Imagine that Page1 has a useEffect which calls an API. You don’t want to do this prematurely, so the useEffect would have to be rewritten to

!preload && useAsyncFetch("/api")

Also, your component might be depending on data in a Redux which is not used yet. Therefore, you must make sure that the component doesn’t crash without having the data. I advise that you render the PreloadLazyComponents before rendering anything else. However, this might mean you don’t have your data ready in the components you are pre-loading. I solved this by always checking if the variable exists.

const Page1 = () => {

const user = useSelector(state => state.user); if (!user) return null;

else return <div> page 1 says hi to { user.name }! </div>

}

or

const Page1 = () => { const [ data, setData ] = React.useState(null)

const user = useSelector(state => state.user) React.useEffect( () => {

setData( user ? fetchDataByUserId(user.id) : null ) } return <div>{data}</div> }

Basically, you need to make sure your components can render without data. I solved this by using react-testing-library to add tests for all my components, with an empty state in all my reducers. Of course you can use Enzyme or your preferred library to test this. Just render the component without any expected state and assert that it doesn’t crash.

test(`<Overview/> renders without data`, async () => {

// All reducer states are initial and empty

const store = mockStore(mockInitialReducer);



const { container } = render(

<ReduxProvider store={store}>

<Router history={history}>

<React.Suspense fallback={<div />}>

<Overview />

</React.Suspense>

</Router>

</ReduxProvider>

);

expect(container).toBeInTheDocument();

});

Conclusion

Well, I know a lot of people thinks that this is over-engineered. Having to check all variables on your lazy loaded components is just messy code. Loading a ton of components early in your application just to remove them seems like a hassle. However, if you want to Lazy Load components and keep your beautiful animations on each navigation, this is the only solution I found, by asking a lot of maintainers of these libraries. If you have a better solution, I would love to see or hear about them.