With mapStateToProps

Typically, you use selectors to create props objects within a mapStateToProps function. This is a core feature of react-redux's connect function. Redux-selectors provides some helper functions that make it much easier to work with some of the peculiar edge cases of mapStateToProps.

For these examples, we're going to be using a configurable selector, selectApplesBySize and a path selector, selectName. We'll be importing them into our examples. There are important differences in how you would work with both types of selectors.

Notice that we're passing filterState as the second argument to withOptions. This will prevent our inner selector from receiving the props argument, which is a benefit to memoization.

For each example below, we will be using the following selectors.

// selectors.js

import { createSelector, withOptions, filterState } from '@comfy/redux-selectors'

export const selectApplesBySize = withOptions(size => createSelector(
  'fruit.apples',
  apples => apples.filter(apple => apple.size === size)
)), filterState)

export const selectName = createSelector('app.currentUser.name')

For each example below, presume the state and ownProps looks like this:

const state = {
  fruit: {
    apples: [
      { id: 1, size: 'big' },
      { id: 2, size: 'small' },
      { id: 3, size: 'medium' }
    ]
  }
  app: { currentUser: { name: 'Buddy' } }
}
const ownProps = { size: 'big' }

Basic example

Here you can see a bare-bones example that isn't using any convenience functions. Our mapStateToProps function simply uses state and ownProps to create a props object. Most of the time, this format gives you the most control and utilizes the built-in memoization that mapStateToProps provides.

import { selectApplesBySize, selectName } from './selectors'

const mapStateToProps = (state, ownProps) => ({
  apples: selectApplesBySize(ownProps.size)(state),
  name: selectName(state)
})

mapStateToProps(state, ownProps) // => { apples: [{ id: 1, size: 'big' }], name: 'Buddy' }

Using combineSelectors

Notice that you could rewrite the mapStateToProps function above using combineSelectors. The main benefit is that you have the option to use path strings as well as selector functions (not shown here).

We need to wrap selectApplesBySize to pass the size from ownProps manually. Notice also that selectName is just a function reference. combineSelectors will call each of your selector functions for you! Under the hood, combineSelectors is memoized to prevent recalculating the props object if state and ownProps have not changed.

import { combineSelectors } from '@comfy/redux-selectors'
import { selectApplesBySize, selectName } from './selectors'

const mapStateToProps = combineSelectors({
  apples: (state, ownProps) => selectApplesBySize(ownProps.size)(state),
  name: selectName
})

mapStateToProps(state, ownProps) // => { apples: [{ id: 1, size: 'big' }], name: 'Buddy' }

Using combineSelectors and withState

Notice that you could rewrite the mapStateToProps function above using withState and combineSelectors. The trade-off is that we can no-longer rely on ownProps for configuration values.

Most functions in redux-selectors accept variable arguments, which causes react-redux to always pass ownProps. Under the hood, react-redux will inspect the function that you pass and cache it differently based on the number of arguments it accepts. In order to rely strictly on state and ignore ownProps, you can use withState, which simply returns a function that accepts only a state argument. This can lead to a performance improvement in cases where your state is very stable and ownProps changes often.

Below, you can see that we're no long depending on ownProps. In cases where you don't need ownProps, you will achieve a slight performance boost by wrapping your combineSelectors with withState. Note: we've hard-coded selectApplesBySize to always be "big".

import { combineSelectors, withState } from '@comfy/redux-selectors'
import { selectApplesBySize, selectName } from './selectors'

const mapStateToProps = withState(combineSelectors({
  apples: selectApplesBySize('big'),
  name: selectName
}))

mapStateToProps(state, ownProps) // => { apples: [{ id: 1, size: 'big' }], name: 'Buddy' }

Using combineSelectors and withState and withProps

Interestingly, withState can be used to wrap any type of selector. Here we're wrapping selectName in withState to prevent it from receiving a props argument. In this case that's not very useful because it's not a memoized selector. However, this pattern can be used to force a selector to memoize by state only, even when combineSelectors receives ownProps. This is useful in cases where some selectors require ownProps and other do not.

We're also wrapping selectApplesBySize in withProps to tell it to get its configuration from props instead. You can see that we're passing a 'size' selector to tell withProps to pass only the size value from ownProps.

import { combineSelectors, withProps, withState } from '@comfy/redux-selectors'
import { selectApplesBySize, selectName } from './selectors'

const mapStateToProps = combineSelectors({
  apples: withProps(selectApplesBySize, 'size'),
  name: withState(selectName)
})

mapStateToProps(state, ownProps) // => { apples: [{ id: 1, size: 'big' }], name: 'Buddy' }

Wrapping combineSelectors in withProps

In cases where you are mixing configurable selectors with state selectors, you can improve memoization if you wrap combineSelectors with withProps as shown below. React-redux will recompute your selector every time state and ownProps are changed. However, our inner selectors only need to be recomputed when state changes. Using withProps allows us to have more control over when our selectors need to recompute.

Also notice below that we're redefining ownProps for each call to mapStateToProps. Technically these are different objects. Normally, memoizeSelector will recompute your selector when it sees different objects, even though they have the same values. Fortunately, withProps is memoized by the real value rather than checking object equality. With this setup, your selector will avoid being recomputed in cases where state is the same and ownProps is technically a different object but has the same values.

If you want to use the props-creator pattern shown in the previous example, you can save your self some trouble and use withProps.

import { combineSelectors, withProps } from '@comfy/redux-selectors'

const mapStateToProps = withProps(props => combineSelectors({
  apples: selectApplesBySize(props.size),
  name: selectName
}))

mapStateToProps(state, ownProps) // => { apples: [{ id: 1, size: 'big' }], name: 'Buddy' }
mapStateToProps(state, { ...ownProps }) // memoized

results matching ""

    powered by

    No results matching ""