<Back

React Hooks

Common hooks

useRef

You can have a reference to a container that allows you to have the same value between renders. It’s a container which exposes one property, current which keeps a reference to whatever you set it to

const randomObj = {};
const renderTarget = useRef();
renderTarget.curent = randomObj;

<div ref={renderTarget}></div>

// you can also keep references to things outside a container

const randomObj = {}

const UseRefMemo = memo(function UseRef() {
	const renderTarget = useRef();
	const insideObj = {}; // this obj is getting created and destroyed
	// every render cycle, every single time
	renderTarget.current = randomObj; // this survives b/w render cycles

	return <div ref={renderTarget}></div>
}

The use case for a useRef is to keep track of the exact same thing that is === to each other so maybe some animation frame you keep track of like a three.js scene etc.

useReducer

The concept of a reduces is just a fancy way of saying “I have a function, that function takes in a bit of state, it takes an action, and it returns a different state”

In effect, it exposes these:

const [state, dispatch] = useReducer(reducer, initialArg, init?)
// optional init field to pass a function that should return
// initial state

// if not passed anything for init, initial state is set
// to the result of calling initialArg
import { useReducer } from 'react';

// fancy logic to make sure the number is between 0 and a max
const limit100 = (num, max) => (num < 0 ? 0 : num > max ? max : num);

const step = 20;

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT_H':
      return Object.assign({}, state, { h: (state.h + step) % 360 });
    case 'DECREMENT_H':
      return Object.assign({}, state, { h: (state.h - step) % 360 });
    case 'INCREMENT_S':
      return Object.assign({}, state, { s: limit100(state.s + step, 100) });
    case 'DECREMENT_S':
      return Object.assign({}, state, { s: limit100(state.s - step, 100) });
    case 'INCREMENT_L':
      return Object.assign({}, state, { l: limit100(state.l + step, 100) });
    case 'DECREMENT_L':
      return Object.assign({}, state, { l: limit100(state.l - step, 100) });
    default:
      return state;
  }
};

const UseReducerComponent = () => {
  const [{ h, s, l }, dispatch] = useReducer(reducer, { h: 50, s: 50, l: 50 });
  return (
    <div className='page use-reducer'>
      <div class='btn-groups'>
        <div className='btn-group'>
          <span class='btn-label'>Hue</span>
          <button onClick={() => dispatch({ type: 'INCREMENT_H' })}>➕</button>
          <button onClick={() => dispatch({ type: 'DECREMENT_H' })}>➖</button>
        </div>
        <div className='btn-group'>
          <span class='btn-label'>Saturation</span>
          <button onClick={() => dispatch({ type: 'INCREMENT_S' })}>➕</button>
          <button onClick={() => dispatch({ type: 'DECREMENT_S' })}>➖</button>
        </div>
        <div className='btn-group'>
          <span class='btn-label'>Lightness</span>
          <button onClick={() => dispatch({ type: 'INCREMENT_L' })}>➕</button>
          <button onClick={() => dispatch({ type: 'DECREMENT_L' })}>➖</button>
        </div>
      </div>
    </div>
  );
};

export default UseReducerComponent;

NOTE: Object.assign() changes the object in place vs { ...spread, ...extend } creates a new POJO object, thus compromising referential equality.

Another related difference is that spread defines new properties, whereas Object.assign() sets them. For example, Object.assign() calls setters that are defined on Object.prototype, whereas the spread operator does not.

useMemo

Let’s say you’re displaying the time within a component that has to do heavy math calculations (like a recursive Fibonacci call every time someone increments count) — the state changes every second but computing values every second will crash your browser and render out an unusable UI.

This is where useMemo comes in, where it can cache expensive computations if a particular component is re-rendered frequently.

const value = useMemo(() => expensiveMathOperation(count), [count]);

Only use this when you have a performance problem, otherwise let React do it’s thing. If you start seeing janky UI (can’t scroll, stuttering, activities locking of the main thread).

Remember, useMemo stores computed values, and doesn’t actually cache the function specifically. In order to cache functions, leverage useCallback

useCallback

This hook is a permutation of useMemo, you can actually implement useCallback using useMemo.

const aUsefulCallback = () => {};
const memoizedCallback = useCallback(aUsefulCallback, []);
// is the same as below, but notice how useCallback takes in the function itself
// and useMemo requires a function to evaluate to what we want to store!
// useMemo = computed values cached, useCallback = function cached
const memoizedCallbackV2 = useMemo(() => aUsefulCallback, []);

useMemo memoizes on the return values of the callback being passed, whereas useCallback memoizes on the function itself.

Suppose you have a parent component which updates a count state at the click of a button, and then this state is passed to the child component responsible for mapping over a list of strings and rendering out count number of list items as below:

