Reputation: 3957
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
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