React Lazy: a take on preloading views
September 24, 2019 / 6 min read
Last Updated: September 24, 2019Ever since I first got to use React Loadable and React Lazy and Suspense, I've been a big advocate of route base code splitting for big applications. They allow the client to only load the chunks of the apps they are actually accessing instead of having to download the whole app before rendering something on the screen. It works so well that this pattern is even featured on the official documentation of the React Library. However, I'm always looking for tips and tricks on how I could make the end-user experience even better and that's why recently I've been trying to address the problem of how to preload chunks in a route base code split React app to make navigation between chunks more seamless. I've seen this behavior on some Gatsby sites and I really liked how discrete and efficient it is. That's why I decided to write my own take on implementing preloading and to share it with you all!
Our app
Let's consider a React app with multiple routes: a landing page route, another one to get a list of todos, another one to inspect todos. Each route renders a specific view / components. We carefully read the React documentation about code splitting and used React.Lazy
and React.Suspense
which results in us having a codebase similar to the following:
Sample route based code split app root
1import React from 'React';2import { Route, Router, Switch } from 'react-router-dom';34const App = React.lazy(() => import('./App'));5const Todos = React.lazy(() => import('./Todos'));6const Todo = React.lazy(() => import('./Todo'));78const routes = [9{ path: '/', exact: true, component: App },10{ path: '/todos', exact: true, component: Todos },11{ path: '/todos/:id', exact: true, component: Todo },12];1314ReactDOM.render(15<Router>16<React.Suspense fallback={'Loading'}>17<Switch>18{routes.map((route) => (19<Route20key={route.path}21exact={route.exact}22path={route.path}23component={route.component}24/>25))}26</Switch>27</React.Suspense>28</Router>29);
If we run our app, we can see in the developer tools of our browser that navigating from one view to another is loading the different "pieces" or "chunks" of our app. Now let's focus on how we could start loading these chunks when the user hovers one of the navigation links instead of loading them after navigating to the new route.
Preload components with React Lazy
To preload the view we'll have to be able to call a preload
method on our chunk. This preload
method would be able to be called to run the import statement that is passed to React Lazy.
While such functionality is available out of the box in React Loadable, React Lazy doesn't seem to provide it and this is why we'll have to implement it from scratch with the following code:
Implementation of ReactLazyPreload
1const ReactLazyPreload = (importStatement) => {2const Component = React.lazy(importStatement);3Component.preload = importStatement;4return Component;5};
We can now redeclare our code split chunks as follows:
Examples of usage of ReactLazyPreload
1const App = ReactLazyPreload(() => import('./App'));23const Todos = ReactLazyPreload(() => import('./Todos'));45const Todo = ReactLazyPreload(() => import('./Todo'));
With the code above we can now call the preload method at will on any of our components which will result in each of them loading their respective chunks:
Calling "preload" on our components
1App.preload();2Todos.preload();3Todo.preload();
Calling preload on the right component for a given route
While we could specify which component we'd wish to preload when the user hovers a given link, wouldn't it be great if we could "find" which component to preload based on the route?
If we take a look at the first code snippet of this post, we can see that we declared a routes
object that contains all the properties required by the React Router Route
component. This is intentional and will be useful for us to find which component is associated to which route.
Let's declare our findComponentForRoute
function:
Implementation of findComponentForRoute
1import { matchPath } from 'react-router-dom';23const findComponentForRoute = (path, routes) => {4const matchingRoute = routes.find((route) =>5matchPath(path, {6path: route.path,7exact: route.exact,8})9);1011return matchingRoute ? matchingRoute.component : null;12};
React Router comes with a pretty handy method called matchPath
which for a given path will return true
if the path passed in the second argument matches. The function in the code snippet above uses this method and when a match is found, returns the associated component or null
if no route has been found.
Now that we have a way to find the component associalted to a given route, we can create a function to preload it:
Implementation of preloadRouteComponent
1const preloadRouteComponent = (path) => {2const component = findComponentForRoute(path, routes);34if (component && component.preload) {5component.preload();6}7};
Finally, we could just add this function to a onMouseEnter
event handler to all our Link
components and call it a day but let's make the things we just implemented easier to use. Let's create a LinkWithPreload
component that will have the same props as Link
but will also use the preloadRouteComponent
function:
LinkWithPreload component
1import { Link } from 'react-router-dom'23...45const LinkWithPreload = ({ to, onPreload, ...rest }) => {6return (7<Link8to={to}9onMouseEnter={() => preloadRouteComponent(to)}10{...rest}11/>12);13};1415export default LinkWithPreload;
Now, by using this component instead of Link
, as you can see below, hovering any navigational links in our App should load the chunk associated with the route of that link:
Do you want to see the whole code? I made the app showcased in this article available here!
Liked this article? Share it with a friend on Bluesky or Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.
Have a wonderful day.
– Maxime
How to add preloading to your route based code split apps