Redux Stack: Modular Redux Configuration

As the Redux ecosystem grows, libraries integrate by offering their own middleware, or enhancers, or even custom compose and createStore functions.

When each library require a different way, or simply its own way to integrate with your Redux store, reducers, and middleware, it’s inevitable that your store set up code will grow and be more complex. This increases the risk that something will break, or at least for the codebase to be rigid and not modular enough to scale in the future.

Redux Stack

redux-stack is a library that helps you build modular, structured, and cleaner redux apps by making the “plugin” idea first-class.

We introduce a concept of initializers: small pieces of integration code, per library, that “declares” how it integrates. redux-stack meshes these together to create your own store builder.

Redux Initializers

An initializer in redux-stack, is a simple Javascript module that exports the following shape:

export default {
reducers: {}
enhancers: []
composers: []
}

The reducers, enhancers (remember, applyMiddleware is one example of an enhancer), and composers fields all contain one or more of the standard redux reducer, enhancer and composer.

Granted, store enhancers are the blessed way to go forward for doing store customization, in reality, I bumped into libraries wanting to add their own reducer, or supply their own createStore function, or even require that you use their own version of a compose function. It was common enough to make these part of the initializer shape.

Let’s create an initializer for ex-navigation: a popular navigation library for React Native that integrates well with Redux. Here’s the required configuration, verbatim from the README:

import { createNavigationEnabledStore, NavigationReducer } from '@exponent/ex-navigation';
import { combineReducers, createStore } from 'redux';

const createStoreWithNavigation = createNavigationEnabledStore({
createStore,
navigationStateKey: 'navigation',
});

const store = createStoreWithNavigation(
/* combineReducers and your normal create store things here! */
combineReducers({
navigation: NavigationReducer,
// other reducers
})
);

export default store;

A few observations: it uses its own createStore function, called createStoreWithNavigation. Next, it also wants to inject a reducer. This is completely trivial to do, however it may become tricky when another library wants to supply its own version of createStore, for example. And in the bigger sense of things — this is really special enough to tuck away and not bother us every time we’re looking at store configuration.

Let’s turn that into an initializer compatible with redux-stack.

First, a sharp eye would identify that createStoreWithNavigation can be turned into a standard Redux enhancer like so:

const navEnhancer = (createStoreFn) => {
return createNavigationEnabledStore({
createStore: createStoreFn,
navigationStateKey: 'navigation',
})
}

Now that we have a standard Redux enhancer, we can make the initializer:

import { NavigationReducer, createNavigationEnabledStore } from '@exponent/ex-navigation'

const navEnhancer = (createStoreFn) => {
return createNavigationEnabledStore({
createStore: createStoreFn,
navigationStateKey: 'navigation',
})
}

export default {
name: 'ex-navigation',
reducers: {navigation: NavigationReducer},
enhancers: [navEnhancer],
}

And let’s make a single index.js to consolidate all initializers:

import initNavigation from './ex-navigation'
export default[
initNavigation
]

After this is done, we want to drop the initializer somewhere convenient, so we’ll add another folder to a standard Redux layout:

actions/
components/
constants/
containers/
initializers/ <---
- ex-navigation.js
- index.js

reducers/
store/

Now, your store set up code can look nice and clean, like this:

import { createStore, combineReducers } from 'redux'
import { buildStack } from 'redux-stack'
import stack from '@/initializers'

const initialState = {}
const { reducers, enhancer } = buildStack(stack)
const store = createStore(combineReducers(reducers), initialState, enhancer)

export default store

If you keep a clean structure, this block of code will be similar for every app that you build, and can be easily extracted into its own piece of library.


Sharing Initializers

We’ve seen how to refactor a Redux app into an app that uses redux-stack, which results in cleaner set up code and a more modular app over all. It means that instead of reading library set up instructions and copying code to your main store configuration helper every time you add a library, you create an initializer and drop it in the initializers/ folder.

Creating an initializer has a small price. However, you can now easily copy that initializer to another project, and it will work as expected there too. It is time well spent.

