Structuring Redux Selectors

Category:

There have recently been some posts about how to structure the redux parts, especially how to handle selectors. My way of doing it is pretty similar, but I do even more automation when globalizing the selectors. I’m really satisfied with it and haven’t encountered any issues yet.

What do we want from selectors?

First of all, let’s recap some common issues / patterns that need to be addressed:

  1. Components access the state only via selectors and every selector should take the root state as an argument. This also holds true for sub-reducers composed with combineReducers. This way components are completely decoupled from the internal state representation.
  2. The first point leads to writing global selectors taking the root state as an argument, but for ease of development we would rather write local selectors taking a child of the root state as an argument. This way we don’t have to unpack the state, and can make sure the sub-reducers are correctly decoupled by preventing access to other parts of the root state.
  3. There should be a mechanism that maps local selectors to global ones, so we only have to implement selectors once and touch a single file. This way we don’t have code redundancy, are more time efficient, and I get to keep my sanity.

Redux File Structure

Take a look at the following state:

Redux state

The state has three sub-states: navigation, notifications, settings. These should (mostly) be independent of each other and we can create three reducers and combine them with combineReducers. I organize my redux files the same way by splitting the store into three sub-folders, one for each reducer.

Redux state

Each sub-folder contains its reducer, its local selectors and its actions. The store folder contains a selectors.js file that gathers all the local selectors and globalizes them. The store’s index.js contains middleWares, combineReducers and creates the store with createStore.

Reducer and selectors in same file?

Some people like to put the selectors into the reducer file, but I prefer keeping them separate. At first, I did this to avoid circular import dependencies, but if you encounter them here it’s probably bad design. For instance, assume you need to access the settings to consume notifications in the notification reducer. If you import some (global) settings selector from within the reducer, you ‘ll get a cyclic dependency graph which cannot be resolved.1

Redux state

You can avoid this by defining thunks in actions.js that retrieve the important part of the settings themselves via the global selectors, and then pass it as part of the payload along with the action. So whether to put selectors and reducers into the same file, is more of a personal preference in my opinion.

Globalizing Selectors

The only thing left is how to create the global version of the selectors without duplicating code and while only touching the local selector files. This can be done by exporting a new set of functions with the same name, whose only purpose is to do the state selection and then invoke the local selector.

// store/navigation/selectors.js
export function getActiveScene(state) {
  return state.activeScene
}
// store/selectors.js

/**
 * Accumulates all the different selectors
 */
import * as navigationSelectors from './navigation/selectors'
import * as notificationSelectors from './notifications/selectors'
import * as settingsSelectors from './settings/selectors'

const selectors = {}
Object.keys(navigationSelectors).forEach(
    funcName => selectors[funcName] = state => navigationSelectors[funcName](state.navigation),
)

Object.keys(notificationSelectors).forEach(
    funcName => selectors[funcName] = state => notificationSelectors[funcName](state.notifications),
)

Object.keys(settingsSelectors).forEach(
    funcName => selectors[funcName] = state => settingsSelectors[funcName](state.settings),
)

// We want to be able to import like this: "import { name1, name2 } from 'selectors'"
// Below code behaves like "export {...selectors}" because of this relationship:
// var module = {}
// var exports = module.exports = {}
module.exports = selectors
// someComponent.js
import { getActiveScene } from '../store/selectors'
const mapStateToProps = state => ({
    activeScene: getActiveScene(state),
})

1: Webpack doesn’t recognize that the settings selector is used in a different JS execution block than the one defining the notification selectors. If you want to cheat, you could use require(./selectors).someSelector here inside your reducer function.

Hi, I'm Christoph Michel 👋

I'm a , , and .

Currently, I mostly work in software security and do on an independent contractor basis.

I strive for efficiency and therefore track many aspects of my life.