Skip to content

Commit

Permalink
[DRAFT]: Add a Code Splitting page to the 'Recipes' section (#3190)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
abettadapur authored and markerikson committed Dec 16, 2018
1 parent d109869 commit 5350611
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 0 deletions.
167 changes: 167 additions & 0 deletions docs/recipes/CodeSplitting.md
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions docs/recipes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions website/sidebars.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 5350611

Please sign in to comment.