Code Splitting with React and Redux
Modern web sites often combine all of their JavaScript into a single, large bundle. When JavaScript is served this way, loading performance suffers. Large amounts of JavaScript can also tie up the main thread, delaying interactivity. This is especially true of devices with less memory and processing power.
An alternative to large bundles is code-splitting, which is where JavaScript is split into smaller chunks. This enables sending the minimal code required to provide value upfront, improving page-load times. The rest can be loaded on demand.
In this guide, we will be talking about Code-Splitting in the context of a React, Redux, React-Redux Application
This guide assumes some level of familiarity with React, Redux and React-Redux.
What is Code-Splitting
Code-Splitting is the act of deferring the import of some portion of our JavaScript until it is needed at a later point in time by a user interaction such as button click, scrolling, typing, etc.
Code-Splitting helps in reducing the amount of JavaScript that is needed to make our app load as quickly as possible thereby maximising user engagement and improve page load times.
Code-Splitting is a feature supported by bundlers like Webpack
, Rollup
and Browserify
(via factor-bundle
) which can create multiple bundles that can be dynamically loaded at runtime.
Code-Splitting in a React Component
The most preferred way to introduce code-splitting in a React Component is via dynamic import()
. The import()
function-like form takes the module name as an argument and returns a Promise which always resolves to the namespace object of the module.
import("./Module").then((Module) => Module.method());
// SomeComponent.js const SomeComponent = () => <p>This is a test component</p>; export default SomeComponent;
// App.js import React, { Component } from "react"; class App extends Component { handleClick = () => { import("./SomeComponent") .then(({ SomeComponent }) => { // Use SomeComponent }) .catch((err) => { // Handle failure }); }; render() { return ( <div> <button onClick={this.handleClick}>Click Me</button> </div> ); } } export default App;
The example above includes SomeComponent.js
as a separate chunk that only loads after the 'Click Me' button has been clicked.
React.lazy
method makes it easy to code-split a React application on a component level using dynamic imports.
React.lazy
takes a function that must call a dynamic import()
. This must return a Promise
which resolves to a module with a default
export containing a React component.
const SomeComponent = React.lazy(() => import("./SomeComponent"));
The lazy component should then be rendered inside a Suspense
component, which allows us to show some fallback content (such as a loading indicator) while we’re waiting for the lazy component to load.
const SomeComponent = React.lazy(() => import("./SomeComponent")); function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <SomeComponent /> </Suspense> </div> ); }
Suspense
fallback props accepts any React elements which is being rendered while waiting for the component to load. We can place the Suspense
component anywhere above the lazy component. We can even wrap multiple lazy components with a single Suspense
component.
const SomeComponent = React.lazy(() => import("./SomeComponent")); const AnotherComponent = React.lazy(() => import("./AnotherComponent")); function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <SomeComponent /> <AnotherComponent /> </Suspense> </div> ); }
React.lazy
currently only supports default exports.
Route-based Code-splitting
Here’s an example of how to setup route-based code-splitting into our app using libraries like React Router with React.lazy
.
// App.js import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; import React, { Suspense, lazy } from "react"; const Home = lazy(() => import("./routes/Home")); const About = lazy(() => import("./routes/About")); const App = () => ( <Router> <Suspense fallback={<div>Loading...</div>}> <Switch> <Route exact path="/" component={Home} /> <Route path="/about" component={About} /> </Switch> </Suspense> </Router> );
Code-Splitting with Redux
From the previous sections, we have been able to demonstrate how we can load our React Component's dynamically,but yet we still need to get the right data into our modules as they load.
Redux as a state management library allows us to provide reducer functions at the time we create the store with createStore
function but does not give us the ability to to register reducer functions on demand. So how to we achieve that?
It turns redux store API exposes a replaceReducer
function that replaces the current active root reducer function with a new root reducer function.
// store.js import { combineReducers, createStore } from "redux"; const initialState = {}; const store = createStore(createReducer(), initialState); const newRootReducer = combineReducers({ existingSlice: existingSliceReducer, newSlice: newSliceReducer, }); store.replaceReducer(newRootReducer);
We could go one step further by creating a reusable injectReducer
function in addition to the replaceReducer
that keeps references to all of the existing slice reducers, and attach that to the store instance.
// reducers.js import { combineReducers } from "redux"; const createReducer = (asyncReducers) => { return combineReducers({ ...asyncReducers, }); }; export default createReducer;
// store.js import { createStore } from "redux"; import createReducer from "./reducers"; const store = createStore(createReducer()); export default function configureStore() { // Add a dictionary to keep track of the registered async reducers store.asyncReducers = {}; // Create an inject reducer function // This function adds the async reducer, and creates a new combined reducer store.injectReducer = (key, asyncReducer) => { store.asyncReducers[key] = asyncReducer; store.replaceReducer(createReducer(store.asyncReducers)); }; // Return the modified store return store; } export function getStore() { return store; }
NB: injectReducer
is not part of the redux store API
And the usage looks like this
// App.js import React from "react"; import { getStore } from "../store"; const store = getStore(); const Section = React.lazy(() => import("../containers/Section").then(async (module) => { const todos = await import("../reducers/todos").then( (todosModule) => todosModule.default ); store.injectReducer("todos", todos); return module; }) ); const App = () => ( <React.Suspense fallback={<div>loading...</div>}> <MainSection /> </React.Suspense> ); export default App;
Some few redux libraries that is worth checking out are
The link below points to a working example of a todo application that uses React.lazy
, React.Suspense
and Redux
Conclusion
We have been able to see how Code Splitting can be help us improve loading time, improve page performance by loading Javascript on demand until it is needed.
Here are some few resources on Code-Splitting to check out