I have an array of events stored in my Redux state where each event is assigned to a user. I show all users stacked in a timeline with the corresponding events in the same row. For this I created a selector to grab the events for each user by userid:
export const getEvents = (state: RootState) => state.schedule.events;
export const getEventsByUser = createSelector(
getEvents,
(_: RootState, userId: string) => userId,
(events, userId) => events.filter((a) => a.userId === userId)
);
I have a EventsRow component that grabs the events and shows them:
const events: EventDTO[] = useAppSelector((state) =>
getEventsByUser(state, user)
);
Now everytime I add or delete an event all user rows are getting updated and rerendered (twice) instead of only the affected user.
extraReducers: (builder) => {
builder
.addCase(postEvent.fulfilled, (state, action) => {
const events: EventDTO[] = action.payload as EventDTO[];
state.events = state.events.concat(events);
})
Since there can be a lot of events rendered simultaniously this can impact performance a lot. Any way to only update the affected user row?
Here’s a minimal repro: https://stackblitz.com/edit/vitejs-vite-bajabcru?file=src%2FEvents.tsx
2
Answers
One way is to normalize the data.
You can read about it here: https://redux.js.org/tutorials/essentials/part-6-performance-normalization#normalizing-data
Another way is to use
shallowEqual
. https://react-redux.js.org/api/hooks#equality-comparisons-and-updatesIssue
You updated
state.schedule.events
which is used in the input selector to yourgetEventsByUser
selector, so it will obviously recompute its output value.Any time any of the input values to a selector update, the selector function will recompute its output value. Here,
events.filter
returns a new array reference.The selector value is memoized. When neither input changes, the computed memoized result is returned. It is the new array reference that is what triggers subscribers to rerender even though the filtered array value might not have actually updated.
Solution Suggestion(s)
Use
shallowEqual
utilityYou can use the
shallowEqual
function exported from React-Redux to do a final equality check on the selected value:See Equality Comparisons and Updates for additional details.
Use
shallowEqual
utility in customcreateAppSelector
factory functionYou can also just incorporate the shallow reference equality check directly in the selector functions you create. See createSelector for details.
Example:
The above example bumps the cache size from the default of 1 to 10, and incorporates the
shallowEqual
utility in the equality checks.Additionally
Just FYI console logging in the function component body is an unintentional side-effect and does not necessarily correlate to component renders to the DOM. Any side-effects like this should be placed in a
useEffect
hook. EachuseEffect
hook call does necessarily correlate to a render cycle.