4

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?

Pandaiolo
  • 11,165
  • 5
  • 38
  • 70
  • I'm quite confused by `getItemById` and `getFooById` which seems to be selector factories (functions which return a new, partially configured selector), used like plain selector. If it's the case, there would be still room for improvement. – Andrea Carraro May 09 '18 at 09:56
  • Yes, that's right, selector factories based on object `id` - added an explanatory comment in example code. – Pandaiolo May 10 '18 at 11:12

1 Answers1

2

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
})
blaz
  • 3,981
  • 22
  • 37
  • Thanks for the thorough answer! Makes total sense. I had an issue with some mixed-up getters/selectors that had the wrong signature `getStuff = id => state => state.stuff[id]` and which led me to the confusion on how to use `createSelector`. I inverted and renamed to `getStuffSelector = state => id => state.stuff[id]` so I can use then in `createSelector` in a way that is very close to your answer. What do you think? – Pandaiolo May 05 '18 at 23:28
  • @Pandaiolo That should be fine I believe. – blaz May 06 '18 at 05:23
  • Ok so I think the identity pattern with `const itemSelector = createSelector(getItem, item => item)` is not working. This basically returns *selector of a selector*, because `getItem` does not returns data, but a selector. I managed to make it work by returning `getItem(itemId)(state)` instead, but then using `itemSelector` is useless, as you can just use `getItem` in `createStructuredSelector` and it will be memoized. Thoughts? – Pandaiolo May 07 '18 at 10:55
  • *(and solution in my above comment did not work either because it memoized the selector function, that never changes, whatever underlying value that it is getting later on)* – Pandaiolo May 07 '18 at 11:22