React useSelector without re-render
React optimisation hack
Why?
Sometimes you want to use a redux useSelector
hook to collect updated values while NOT causing a re-render.
For example, to collect updated state data for later use in an event handler.
useSelector
will always cause a re-render if its comparison/equality function (the optional second arg) returns false.
Here’s how you do it…
const selectedUsersRef = useRef<User[]>([]);
// useSelector without re-render
useSelector(
(state) => state.user.selectedUsers,
(_, b) => {
selectedUsersRef.current = b; // <- collect the new value
return true; // <- prevent re-render
}
);
const handleSubmit = useCallback(() => {
const selectedUsers = { ...selectedUsersRef.current };
// . . .
}, []);
So what’s happening here?
The optional second argument to useSelector
is a function that takes 2 args — the previous value and the current value — and returns true if they are equal (unchanged) or false if they are unequal (changed).
The default equality function, if not passed, is basically a === b
.
From the react-redux code:
export declare type EqualityFn<T> = (a: T, b: T) => boolean;
// . . .
const refEquality: EqualityFn<any> = (a, b) => a === b
// . . .
function useSelector<TState, Selected extends unknown>(
selector: (state: TState) => Selected,
equalityFn: EqualityFn<Selected> = refEquality
): Selected {
// . . .
}
So, we can leverage said equality function to load the result into a useRef
MutableRefObject.
The purpose of useRef
is for managing variables that you don't want to cause re-renders when changed.
Make it reusable
I made a useSelectorRef
hook for it 😎 …
I’m using Redux Toolkit so you may need to tweak the imports …
import { useRef } from 'react';
import type { MutableRefObject } from 'react';
import { useSelector } from 'hooks';
import type { RootState } from 'app/store';
// useSelector without re-render
export default function useSelectorRef<T = unknown>(
selectHandler: (state: RootState) => T
): MutableRefObject<T> {
const ref = useRef<T>();
useSelector<T>(selectHandler, (_, b) => {
ref.current = b;
return true;
});
return ref as MutableRefObject<T>;
}
Usage example
Assuming the above hook code is saved to hooks/useSelectorRef.ts
import { useCallback } from 'react';
import { Button } from '@mui/material';
import { useDispatch } from 'hooks';
import useSelectorRef from 'hooks/useSelectorRef';
import { selectSelectedUsers, notifyUsers } from 'features/user';
import type { User } from 'models';
export default function SubmitUsersComponent() {
const selectedUsersRef = useSelectorRef<User[]>(selectSelectedUsers);
const dispatch = useDispatch();
const handleSubmit = useCallback(() => {
const users = { ...selectedUsersRef.current };
dispatch(notifyUsers(users));
}, [dispatch]);
return <Button onClick={handleSubmit}>Submit</Button>;
}
In the above example, no matter how many times the selectedUsers
redux state object is externally updated, this SubmitUsersComponent
won’t re-render.
Kudos
The following people and entities gave their time and resources to contribute to this publication:
- Xam Consulting Australia
- Xam React Center of Excellence
- Eugene Kerner · Lead Consultant · Xam Consulting