React useSelector without re-render

React optimisation hack

Xam React CoE - Xam Consulting
3 min readMar 27, 2023

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 React CoE - Xam Consulting
Xam React CoE - Xam Consulting

Written by Xam React CoE - Xam Consulting

The Xam Consulting React Centre of Excellence aims to promote the teaching and learning of React, React Native, Typescript, NodeJS, and related technologies.

No responses yet