const items = [
  { id: 0, content: 'haha' },
  { id: 1, content: 'hoho' },
  { id: 2, content: 'hehe' },
  { id: 3, content: 'hyhy' },
];

// Parent component
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [reverseCount, setReverseCount] = useState(100);

  const getListItems = () => {
    return listItems[count];
  };

  return (
    <div>
      <p>{reverseCount}</p>
      <button onClick={() => setReverseCount(reverseCount - 1)}>
        Decrement
      </button>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ListItemComponent getListItems={getListItems} />
    </div>
  );
}

// Child component
function ListItemComponent({ getListItems }) {
  const [items, setItems] = useState([]);

  useEffect(() => {
    setItems([...items, getListItems()]);
  }, [getListItems]);

  return (
    <div>
      {items.map((item) => (
        <p key={item?.id}>{item?.content}</p>
      ))}
    </div>
  );
}

Notice how there’s another state reverseCount that doesn’t affect the child or how it would render the list items? Now imagine we increment by 2 and list item 1 and 2 get rendered thus far.

Now, if we were to hit the decrement button, and trigger re-renders, a side-effect would be that list item 2 would get spit out again even though the state being changed is unrelated to the one our child component cares about.

This is due to the fact that we are passing down a function as a prop to the child component and whenever the parent re-renders, the function gets re-created

In order to fix this issue, we only want to re-create the function if the count state changes and not when any other state updates.

const getListItems = useCallback(() => {
  return listItems[count];
}, [count]);

useLayoutEffect

useLayoutEffect is almost the same as useEffect except that it’s synchronous to render as opposed to scheduled like useEffect.

A useEffect hook is NOT synchronously afterwards even though it generally occurs after a render ends. There usually is some pause (ms) before your function is run. It’s problematic for UIs which contain rapidly updating feedback loops like animations. useLayoutEffect allows us to not wait that extra ms or frame that we’d normally need to wait with useEffect.

Note: If you make the useLayoutEffect into a useEffect it will have a janky re-render where it’ll flash before it renders correctly. This is exactly why we need useLayoutEffect.

useId

A new hook for version 18 of React is useId. Frequently in React, you need unique identifiers to associate two objects together. An example of this would be making sure a label and an input are associated together by the htmlFor attribute.

Previously you could maintain some sort of unique counter that was tracked across renders. With concurrent React and batching in version 18 that’s no longer possible. useId will give you a consistent via a hook so that they can always be the same.

This is useful for the thing we see above: we have a label which needs a for attribute that corresponds to an input. We would either need to use some piece of data/parameter that we’d pass into the component that would serve as the key or we can use this hook to give it a unique ID.

If you need multiple IDs in the same component just do {id}-name, {id}-address, {id}-number , etc. No need to call useId multiple times.

This is safe across server-side renders and client-side.

Obscure Hooks

useImperativeHandle

Imagine you make a super fancy input component as part of your design system. Imagine that a parent element (i.e. the component that renders the fancy input) that needs to call focus() on the fancy input because of a validation error.

This is what useImperativeHandle is for. It allows a child component to expose a method (I used focus as an example but could be anything). You pass in a ref from useRef to the child component, it uses that ref to pass back methods to the parent. If you make libraries or design systems, this is useful.

useDebugValue

useful for testing out customHooks, expose values out to React devtools.

useSyncExternalStore

Synchronize external libraries like Redux, Mobx, etc. with React through subscriptions. This is mostly to be used by library devs, not app devs.

useInsertionEffect

useLayoutEffect happens after rendering vs useInsertionEffectwhich happens before rendering. Mostly used for CSS-in-JS libraries like emotion and styled-components. Again, not really meant for use by app devs.

useDeferredValue

This one and useTransition center around low priority updates. A good example of these is type-ahead suggestions. Type-ahead is not super important, and often a user is typing fast enough that lots of suggestions are getting thrown away as they type.

It therefore is a low priority UI update and we should not lock up the entire UI trying to render low priority work. This is what useDeferredValue allows you to do. It allows you identify data which would cause a re-render as “this can be interrupted, if you have something else happen while this is trying to compute, do that other stuff first and then come back to this.”

useTransition

Likewise to useDeferredValue, it allows you to set up “low priority” updates. useTransition gives you back a function to start a transition that can be interrupted if something higher priority comes up, like a user clicking somewhere. After you start that transition, it will give you back a boolean isCurrentlyTransitioning flag that will allow you to show a spinner while this transition is being delayed. Once React has cleared everything out and gotten to the low priority transition, everything will settle into a normal state.

Like above, this creates some indirection in how your app renders and I don’t choose to use this very frequently. If you have some low priority things to render at the bottom of your page that are expensive to render (think like a comment section at the bottom of an article) then this would be a good case to use that.