Rarely Used React Hooks

Last updated on September 2022

I think almost all React devs are familiar with useState(), useCallback(), useEffect() etc. But there are quite a few more than are less common to see. They appear a lot in libraries, and definitely have their use cases.

Here is a list of React hooks that are less commonly used. (Some more than others).

*Its basically a list of the built in hooks except state/callback/memo/effect/context/ref... If i am honest I wouldn't say all on this page are "rarely" used. *

useDeferredValue() to stop laggy renders

This is one that I think should be used more often.

It is similar to a debounce function.

It works like a wrapper around a value, and if the value changes it will return the old value for a while. Once there are no updates, it will then return the new value.

If you have this in your component:

const deferredValue = useDeferredValue(value)

During the initial render, the deferred value will be the same as the value you provided.

During updates, the deferred value will “lag behind” the latest value. In particular, React will first re-render without updating the deferred value, and then try to re-render with the newly received value in background.

This is useful if you had a text input, and you don't want or need to run changes until the user has stopped typing.

The following example shows it in use. Without useDeferredValue(), it is very laggy when typing quickly as between each stroke it will generate an array of 20k items and render them on the screen. But with useDeferredValue() it feels much smoother - you can update the text input as quick as you like, and it won't render until you stop typing. It feels natural and without lag.

import {useDeferredValue, useState} from 'react';

export default function App() {
  const [name, setName] = useState("")

  function updateName(event) {
    setName(event.currentTarget.value)
  }
  
  return (
    <>
      <label>
          Enter your name:
          <input onChange={updateName} />
      </label>
      <DoSomethingSlowly name={name} />
    </>
  );
}

function DoSomethingSlowly({name}) {
  const deferredName = useDeferredValue(name); // << the magic!
  console.log({name, deferredName});   
  if(!deferredName) return;

  // do something slowly:
  const items = Array.from({length: 5_000}).map((_,i)=>deferredName.toUpperCase())

  // output all of those items:
  return (<ul>
    {items.map(item => <li>{item}</li>)}
  </ul>);

}

If you type something in the text input, the console.log in DoSomethingSlowly will always log the up to date name, but the deferredName will be the old value until you stop typing.

useId() to get unique IDs to reference in HTML

Sometimes we need to use and reference IDs in HTML, such as when using some aria attributes, or <label htmlFor="">

The aria-describedby attribute takes in an ID, and it expects one element to exist with that ID.

  <input type="password" aria-describedby="password-hint" />
  // ...
  <p id="password-hint">Enter your password</p>

If you know and can guarantee only one element with that ID is in the DOM then you can get away with manually setting a fixed value (like that example). But sometimes you might not be able to know that, so then you might end up creating a random one and using useState() and using that.

Note: even if you can guarantee only one element with that ID, it is still better to not hard code it

There is a nicer and much cleaner way. useId()

function PasswordField() {
  const elementId = useId();
  
  return (<>
      <input type="password" aria-describedby={elementId} />
      <p id={elementId}>Enter password...</p>
    </>)
}

You can then include that <PasswordField /> as many times as you want on a page, and they will always have unique IDs.

🚨️ Warning:

if using server side rendering, useId requires exactly the same component tree on the server and the client side when you use server rendering. If they do not match, the IDs will not match either.

Note: If you have multiple items in your component that need a unique id, you can use that ID and append the field name, like this:


function YourComponent() {
    const id = useId()
    
    return <div>
        <label htmlFor={`${id}-email`}>Email</label>
        <input id={`${id}-email`} name="email" />
        
        <label htmlFor={`${id}-phone`}>Phone</label>
        <input id={`${id}-phone`} name="phone" />
    </div>
}

useDebugValue() to add labels to React Dev Tools

Hopefully every React dev is aware of the React Dev Tools extension.

useDebugValue is a React Hook that lets you add a label to a custom Hook in React DevTools.

Example of using it:

import { useDebugValue } from 'react';

function useOnlineStatus() {
  // ...
  useDebugValue(isOnline ? 'Online' : 'Offline');
  // ...
}

It takes 2 arguments, the label and an optional "format" arg. This is useful if you want to debug an object and have it formatted in a human readable way. The formatting will be deferred (until its value is shown in React Dev Tools).

You can use it like this:

useDebugValue(date, date => date.toDateString());

useTransaction to update the state without blocking UI

useTransition() is a React Hook that lets you update the state, but without blocking your UI.

It returns two items - a boolean indicating if there is a pending transition, and a function to call when you want to mark a state update as a transition.