To take the idea one step forward, you can even give this initializer to another team, or open-source it for others to enjoy. If you’re a library author, you can supply this initializer for your users as part of the library.

Towards Redux Initializers and Presets

And so, one goal around redux-stack is to have an extensive, pre-made set of initializers as part of redux-stack, or libraries themselves, so people don’t need to make them. For every well-known library you’ll have a ready-made initializer that takes care of its configuration and integration into your Redux app in a uniform way.

Another goal is to provide presets, which are pre-made sets of initializers. Or in other words: complete Redux app stacks for you to use. This idea is inspired from babel presets. So you could even do something like the listing below, to get Exponent’s favorite flavor of infrastructure (note: I’m not affiliated with Exponent, I just appreciate their libraries):

import { createStore, combineReducers } from 'redux'
import { buildStack } from 'redux-stack'
import stack from 'stack-exponent-react-native'

const { reducers, enhancer } = buildStack(stack)
const store = createStore(combineReducers(reducers), {}, enhancer)

export default store

See the Big Picture

Here’s how Redux Stack improves the way your codebase feels, and supports modularity.

Before

import users from '@/modules/users/state'
import settings from '@/modules/settings/state'
import intro from '@/modules/intro/state'
import { NavigationReducer, createNavigationEnabledStore } from '@exponent/ex-navigation'
import thunk from 'redux-thunk'
import createLogger from 'redux-logger'
import { createStore, applyMiddleware, combineReducers, compose } from 'redux'
import { persistStore, autoRehydrate } from 'redux-persist'
import immutableTransform from 'redux-persist-transform-immutable'
import {AsyncStorage} from 'react-native'
const devtools = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

const reducers = {
users,
settings,
intro,
}
const rootReducer = combineReducers({navigation: NavigationReducer, ...reducers})


const logger = createLogger({
collapsed: true,
// only log in development mode
predicate: () => __DEV__,
// transform immutable state to plain objects
stateTransformer: state => removeNavigation(fromJS(state).toJS()),
// transform immutable action payloads to plain objects
actionTransformer: action =>
action && action.payload && action.payload.toJS
? {...action, payload: action.payload.toJS()}
: action
})

let middleware = [
thunk,
logger
]

const createStoreWithNavigation = createNavigationEnabledStore({
createStore,
navigationStateKey: 'navigation',
})

const initialState = {}

const store = createStoreWithNavigation(
rootReducer,
initialState,
compose(
autoRehydrate(),
devtools(
applyMiddleware(...middleware)
)
)
)

persistStore(store, {
storage: AsyncStorage,
blacklist: ['navigation'],
transforms: [immutableTransform()]
})

export default store

This setup code is convoluted. In addition, imports and library usage are separated and are out of mind — they’re all over the place. It also means that if you ever want to change, or remove components you’ll have to hunt them down and hope you didn’t break your app; we can also see that there’s a lot of importance for how your order your integration.

After

import { createStore, combineReducers } from 'redux'
import { buildStack } from 'redux-stack'
import stack from '@/initializers'

const initialState = {}
const { reducers, enhancer } = buildStack(stack)
const store = createStore(combineReducers(reducers), initialState, enhancer)

export default store

And we have a new initializers folder:

initializers/
├── devtools.js
├── ex-navigation.js
├── index.js
├── middleware.js
├── reducers.js
├── redux-logger.js
└── redux-persist.js

The set up code is a no brainer. It is clean and reasonable and probably will look the same for every new app you make, so you can even extract that out into an npm module.

The initializers realm is separate and modular. You can remove and add any initializer you like and be certain nothing will break. If you built your app in a modular fashion, every module can “contribute” an initializer which sets itself up, and no drama happens — no other code has to change and no module to wreck by mistake.

Getting Started

You can start using redux-stack today, the implementation behind it is no more than 30 lines of code, but the idea around uniform initializers being first-class is much more profound. Go ahead and check out the Github repo for more information about using redux-stack.

Thanks Maiz Lulkin and Xavier Via for listening to my thoughts!