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
Now every time I add or delete an event all user rows are getting updated and rerendered (twice) instead of only the affected user.
You updated state.schedule.events
which is used in the input selector to your getEventsByUser
selector, so it will obviously recompute its output value.
export const getEvents = (state: RootState) => state.schedule.events; export const getEventsByUser = createSelector( getEvents, // <-- when input value changes, output value is recomputed (_: RootState, userId: string) => userId, (events, userId) => events.filter((a) => a.userId === userId) );
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.
I assumed that since the selector should be memoized that it will not rerender the EventRow for cases where the resulting filtered array didn't change.
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.
shallowEqual
utilityYou can use the shallowEqual
function exported from React-Redux to do a final equality check on the selected value:
import { shallowEqual } from 'react-redux';
...
const events: MyEvent[] = useAppSelector((state) =>
getEventsByUser(state, user),
shallowEqual
);
See Equality Comparisons and Updates for additional details.
shallowEqual
utility in custom createAppSelector
factory functionYou can also just incorporate the shallow reference equality check directly in the selector functions you create. See createSelector for details.
Example:
import {
....,
createSelectorCreator,
lruMemoize,
} from '@reduxjs/toolkit';
import { shallowEqual } from 'react-redux';
import microMemoize from 'micro-memoize';
import { RootState } from './store';
const createAppSelector = createSelectorCreator({
memoize: lruMemoize,
argsMemoize: microMemoize,
memoizeOptions: {
maxSize: 10,
equalityCheck: shallowEqual,
resultEqualityCheck: shallowEqual,
},
argsMemoizeOptions: {
isEqual: shallowEqual,
maxSize: 10,
},
}).withTypes();
...
export const getEventsByUser = createAppSelector(
[getEvents, (_: RootState, userId: string) => userId],
(assignments, user) => assignments.filter((e: MyEvent) => e.user === user)
);
const events: MyEvent[] = useAppSelector((state) =>
getEventsByUser(state, user)
);
The above example bumps the cache size from the default of 1 to 10, and incorporates the shallowEqual
utility in the equality checks.
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. Each useEffect
hook call does necessarily correlate to a render cycle.
export function EventsRow({ user }: { user: string }) {
const events: MyEvent[] = useAppSelector((state) =>
getEventsByUser(state, user)
);
useEffect(() => {
console.warn('Events Row rendered for user: ' + user);
});
return (
<div id={`user-${user}`}>
{events.map((x) => (
<div key={x.title}>{x.title}</div>
))}
</div>
);
}