const [isPending, startTransition] = useTransition()

This is a recent addition (React 18 - last year) as part of the concurrency updates.

Use it to tell React that a state change has low priority. The state will get updated - but maybe after other rendering is done first.

const allItems = [ /* ... */ ] // huge array! 

function filterItems(searchTerm) {
    // slow function that returns only
    // items that include searchTerm
    return items.filter(item => item.title.includes(searchTerm))
}
function App() {
  const [isPending, startTransition] = useTransition();
  const [searchTerm, setSearchTerm] = useState('');
  const searchResults = filterItems(searchTerm);

  function updateFilterHandler(event) {
    startTransition(() => {
        // update state, but within the low priority transition
        setSearchTerm(event.target.value);
    });
  }

  return (
    <div>
        <label>
            Search:
            <input type="text" onChange={updateFilterHandler} />
        </label>
        
        {isPending && <p>Loading/updating...</p>}
        
        <ul>
            {searchResults.map(result => <li>{result.title}</li>)}
        </ul>
    </div>
  );
}

useTransaction vs useDeferredValue: They both seem similar! useTransition to tell React a state update has low priority. useDeferredValue when something isn't updating the state, but needs to be deferred

useSyncExternalStore() to subscribe to events from external data

You can use useSyncExternalStore() to subscribe to an external source of data.

It takes required 2 parameters (and a optional getServerSnapshot parameters)

  • The first is a subscribe function. When called it should subscribe to changes from the store, and it should return a function to unsubscribe
  • The second parameters is the getSnapshot function, which when called should return the latest data

The subscribe function should accept one argument - a callback that should be run when the data changes.

Example:

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

function TodosApp() {
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  // ...
}

You can also use this to subscribe to changes in the browser. You can write wrappers around some web APIs. This is an example of how to subscribe to changes to navigator.onLine which returns the online status of the browser (see docs for details).

import { useSyncExternalStore } from 'react';

export default function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  
  return <div>{isOnline ? 'Connected to internet' : 'Disconnected from internet'}</div>;
}

function getSnapshot() {
   // the getSnapshot parameter (2nd param) should return
   // the data...
   return navigator.onLine;
}

function subscribe(callback) {
  // call the `callback` function when going on/offline
  // then React will know to call the `getSnapshot()` function
  // to get the current value.  
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

This hook is useful inside custom hooks (but it works in components too).

useImperativeHandle() to customize a ref

This is probably one of the more well known ones in this page (although I don't see it used in many places except libraries).

You can use useImperativeHandle along with forwardRef to customize a ref handle.

This is used when you have two components (parent/child). Parent needs a ref, so create one there. Pass it to the child component. The child component wraps itself in React.forwardRef(), and inside the component (function), calls React.useImperativeHandle(), passes in the passed down ref, and set up an object (with functions, like blur() in this example). Then the parent can call those function(s) defined in the child.

It's probably easier to understand by looking at an example!

Example:

const InputComponent = React.forwardRef((_props, ref) => {
  const [val, setVal] = React.useState('');
  const inputRef = React.useRef();

  React.useImperativeHandle(ref, () => ({
    blur: () => {
      inputRef.current.blur();
    }
  }));

  return (
    <input
      ref={inputRef}
      val={val}
      onChange={e => setVal(e.target.value)}
    />
  );
});

const App = () => {
  const ref = useRef(null);
  
  const onBlur = () => {
    ref.current.blur();
  };

  return <InputComponent ref={ref} onBlur={onBlur} />;
};

useLayoutEffect() is like useEffect, but runs before the browser repaints

useLayoutEffect() is very similar to the commonly used useEffect, except that fires just before the browser repaints the screen (after React has made updates to the DOM)

It is useful if you need to calculate sizes or dimensions.

If you use getBoundingClientRect() within useEffect(), it would be before the DOM changes were applied and the measurements might be incorrect when it is applied and rendered.

Apart from when it runs, it is the same as useEffect() and takes the same parameters.

Example usage:

import { useState, useRef, useLayoutEffect } from 'react';

function TooltipMessage() {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
  }, []);
  
  // ...

Warning: If possible you should try and use useEffect(). It is uncommon to use useLayoutEffect(), and misusing it can lead to performance issues.


note: some examples based heavily on examples from the React docs. I recommend checking them out if you want to understand them better, and have a play with the examples on their documentation.

© 2019-2023 a5h.dev.
All Rights Reserved. Use information found on my site at your own risk.