foralobo
foralobo

Reputation: 3957

Reselect: createSelector not working correctly

I have a problem with my memoized selectors.

Reading the docs on https://redux.js.org/usage/deriving-data-selectors I taken this snippets:

const state = {
  a: {
    first: 5
  },
  b: 10
}

const selectA = state => state.a
const selectB = state => state.b

const selectA1 = createSelector([selectA], a => a.first)

const selectResult = createSelector([selectA1, selectB], (a1, b) => {
  console.log('Output selector running')
  return a1 + b
})

const result = selectResult(state)
// Log: "Output selector running"
console.log(result)
// 15

const secondResult = selectResult(state)
// No log output
console.log(secondResult)
// 15

My problem is that the secondResult function, log the result.

All this is a little premise.

My very problem:

but...

When I dispatch an update with "adapter.updateOne", the standard selector "SelectAll" every time (i think) changes ref.

Example

I have this selectors from "slice"

export const {
    selectAll,
    selectIds: selectTodosIDs,
    selectTotal: selectTodosCount,
    selectById: selectTodoById
} = todosSelector;

If I create this selector

export const selectIdsCustom = createSelector(
    selectTodosIDs,

    (ids) => {
        console.log('execute output function');
        return ....
    }
)

It' all ok (state.todos.ids not change obviously).

If I create this selector:

export const selectTodosCustom = createSelector(
    selectAll,

    (todos) => {
        console.log('execute output function');
        return ....
    }
)

selectTodosCustom run "always".

Whyyy???

With updateOne I am modifing "only" an entity inside "state.todos.entities"

Where am I wrong ?? What I did not understand?

My app is just un case study. The complete app is on: https://codesandbox.io/s/practical-hermann-60i7i

I only created the same app in typescript and, when I as my app had this problem:

But I have the some problem also in the official example!!!!!

Problem is my env? some library version?

Upvotes: 0

Views: 2811

Answers (1)

Denwakeup
Denwakeup

Reputation: 266

When I dispatch an update with "adapter.updateOne", the standard selector "SelectAll" every time (i think) changes ref.

Yes, you are right. It is correct. selectAll from @reduxjs/toolkit depends on ids and entities from the entities state.

{
  ids: ['1', '2'],
  entities: {
    1: {
      id: '1',
      title: 'First',
    },
    2: {
      id: '2',
      title: 'Second',
    },
  },
}

Every time you dispatch an update with adapter.updateOne, the reference to the entities object changes. This is normal, this is how immerjs (used under the hood of reduxtoolkit) provides correct immutability:

dispatch(updateOne({ id: '1', changes: { title: 'First (altered)' } }));

const state = {
  ids: ['1', '2'],
  entities: { // <--- new object
    1: { // <--- new object
      id: '1',
      title: 'First (altered)',
    },
    2: { // <--- old object
      id: '2',
      title: 'Second',
    },
  },
};

If the entities object remained old, the selector selectAll would return a memoized value with an incorrect title for the first element.

To optimize the re-render of the list (which in actually useful only for large lists), you should use selector selectIds in the parent component and selector selectById in the child components.

const Child = ({ id }) => {
  const dispatch = useDispatch();
  const book = useSelector((state) => selectById(state, id));

  const handleChange = useCallback(() => {
    // the parent component will not be re-render
    dispatch(
      bookUpdate({
        id,
        changes: {
          title: `${book.title} (altered)`,
        },
      })
    );
  }, [dispatch, id, book]);

  return (
    <div>
      <h2>{book.title}</h2>
      <button onClick={handleChange}>change title</button>
    </div>
  );
};

function Parent() {
  const ids = useSelector(selectIds);

  return (
    <div>
      {ids.map((id) => (
        <Child key={id} id={id}></Child>
      ))}
    </div>
  );
}

UPDATE

In this case, item not change ref and in "entities" I am changing a single prop inside it.

It is not right???

No, you can't do that when using redux. Redux is based on the idea of immutability. Any state change creates a new state object. Updating an entity is just a deep change of the state object. And all objects on the path to the updated element must be new. If you do not follow this rule, then base tools will not work correctly. All of them use strict comparison by default.

But you can still avoid re-render the component even when the selector returns the equal arrays with different references. Just pass your own comparison function:

...  
const todoIds = useSelector(
    selectFilteredTodoIds, 
   // the component will be re-render only if this function returns false
    (next, prev) => next.every((id, index) => id === prev[index])
);
...

Upvotes: 1

Related Questions