From 53506112fcb38d62fa9b769186eb14bac517abbb Mon Sep 17 00:00:00 2001 From: Alex Bettadapur Date: Sun, 16 Dec 2018 15:12:13 -0500 Subject: [PATCH] [DRAFT]: Add a Code Splitting page to the 'Recipes' section (#3190) An initial draft at a code splitting page. @markerikson Let me know your thoughts, if I need to add more sections or clarify anything in more detail --- docs/recipes/CodeSplitting.md | 167 ++++++++++++++++++++++++++++++++++ docs/recipes/README.md | 1 + website/sidebars.json | 1 + 3 files changed, 169 insertions(+) create mode 100644 docs/recipes/CodeSplitting.md diff --git a/docs/recipes/CodeSplitting.md b/docs/recipes/CodeSplitting.md new file mode 100644 index 0000000000..51fe9e5e0e --- /dev/null +++ b/docs/recipes/CodeSplitting.md @@ -0,0 +1,167 @@ +--- +id: code-splitting +title: Code Splitting +sidebar_label: Code Splitting +hide_title: true +--- + +# Code Splitting + +In large web applications, it is often desirable to split up the app code into multiple JS bundles that can be loaded on-demand. This strategy, called 'code splitting', helps to increase performance of your application by reducing the size of the initial JS payload that must be fetched. + +To code split with Redux, we want to be able to dynamically add reducers to the store. However, Redux really only has a single root reducer function. This root reducer is normally generated by calling `combineReducers()` or a similar function when the application is initialized. In order to dynamically add more reducers, we need to call that function again to re-generate the root reducer. Below, we discuss some approaches to solving this problem and reference two libraries that provide this functionality. + +## Basic Principle + +### Using `replaceReducer` + +The Redux store exposes a `replaceReducer` function, which replaces the current active root reducer function with a new root reducer function. Calling it will swap the internal reducer function reference, and dispatch an action to help any newly-added slice reducers initialize themselves: + +```js +const newRootReducer = combineReducers({ + existingSlice: existingSliceReducer, + newSlice: newSliceReducer +}) + +store.replaceReducer(newRootReducer) +``` + +## Reducer Injection Approaches + +### Defining an `injectReducer` function + +We will likely want to call `store.replaceReducer()` from anywhere in the application. Because of that, it's helpful +to define a reusable `injectReducer()` function that keeps references to all of the existing slice reducers, and attach +that to the store instance. + +```javascript +import { createStore } from 'redux' + +// Define the Reducers that will always be present in the appication +const staticReducers = { + users: usersReducer, + 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) => { + this.asyncReducers[key] = asyncReducer + this.replaceReducer(createReducer(this.asyncReducers)) + } + + // Return the modified store + return store +} + +function createReducer(asyncReducers) { + return combineReducers({ + ...staticReducers, + ...asyncReducers + }) +} +``` + +Now, one just needs to call `store.injectReducer` to add a new reducer to the store. + +### Using a 'Reducer Manager' + +Another approach is to create a 'Reducer Manager' object, which keeps track of all the registered reducers and exposes a `reduce()` function. Consider the following example: + +```javascript +export function createReducerManager(initialReducers) { + // Create an object which maps keys to reducers + const reducers = { ...initialReducers }; + + // Create the initial combinedReducer + let combinedReducer = combineReducers(reducers); + + // An array which is used to delete state keys when reducers are removed + const keysToRemove = []; + + return { + getReducerMap: () => reducers, + + // The root reducer function exposed by this object + // This will be passed to the store + reduce: (state, action) => { + // If any reducers have been removed, clean up their state first + if (keysToRemove.length > 0) { + state = { ...state as any }; + for (let key of keysToRemove) { + delete state[key]; + } + keysToRemove = []; + } + + // Delegate to the combined reducer + return combinedReducer(state, action); + }, + + // Adds a new reducer with the specified key + add: (key, reducer) => { + if (!key || reducers[key]) { + return; + } + + // Add the reducer to the reducer mapping + reducers[key] = reducer; + + // Generate a new combined reducer + combinedReducer = combineReducers(reducers); + }, + + // Removes a reducer with the specified key + remove: (key: string) => { + if (!key || !reducers[key]) { + return; + } + + // Remove it from the reducer mapping + delete reducers[key]; + + // Add the key to the list of keys to clean up + keysToRemove.push(key); + + // Generate a new combined reducer + combinedReducer = combineReducers(rm); + } + }; +} + +const staticReducers = { + users: usersReducer, + posts: postsReducer +}; + +export function configureStore(initialState) { + const reducerManager = createReducerManager(staticReducers); + + // Create a store with the root reducer function being the one exposed by the manager. + const store = createStore(reducerManager.reduce, initialState); + + // Optional: Put the reducer manager on the store so it is easily accessible + store.reducerManager = reducerManager; +} +``` + +To add a new reducer, one can now call `store.reducerManager.add("asyncState", asyncReducer)`. + +To remove a reducer, one can now call `store.reducerManager.remove("asyncState")` + +## Libraries and Frameworks + +There are a few good libraries out there that can help you add the above functionality automatically: + +- [`redux-dynostore`](https://github.com/ioof-holdings/redux-dynostore): + Provides tools for building dynamic Redux stores, including dynamically adding reducers and sagas, and React bindings to help you add in association with components. +- [`redux-dynamic-modules`](https://github.com/Microsoft/redux-dynamic-modules): + This library introduces the concept of a 'Redux Module', which is a bundle of Redux artifacts (reducers, middleware) that should be dynamically loaded. It also exposes a React higher-order component to load 'modules' when areas of the application come online. Additionally, it has integrations with libraries like `redux-thunk` and `redux-saga` which also help dynamically load their artifacts (thunks, sagas). +- [Redux Ecosystem Links: Reducers - Dynamic Reducer Injection](https://github.com/markerikson/redux-ecosystem-links/blob/master/reducers.md#dynamic-reducer-injection) diff --git a/docs/recipes/README.md b/docs/recipes/README.md index 167589d957..8655c9666c 100644 --- a/docs/recipes/README.md +++ b/docs/recipes/README.md @@ -13,6 +13,7 @@ These are some use cases and code snippets to get you started with Redux in a re - [Migrating to Redux](MigratingToRedux.md) - [Using Object Spread Operator](UsingObjectSpreadOperator.md) - [Reducing Boilerplate](ReducingBoilerplate.md) +- [Code Splitting](CodeSplitting.md) - [Server Rendering](ServerRendering.md) - [Writing Tests](WritingTests.md) - [Computing Derived Data](ComputingDerivedData.md) diff --git a/website/sidebars.json b/website/sidebars.json index 103a4e5dcf..c30184ece0 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -41,6 +41,7 @@ "recipes/implementing-undo-history", "recipes/isolating-redux-sub-apps", "recipes/using-immutablejs-with-redux", + "recipes/code-splitting", { "type": "subcategory", "label": "Structuring Reducers",