Reselect: Composing selectors?

Created on 20 Dec 2017  路  4Comments  路  Source: reduxjs/reselect

I have written the following selector for selecting a denormalized item by its id

export const selectBankItem = createSelector(
  [
    (state, itemId) => state.bank.items[itemId],
    (state, itemId) => state.bank.accounts[itemId],
    (state, itemId) => state.bank.transactions[itemId],
  ],
  (item, accounts, transactions) => ({
    ...item,
    accounts: _.toArray(accounts),
    transactions: _.toArray(transactions),
  }),
);

I'm trying to figure out how I can now write the selectAllBankItems selector without duplicating all of this code, and with caching. It doesn't seem possible to me, yet it seems like an extremely common use case (having an ItemContainer, and an ItemListContainer, and not having to duplicate 100% of the selector code, that is).

Can anyone help?

Most helpful comment

My approach was basically the same as @alizbazar's, though due to some relations being arrays-of-ids, I needed to do array-aware memoization.

That said, their first suggestion is more on the money: Create a denormalizing selector per-component and you don't need to worry about caching everything globally, which requires more work either with collection caches like they did or with something like re-reselect, which is basically the same thing but tied up neatly with a bow on top.

My own suggestion is: stick with directly interacting with normalized data in your React components, then all of this extra caching madness goes away. You end up with more connect()s in your tree, but you gain the benefit of less thinking about what needs to be cached and you deal with fewer transformations of data in your app, and with fewer chances of random store updates causing redraws in unrelated components. This has the added benefit of any data transforms you do have being colocated with the components using them.

After extensive work with React and Redux, I've come to be of the opinion that if you normalize your data, you should not bother denormalizing it. Conversely, if you want to deal with denormalized data, then don't bother normalizing it and just fetch it fresh every time. If you want to have your normalization cake and eat it simultaneously, maybe look into something like GraphQL+Relay/Apollo, which will automate this drudgery for you. If you can't use GraphQL, then see about having Swagger docs added to your API so you can build tooling to do that drudgery. If you can't do that, then you're stuck manually writing everything, which will become a maintenance and mental burden.

All 4 comments

If you're using React, usually I find this pattern working so that one smart component is using reselect to select an array of items, and then based on the itemID's it renders separate smart item components. Then each of the smart item components are using their own reselect selector to memoize single item output. Here's an example:

const Item = ({name}) => (
  <div>
    {name}
  </div>
)

const makeGetItem = () => createSelector(
  (state, props) => state.bank.items[props.itemID],
  (state, props) => state.bank.accounts[props.itemID],
  (state, props) => state.bank.transactions[props.itemID],
  (item, accounts, transactions) => ({
    ...item,
    accounts: _.toArray(accounts),
    transactions: _.toArray(transactions),
  })
)

const ItemContainer = connect(makeGetItem)(Item)


class List extends PureComponent {
  render() {
    return this.props.items.map(itemID => (
      <ItemContainer itemID={itemID} key={itemID} />
    ))
  }
}

const getItems = createSelector(
  state => state.bank.items,
  items => Object.keys(items),
)

const ListContainer = connect((state, props) => ({
  items: getItems(state, props),
}))(List)

However, reselect I think could be used even without such abstractions or using React. I would love to get feedback on this approach from others! 馃挰

const selectAllBankItems = createSelector(
  state => state.bank.items,
  state => state.bank.accounts,
  state => state.bank.transactions,
  (items, accounts, transactions) => {
    const enhancedItems = {}
    for (let itemID of Object.keys(items)) {
      enhancedItems[itemID] = getItemSelector(itemID)(items, accounts, transactions)
    }
    return enhancedItems
  }
)

const makeItemSelector = itemID => createSelector(
  (items, accounts, transactions) => items[itemID],
  (items, accounts, transactions) => accounts[itemID],
  (items, accounts, transactions) => transactions[itemID],
  (item, accounts, transactions) => {
    console.log('calculating', itemID)
    return Object.assign({}, item, {
      accounts: _.toArray(accounts),
      transactions: _.toArray(transactions),
    })
  }
)

const getItemSelector = (() => {
  const all = {}
  return itemID => {
    if (!all[itemID]) {
      all[itemID] = makeItemSelector(itemID)
    }
    return all[itemID]
  }
})()

Here's a demonstration in repl.

My approach was basically the same as @alizbazar's, though due to some relations being arrays-of-ids, I needed to do array-aware memoization.

That said, their first suggestion is more on the money: Create a denormalizing selector per-component and you don't need to worry about caching everything globally, which requires more work either with collection caches like they did or with something like re-reselect, which is basically the same thing but tied up neatly with a bow on top.

My own suggestion is: stick with directly interacting with normalized data in your React components, then all of this extra caching madness goes away. You end up with more connect()s in your tree, but you gain the benefit of less thinking about what needs to be cached and you deal with fewer transformations of data in your app, and with fewer chances of random store updates causing redraws in unrelated components. This has the added benefit of any data transforms you do have being colocated with the components using them.

After extensive work with React and Redux, I've come to be of the opinion that if you normalize your data, you should not bother denormalizing it. Conversely, if you want to deal with denormalized data, then don't bother normalizing it and just fetch it fresh every time. If you want to have your normalization cake and eat it simultaneously, maybe look into something like GraphQL+Relay/Apollo, which will automate this drudgery for you. If you can't use GraphQL, then see about having Swagger docs added to your API so you can build tooling to do that drudgery. If you can't do that, then you're stuck manually writing everything, which will become a maintenance and mental burden.

If you feel like reselect is not covering all your use cases, it might be worth exploring other options, I developed an alternative selector library called recompute that might fit your needs:

        const getAllItems = createObserver(state => state.bank.items)
        const getItem = createObserver((state, itemId) => state.bank.items[itemId])
        const getAccounts = createObserver((state, itemId) => state.bank.accounts[itemId])
        const getTransactions = createObserver((state, itemId) => state.bank.transactions[itemId])

        const selectBankItem = createSelector(id => ({
            ...getItem(id),
            accounts: _.toArray(getAccounts(id)),
            transactions: _.toArray(getTransactions(id)),
        }))

        const selectAllBankItems = createSelector(() => 
            Object.keys(getAllItems()).map(id => selectBankItem(id))
        )
Was this page helpful?
0 / 5 - 0 ratings