A foray into React/Redux: Creating flexible redux stores and modules

In the last few months I’ve been creating and iterating on a new front end architecture, utilising React and Redux. It’s been super fun! Not least because it’s been a monumental jump over the sparse, simplistic jQuery that has otherwise been employed for a while in my department. Going from that to a very much modern front end toolchain has certainly been interesting.

One challenge, which is the one I’m going to go into very briefly today, is how to create a redux store that can scale and be maintained with as little effort as possible. Ideally it should also be very simple to create application variants (to give some context, the internal applications we build tend to share certain major screens, but each line of business can potentially want many different unique combinations).

In order to simplify everything, two fundamental rules were established early on:

  • Redux modules can only ever be added as a direct property on the top level reducer
  • The module itself defines the key of its reducer on the store.

Now, what’s the benefit of this? Well, in Redux applications the primary way for a store subscriber to obtain data from the store is to use a function called a selector. The selector takes the store object, uses some predefined logic, and returns a value. To see how this factors in, I’ll quickly throw up the directory structure of a redux module:

/module
/components
index.js
/* React components go here */
actions.js
reducers.js
constants.js
index.js

It’s all pretty nice and contained within one directory. We don’t really want to have this discrete chunk of functionality to be dependant on anything else (common components aside), as it’d be super useful to drag and drop this into different projects, or be able to reuse it easily via application specific configuration.

Usually, in smaller applications, the store script (or at least some other script importing the store) will contain some selectors which point to the base location of a modules reducer regardless of nesting, which can then be imported by the module itself. This is fine, but it doesn’t lend itself to projects where many applications (and by extension, stores) may be present, as this method makes the module reliant on a specific store to feed it the location. This wasn’t particularly easy in our case when we could have many different application stores using a certain module.

Therefore, the solution we came up with was to make an assumption. If the module was always at the top level, the ‘base selector’ of a reducer would always be

const reducerSelector = state => state[reducerKey];

And if we defined the reducer key within the module itself (say, as a constant in the constants file), then we could also export that key when exporting our reducer:

export default {
[reducerKey]: reducer
}

then the top level store could use this key during its own combineReducers operation

const storeReducer = combineReducers({
...ourImportedReducer
})

Now, throw in some standards for how the index.js of a module exports reducers and some helper functions for dynamic requires via commonJS. Suddenly we’re able to generate a store based on config or even what is currently present in a certain folder without any hard coded imports into the application store! Plus we’ve also deftly sidestepped the need to create selectors at the top level store for an application in order to import the base location of the reducer module, since we know it’s always at the top level.

It certainly helped simplify our module organisation, and now modules are extremely painless to add. Depending on an applications configuration, we can now simply add a module to a location and have it drop straight into our application (we also have auto loading helpers for module routes)! A victory for laziness!

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.