Code Splitting for redux and redux-saga
April 16, 2019 · 7 mins to read
Code Splitting
Nowadays, most web application files are combined into a single file called a “bundle”. In a relatively large application, you often don’t need to load all your bundle during the initial load as only a small part of that code is end evaluated (you can check the percentage in “Coverage” tab of Chrome Developer Tools).
The solution for that problem is to split up the application into multiple JS chunks and load them on demand (lazy-load). This process is called code-splitting. It helps to reduce the initial JS files size, hence to fasten up the startup of the web application, as the most time during startup is spent on load, parse/compile1.
React Components Splitting
React components are relatively easy to split into separate chunks through import()
, React.lazy
, and React.Suspense
. If you are not familiar with that I recommend you to read the official React Docs about Code Splitting first then come here and continue.
Example of components code splitting.
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>
);
Route-based code splitting
The most common strategy for choosing what to load dynamically (via import()
) and what statically is route-based code splitting.
Most web applications are using some kind of routing of their components. Splitting the page components will help you to start the process of code-splitting.
Here’s an example where we have two pages: Home page and About page. We are using react-router-dom packages for our routing.
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>
);
Redux Code Splitting
Most Redux application has one root reducer which is normally generated by combineReducers()
.
And code-splitting for Redux apps will be a way to add reducer on-demand. Unfortunately, redux doesn’t offer a built-in API to do that, however, you can replace the whole reducer by calling store.replaceReducer(nextReducer)
.
We can define a new function on store
called injectReducer
which will create a new reducer from our reducers map and replace the store’s reducer with newly created one.
import { createStore } from 'redux';
// Define the reducers that will always be present in the application
const staticReducers = {
posts: postsReducer,
};
// Configure the store
export default function configureStore(initialState) {
const store = createStore(createReducer(), initialState);
// 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;
}
function createReducer(asyncReducers) {
return combineReducers({
...staticReducers,
...asyncReducers,
});
}
You can read more about this approach in the official Redux docs.
Now, we can modify our React components code-splitting to import reducers and inject them.
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';
import { Provider } from 'react-redux';
import initStore from './store';
const store = initStore();
// ./modules/home file exports reducer under "reducer" named export
const Home = lazy(() =>
import('./modules/home').then(module => {
store.injectReducer('home', module.reducer);
return import('./routes/Home');
})
);
// ./modules/about file exports reducer under "reducer" named export
const About = lazy(() =>
import('./modules/about').then(module => {
store.injectReducer('about', module.reducer);
return import('./routes/About');
})
);
const App = () => (
<Provider store={store}>
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
</Provider>
);
Redux Saga Code Splitting
Redux Saga allows to easier manage application side-effects. If you use redux-saga
, splitting them is very easy.
In the typical redux-saga based application you will have watch
sagas which are watching for some actions and forking functions whenever
those actions are dispatched. You can easily add new sagas and cancel them whenever you want. To do that we are going to use
middleware.run()
function.
// runSaga is middleware.run function
// rootSaga is a your root saga for static saagas
function createSagaInjector(runSaga, rootSaga) {
// Create a dictionary to keep track of injected sagas
const injectedSagas = new Map();
const isInjected = key => injectedSagas.has(key);
const injectSaga = (key, saga) => {
// We won't run saga if it is already injected
if (isInjected(key)) return;
// Sagas return task when they executed, which can be used
// to cancel them
const task = runSaga(saga);
// Save the task if we want to cancel it in the future
injectedSagas.set(key, task);
};
// Inject the root saga as it a staticlly loaded file,
injectSaga('root', rootSaga);
return injectSaga;
}
// Our previously defined method for reducers injection
function configureStore(initialState) {
// Add sagas middleware
const store = createStore(createReducer(), initialState, applyMiddleware(sagaMiddleware));
// Add injectSaga method to our store
store.injectSaga = createSagaInjector(sagaMiddleware.run, rootSaga);
// The rest of the code
...
}
Now, we should modify our React components code splitting to inject sagas:
// ./modules/about file exports reducer under "reducer" named export and
// sagas under "saga" named export
const Home = lazy(() =>
import('./modules/home').then(module => {
store.injectReducer('home', module.reducer);
store.injectSaga('home', module.saga);
return import('./routes/Home');
})
);
const About = lazy(() =>
import('./modules/about').then(module => {
store.injectReducer('about', module.reducer);
store.injectSaga('about', module.saga);
return import('./routes/About');
})
);
In the result, our sagas and reducers will be lazy loaded when user loads the required page.
You should understand that if you have the injected reducer or saga in your dependency tree before import() the whole point of code-splitting will be wasted as your bundle will already contain the reducer and the saga in your code. 2
Webpack Optimization
If you are using Webpack as your bundler you can explicitly tell Webpack to bundle all route files into one file (chunk), by using so-called magic comments.
The magic comment we are interested in is called webpackChunkName
and webpackPrefetch
.
The default behavior of webpackChunkName
is to combine all modules with the same name in one chunk.
// Include ./modules/home file/module in a chunk called "home"
const Home = lazy(() =>
import(/* webpackChunkName: "home" */ './modules/home').then(module => {
store.injectReducer('home', module.reducer);
store.injectSaga('home', module.saga);
// Include ./routes/Home file/module in a chunk called "home"
return import(
/* webpackChunkName: "home" */
'./routes/Home'
);
})
);
// Include ./modules/about file/module in a chunk called "about"
const About = lazy(() =>
import(/* webpackChunkName: "about" */ './modules/about').then(module => {
store.injectReducer('about', module.reducer);
store.injectSaga('about', module.saga);
// Include ./routes/About file/module in a chunk called "about"
return import(/* webpackChunkName: "about" */ './routes/About');
})
);
Conclusion
In this blog post, we saw how we can easily code-split our Redux and redux-saga based application into separate files which one of the most powerful ways to speed up your application. After understanding how the code-splitting works for Redux and redux-saga you may prefer to use maintained libraries for better managed of async redux reducers and redux-saga sagas. You can find the comprehensive list of such libraries in Redux Ecosystem Links: Reducers - Dynamic Reducer Injection.
Also, I’d recommend going through the links in Further reading to better understand the concepts of code-splitting.