lowtex
lowtex

Reputation: 727

How to write Redux selectors that are both reusable and modular?

I am new to Redux and trying to figure out how to take full advantage of it.

When writing a selector for a module of the application, what part of the state tree should be passed to the selector so that the selector is both reusable and modular?

For example, given the code below, what is a good way to write selectModuleItemsById with a state shape similar to stateShapeExample?

let stateShapeExample = {
    module: {
        items: {
            firstItemId: {...},
            secondItemId: {...},
            ...
        }
    }
}

const selectModuleRoot = (state) => state.module;

// First Option: starts from the module root
const selectModuleItemById = (state, id) => state.items[id];

// Second Option: starts from the global root
const selectModuleItemById = (state, id) => state.module.items[id];

// Something Else: ???
const selectItemById = (state, id) => state[id];

Upvotes: 3

Views: 2093

Answers (2)

markerikson
markerikson

Reputation: 67469

The short answer is that it's pretty tricky.

The best writeup on this that I've seen is Randy Coulman's series of posts on modularizing selectors:

The general summary of that seems to be letting "module reducers" write selectors that know how to pick pieces of data out of their own state, then "globalizing" them at the app level based on where that module/slice is mounted in the state tree. However, since the module probably needs to use the selectors itself, you may have to move the registration / setup process into a separate file to avoid a circular dependency issue.

Upvotes: 3

Bryan Downing
Bryan Downing

Reputation: 15472

Selectors, by definition, take in the entire state and return a portion of the state. Anything else is basically just a data utility function.

I use ramda lenses to manage this kind of thing.

Consider a directory structure like this:

store
  module
    data.js
    selectors.js
    reducers.js
    actions.js

data.js would export the initial state (in this case, just the initial state for modules) and ramda lenses that describe where pieces of state are.

import { lensPath } from 'ramda'

export default {
    items: {
        firstItemId: {...},
        secondItemId: {...},
        ...
    }
}

export const itemsLens = lensPath(['module', 'items'])
export const makeItemLens = id => lensPath(['module', 'items', id])

Then, in selectors.js you import the lenses to select the data from the entire state tree.

import {view} from 'ramda'
import {itemsLens, makeItemLens} from './data.js'

export const selectModuleItems = state => view(itemsLens, state)
export const selectModuleItemById = (state, id) => view(makeItemLens(id), state)

This strategy has a few benefits:

  1. Using ramda's lensPath with view enables you to do deep property lookups without risking, Cannot read propery firstItemId of undefined errors. Other libraries have equivalent functions if ramda aint your thing (lodash, immutable.js, etc).
  2. Having the lenses alongside your initial state adds a lot of clarity for other developers.
  3. Abstracting object paths to lenses makes it a bit easier to restructure your state tree, if needed.

The downside is that it's a bunch of extra boilerplate code, but there's value in being explicit and avoiding magical code IMO.

Having said all that, you should also check out reselect for more advanced selector strategies (something I have yet to play with extensively).

Upvotes: 1

Related Questions