13

I use reselect to get parts of my redux state. i have a list of objects in my state. One of my subselector for my create selector is a filter function of this list:

state => state.list.filter(filterFunction)

So i pass this to my createSelector:

createSelector(
  state => state.list.filter(filterFunction),
  (subsetOfObjects) => subsetOfObjects.map(doSomething)
);

This filter function returns a subset of my objects in the list. So if the list changes, reselect always returns a new object, even if the subset didn't changed, because the list isn't the same (fully correct).

is there a possibility to get only a new object if there are changes to the objects or the filtered list?

bitrevolution
  • 437
  • 6
  • 16
  • 1
    It happens because `filter` (your input selector) always returns a new object. AFAIK, `reselect` compares the result of input selectors through `===` (`state.list.filter(filterFunction) === state.list.filter(filterFunction)`) and it will always be `false` even if the list doesn't change. – Diego Haz Jun 20 '16 at 11:37
  • To clarify, the way you've implemented that, `createSelector` ends up being useless. It will recompute on every call even if the state doesn't change, because the input selector (filter) always returns a new object. – Diego Haz Jun 20 '16 at 11:41

3 Answers3

8

After all i had an idea which could work:

const list = {


object1: {
    id: 'object1',
  },
  object2: {
    id: 'object2',
  },
  object3: {
    id: 'object3',
  },
};

const order = ['object1', 'object3'];

const selector = (...args) => args.reduce((prev, curr) => ({...prev, [curr.id]: curr}), {});

createSelector(
  state => state.order,
  order => createSelector(
    ...order.map(id => state => state.list[id]),
    selector,
  )
);

The line ...order.map(id => state => state.list[id]), will spread the objects as arguments. They will be the same if the order-array will not be changed. So i can generate a new Object with only the Objects listed in the order.

The evaluation function of the first create Selector only gets called if the order array changes. If this happens, a recalculation of the result is necessary anyway. So this is fine. The second one only recalculates if it gets new values. The values are functions that are generated out of the order array. Even if the list object changes (due to more entries or smaller changes on other objects that are not considered in the current list), the pointer to the objects of the order array stays the same. So we always get a the same objects as arguments to our second evaluation function. This prevents an unwanted update.

bitrevolution
  • 437
  • 6
  • 16
0

Just use re-reselect.

re-reselect is a lightweight wrapper around Reselect meant to enhance selectors with deeper memoization and cache management.

Maksim Boltik
  • 321
  • 1
  • 2
  • 6
  • Looks like she *is* using re-select...? – strickli Mar 25 '21 at 01:24
  • No, Maksim is suggesting a library called re-reselect, not reselect (easy to misread the name -- I did too at first). Details about how it applies to this specific problem would have been useful in the reply though. – Ben Jaffe Jun 07 '21 at 15:46
-1

EDIT

I was sleeping and dreamed (seriously haha) with a simple (but maybe still unnecessary) solution for this case. You must return a primitive type on the filter result, so you can JSON.stringify() it and JSON.parse() on the second selector. The following test suite passes:

const state = {
  list: [
    { id: 1, active: true },
    { id: 2, active: false },
    { id: 3, active: false },
    { id: 4, active: false }
  ]
}

const getFilteredListIds = createSelector(
  (state) => JSON.stringify(state.list.filter((object) => !!object.active)),
  (filteredList) => JSON.parse(filteredList).map((object) => object.id)
)

expect(getFilteredListIds.recomputations()).toEqual(0)
expect(getFilteredListIds(state)).toEqual([1])
expect(getFilteredListIds.recomputations()).toEqual(1)
expect(getFilteredListIds(state)).toEqual([1])
expect(getFilteredListIds.recomputations()).toEqual(1)

const newState = {
  list: [
    ...state.list,
    { id: 5, active: false } // should not change subset
  ]
}

expect(getFilteredListIds(newState)).toEqual([1]) // subset didn't change
expect(getFilteredListIds.recomputations()).toEqual(1) // pass :)

However, depending on your use case, it may be slow than filter on every call. If you test that performance, share with us.


FIRST POST

As I said in the comments, the way you've implemented that makes createSelector useless.

const state = {
  list: [
    { id: 1, active: true },
    { id: 2, active: false },
    { id: 3, active: false },
    { id: 4, active: false }
  ]
}

const getFilteredListIds = createSelector(
  (state) => state.list.filter((object) => !!object.active),
  (filteredList) => filteredList.map((object) => object.id)
)

expect(getFilteredListIds.recomputations()).toEqual(0)
expect(getFilteredListIds(state)).toEqual([1])
expect(getFilteredListIds.recomputations()).toEqual(1)
expect(getFilteredListIds(state)).toEqual([1])
expect(getFilteredListIds.recomputations()).toEqual(1) // fail

First, I made some adaptations to solve this first problem.

const state = {
  list: [
    { id: 1, active: true },
    { id: 2, active: false },
    { id: 3, active: false },
    { id: 4, active: false }
  ]
}

const getList = (state) => state.list

// it makes filter only happen if state.list changes
const getFilteredList = createSelector(
  getList,
  (list) => list.filter((object) => !!object.active)
)

const getFilteredListIds = createSelector(
  getFilteredList,
  (filteredList) => filteredList.map((object) => object.id)
)

expect(getFilteredListIds.recomputations()).toEqual(0)
expect(getFilteredListIds(state)).toEqual([1])
expect(getFilteredListIds.recomputations()).toEqual(1)
expect(getFilteredListIds(state)).toEqual([1])
expect(getFilteredListIds.recomputations()).toEqual(1)
// everything pass

Now your question is valid:

is there a possibility to get only a new object if there are changes to the objects or the filtered list?

What you want is this, right?

const newState = {
  list: [
    ...state.list,
    { id: 5, active: false } // should not change subset
  ]
}

expect(getFilteredListIds(newState)).toEqual([1]) // subset didn't change
expect(getFilteredListIds.recomputations()).toEqual(1) // fail

But the last line will fail because recomputations() would be 2.

The only way I see to solving this is making the memoized filteredList part of your state, but it may be weird.

Community
  • 1
  • 1
Diego Haz
  • 952
  • 1
  • 6
  • 24