Pandaiolo
Pandaiolo

Reputation: 11568

Select inter-dependant data in redux store with reselect createSelector

So, I am using redux, and reselect tool createSelector to memoize my selectors in mapStateToProps, and it is great.

Now, I have some normalized data in my store, and a container that needs inter-dependant data from the store.

I am doing this, for the moment, and it works:

const getItemId = props => parseInt(props.match.params.itemId, 10)

const containerSelector = createSelector(getItemId, itemId =>
  createSelector(
    [getUser, getItem(itemId)],
    (user, item) =>
      createSelector(
        [
          getFoo(item.fooId),
          getBar(item.barId)
        ],
        (foo, bar) => ({ user, item, foo, bar })
      )
  )
)

const mapStateToProps = (state, ownProps) =>
  containerSelector(ownProps)(state)(state)

export default connect(mapStateToProps)(Container)

Question: is there a better way to do this?

  1. more readable?
  2. Better practice?
  3. More performant? (is this performant?)

Edit:

Thanks to Tho Vu answer, I could made it simpler and memoized, like this:

import { getUser, getItemById, getFooById } from 'selectors'

// Note: getStuffById are selector factories like: 
//   const getStuffById = id => state => state.stuff.objects[id] 

const itemIdSelector = (_, props) => parseInt(props.match.params.itemId, 10)

const itemSelector = (state, props) => {
   const itemId = itemIdSelector(state, props)
   return getItemById(itemId)(state)
}

const fooSelector = (state, props) => {
   const item = itemSelector(state, props)
   return item ? getFooById(item.fooId)(state) : null
}

const mapStateToProps = createStructuredSelector({ 
  user: getUser, 
  itemId: itemIdSelector, 
  item: itemSelector,
  foo: fooSelector
})

But I am still mixed about it? Or maybe it is fine already?

Upvotes: 4

Views: 738

Answers (1)

blaz
blaz

Reputation: 4068

From the documentation of reselect:

When a selector is connected to a component with connect, the component props are passed as the second argument to the selector

So the first createSelector layer used for getting itemId is actually unnecessary, which you can replace with:

const getItem = (state, props) => {
   const itemId = parseInt(props.match.params.itemId, 10)
   return getItem(itemId)
}

Secondly, createSelector has following attribute:

Selectors created with createSelector have a cache size of 1. This means they always recalculate when the value of an input-selector changes, as a selector only stores the preceding value of each input-selector.

Which means your custom selector will recalculate with every changes of user, itemId, item, foo, bar. It is hard to say about the rest of input-selectors, but I don't see any relationship between user and the others. Thus you should create a separate selector for only user.

Also, the purpose of createSelector is to create a selector with retainable memoization, so nesting them the way you do will cause inside selector to be recreated every time outer ones recalculate.

What I normally do is flatten them like this:

const getItem = (state, props) => {
   const itemId = parseInt(props.match.params.itemId, 10)
   return getItem(itemId)
}

const userSelector = createSelector(getUser, user => user)
const itemSelector = createSelector(getItem, item => item)

foo and bar are more difficult, because in your code, the selectors look for changes in item, item.fooId and foo too recalculate. We still need to nest the selectors, but separation of foo and bar is required:

const fooFromItemSelector = createSelector(itemSelector, item => getFoo(item.fooId))
const fooSelector = createSelector(fooFromItemSelector, foo => foo)

and same goes for bar.

Lastly, you will want to group them together to create containerSelector:

import { createStructuredSelector } from 'reselect'

const containerSelector = createStructuredSelector({
    user: userSelector,
    item: itemSelector,
    foo: fooSelector,
    bar: barSelector
})

Upvotes: 2

Related Questions