An opinionated guide to React hooks
Tags: react, hooks, bestpractices, opinion
Published: 2022-02-10 20:36:36

---
title: An opinionated guide to React hooks
published: true
description: More personal opinions on how to use React hooks
tags: react, hooks, bestpractices, opinion
//cover_image: https://direct_url_to_image.jpg
---

React API offers you multiple built-in hooks. However not all of them are equally useful. Some you can see almost in every app or a library, some others you will not need unless you are writing a super special module. React documentation gives some guidance where to use hooks, but in a super neutral format. In this article I will try to dive deeper into the real use-cases, giving my opinion on how every hook should be used.

## Basic hooks

In their docs, React already has separation on basic and advanced hooks:

### Basic

- useState
- useEffect
- useContext

### Advanced

- useReducer
- useRef
- useLayoutEffect
- useImperativeHandle
- useCallback
- useMemo
- useDebugValue

The docs do not clarify reasons for this separation, however it is important for understanding the hooks API. Basic hooks cover some common use-cases, their purpose is clear and does not cause any controversy in the discussions.

## Advanced hooks

You likely do not need to use these hooks. Almost every task can be solved without these, you will get clean and idiomatic React code. Every time you use a hook from this list, you are making a compromise and stepping off the normal "React-way". You need to have a good reason and explanation to use a hook from the advanced list. In this article we cover typical valid and invalid use-cases for advanced hooks.

## useReducer

This is a form of setState for complex values. Sometimes you store not just one value, but a combination of related values. For example, state of a data-fetching process:

```ts
interface DataFetchingState {
  data: Data | null; // fetched data
  isLoading: boolean; // whether data-fetching is in progress
  error: Error | null; // error information, if data-fetching attempt failed
}
```

This can be solved using a few separate `useState` hooks. However you may want to enforce some constraints in this state, for example prvent a combination of `{isLoading: true, error: anError}`. Previous error needs to be removed when a new data-fetching attempt begins. `useReducer` allows you to control state changes via wrapping them into `actions`. This way you can only dispatch a certain predefined set of actions, which will properly handle the respective state changes.

**When to use it?** I would recommend switching to `useReducer` when you have 3 or more related state values. Fewer values work just fine via `useState`, `useReducer` would be an overkill, it will require you to write more code to handle a simple case.

**When not to use it?** If you have multiple state values, but they all are unrelated. For example, you have multiple form fields:

```js
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
```

These fields do not depend on each other, user can fill them in any order. Even though there are 3 different values, they are not _related_, so no need for `useReducer`.

## useRef

Originally, refs in React provided a way to interact with DOM nodes directly. However, later this concept evolved into a general storage of any kind of value between component renders. `useRef` is also [recommended as a replacement for class instance properities](https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables), `this.something`, which is not available in functional components.

**When to use it?**

If you need to access a DOM node, this hook seems unavoidable, however ask yourself first — do I really need to manipulate with DOM by hand? When you go this way, you become in charge of handling state updates properly and integrate with component mount/unmount lifecycle. Basically, you are stepping off one of the greatest power in React – the VDOM. Did you check if there is an option to do the same manipulation by refactoring your CSS? Or can you just read the DOM value inside an event handler via `event.target` and therefore reduce the number of direct manipulations down to events only?

Then we also have a use-case about storing other content, not DOM nodes. Note, that assigning `ref.current = newValue` does not trigger a component re-render. If you need this, perhaps it is better to put it into `useState`?

Sometimes you put the value in ref to later use it inside effect cleanup. However, it is redundant in some cases:

```js
const observerRef = useRef();
useEffect(() => {
  observerRef.current = new MutationObserver(() => {
    /* do something */
  });
  observerRef.current.observe(document.body);
  return () => {
    observerRef.current.unobserve(document.body);
  };
}, []);
```

Using `observerRef` is redundant here. The value can be stored as a plain variable:

```js
useEffect(() => {
  const observer = new MutationObserver(() => {
    /* do something */
  });
  observer.observe(document.body);
  return () => {
    observer.unobserve(document.body);
  };
}, []);
```

This is also much shorter to write!

To sum it up, `useRef` in your components only if these conditions met:

- The value does not depend on component rendering
- The value cannot be stored inside a closure of useEffect hook

## useLayoutEffect

This is where many people may fall into the trap "misguided by the name". If the hook name contains layout, I should put all my layout operations there, shouldn't I? However, this is not always the case. The primary difference between `useEffect` and `useLayoutEffect` is the timing of the operation. `useEffect` is asynchronous and `useLayoutEffect` is synchronous. Let's look at [a simple demo](https://glitch.com/edit/#!/bustling-bony-medusaceratops):

```js
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("effect");
  }, [count]);

  useLayoutEffect(() => {
    console.log("layout effect");
  }, [count]);

  function onClick() {
    setCount((count) => {
      console.log("during update");
      return count + 1;
    });
    console.log("after update");
    Promise.resolve().then(() => {
      console.log("microtask after update");
    });
  }

  return (
    <>
      <button onClick={onClick}>increment</button>
      <div>{count}</div>
    </>
  );
}
```

This is what we see in the console after clicking the button:

```js
"during update";
"after update";
"layout effect";
"microtask after update";
"effect";
```

Effect is the most delayed operation here. It gets called when all other updates completed and you can read the final DOM state (or do any other side effects). Layout effect fires right after React finished its updates, but before browser repainted the page. It is useful to apply some adjustments before user sees fully rendered page, however beware of [forced sychronous layouts](https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing?utm_source=devtools#avoid-forced-synchronous-layouts) which may slow down the rendering performance, especially, if you call that effect often. Also, keep in mind that because layout effect is synchronous, some other operations may not be completed yet. I happened to see this code:

```js
useLayoutEffect(() => {
  // delaying operation because something is not ready yet
  const frame = requestAnimationFrame(() => {
    /*do something*/
  });
  return () => {
    cancelAnimationFrame(frame);
  };
}, []);
```

This is redundant, here we just reinvented a wheel (useEffect). This code will do the same, but much simpler:

```js
useEffect(() => {
  /*do something*/
}, []);
```

Also note if `useLayoutEffect` tries to execute during server-side rendering, [it prints you a warning](https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85). This is also likely a sign you should be using `useEffect` instead.

## useCallback

When we define an inline function inside our functional component, we are getting a new instance on each render

```js
function Demo() {
  const handler = () => {};
  return <div>something</div>;
}
```

Usually, it does not cause any inconvenience. However, sometimes it happens, most often when the handler is a dependency of `useEffect`:

```js
const handler = () => {};

useEffect(() => {
  // heavy side effect is here
}, [handler]);
```

Whenever handler changes, "heavy side effect" will be executed again. However, because handler function is inline, the change will be detected on every component render. `useCallback` comes to the rescue:

```js
// now we have the same instance of `handler` on each render
const handler = useCallback(() => {}, []);

useEffect(() => {
  // heavy side effect is here
}, [handler]);
```

However it only works that easy with `[]` in the dependencies array. More likely, there will be something, sometimes another function:

```js
const doSomething = () => {};
const handler = useCallback(() => {}, [doSomething]);
```

Now we need to useCallback-ify this too:

```js
const doSomething = useCallback(() => {}, []);
const handler = useCallback(() => {}, [doSomething]);
```

This way we are piling up a fragile pyramid of callbacks, if any of them will not memoize properly, the heavy side effect will be executed regardless our efforts. Very often it happens when we receive a value from props:

```js
function Demo({ onChange }) {
  const handler = useCallback(() => {
      onChange();
      // do something else
  }, [onChange]);

  useEffect(() => {
    // heavy side effect is here
  }, [handler]);
}

// oh no! Our side effect got out of control!
<Demo onChange={() => {}}}>
```

We might useCallback-ify the handler in the parent component too, but how do we ensure we captured all instances? The code may be split in different files and even repositories. The effort seems futile.

Fortunately, there is a more elegant solution to this problem, React documentation [mentions this](https://reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often):

```js
// custom reusable hook
function useStableCallback(fn) {
  const ref = useRef();
  useEffect(() => {
    ref.current = fn;
  }, [fn]);
  const stableCallback = useCallback((...args) => {
    return ref.current(...args);
  }, []);
  return stableCallback;
}
```

This way we are getting back to a simple dependency-free `useCallback`, which relies on `ref` to deliver the actual latest value. Now we can refactor our code and remove all manual dependency tracking:

```js
function Demo({ onChange }) {
  const handler = useStableCallback(() => {
    onChange();
    // do something else
  });

  useEffect(() => {
    // heavy side effect is here
  }, [handler]);
}
```

Now we do not have to worry about `onChange` reference, `handler` will be called with latest instance, whichever it was at the moment of calling.

**When not to use it?** Do not useCallback if you have a cascade of functions depending on each other. Consider refactoring via `useStableCallback` custom hook. For functions in `useEffect` dependencies, wrap only the the direct dependency, all other functions may remain inline arrow functions, keeping your code simple and readable.

**When not to use it?** Do not useCallback to "optimize" event handlers. There is no evidence that it improves anything. Adding event listeners to DOM-nodes is a super cheap operation, a fraction of millisecond. On the other hand, wrapping into `useCallback` is also not a free operation, it comes with a cost, more expensive than actually refreshing event handlers. React is already optimized by default, no need over-optimize by hand. If you do not trust me, make your own experiments, try to find a difference and let me know, I will be happy to learn!

## useMemo

This is a bigger brother of `useCallback`. That hook worked only for functions, this one can store any kind of values:

```js
// avoid computing fibonacci number on every render
const fib = useMemo(() => {
  return fibonacci(N);
}, [N]);
```

Sometimes you integrate with a 3rd-party library and you need to create an object instance, but this one is expensive:

```js
const ace = useMemo(() => {
  const editor = ace.edit(editorRef.current);
  editor.on("change", onChange);
}, [onChange]);
```

Note, that the hazard of dependencies from `useCallback` applies here too. Solution is also the same – wrap into stable callback

```js
const onChangeStable = useStableCallback(onChange);
const ace = useMemo(() => {
  const editor = ace.edit(editorRef.current);
  editor.on("change", onChangeStable);
}, [onChangeStable]);
```

**When to use it?** When you have a solid proof that your operation is expensive (for example, you compute fibonacci numbers, or instantiate a heavy object).

**When not to use it?** When you are unsure if the operation is expensive or not. For example, this is unnecessary:

```js
function Select({ options }) {
  const mappedOptions = useMemo(
    () => options.map((option) => processOption(option)),
    [options]
  );

  return (
    <select>
      {mappedOptions.map(({ label, value }) => (
        <option value={value}>{label}</option>
      ))}
    </select>
  );
}
```

Always bechmark your code before doing any optimizations! There will not be millions of items in `options` array (in which case we will need to talk about UX in your app). Memoization does not improve anything in render time. The code could be simplified without any harm:

```js
function Select({ options }) {
  const mappedOptions = options.map((option) => processOption(option));

  return (
    <select>
      {mappedOptions.map(({ label, value }) => (
        <option value={value}>{label}</option>
      ))}
    </select>
  );
}
```

How to `useMemo` properly: you write the code without any memoization, then confirm it is slow and this slowdown is significant (this is an important step, many potential optimizations will not pass this check). If there is a confirmed improvement, create also a test to ensure that the optimization worked and has an obsevable impact. Do not forget about `useMemo` dependencies array, any change there will waste all your efforts. Choose your dependencies carefully!

## Super advanced hooks

This section could be called "wow, what is that hook?" These hooks have super niche use-cases and if you have one, you likely already know everything this article wanted to say, but here we go anyway.

## useImperativeHandle

React tries to be a declarative framework, where you are describing _what_ you want to get and then React internally figures out _how_. However, in real world, there are many imperative APIs, for example is focusing DOM elements programmatically.

Let's say we are building an custom Input component:

```js
const Input = React.forwardRef((props, ref) => {
  return <input ref={ref} />;
});
```

It is a good practice to wrap component into `forwardRef` to allow consumers to interact with the underlying native input, for example focus it via `inputRef.current.focus()`. However, sometimes we may want to add some extra code when the native element gets focused. `useImperativeHandle` helps us to proxy the call:

```js
const Input = React.forwardRef((props, ref) => {
  const nativeInputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      nativeInputRef.current.focus();
      // do something else, if needed
    },
  }));

  return <input ref={nativeInputRef} />;
});
```

Note that this way we also encapsulating access to the underlying `<input>` element, only `focus` function is exposed. This is also useful when you want to enforce API boundaries for your components and prevent unauthorized access to element internals.

## useDebugValue

React recommends to extracting group of related hooks into a function and treat it as a custom hook. For example we created a custom `useStableCallback` hook above:

```js
function useStableCallback(fn) {
  const ref = useRef();
  useEffect(() => {
    ref.current = fn;
  }, [fn]);
  const stableCallback = useCallback((...args) => ref.current(...args), []);
  return stableCallback;
}
```

We can have multiple other custom hooks, for example `useDarkMode()`, which returns you the current color scheme of the page:

```js
const darkMode = useDarkMode();

<div style={{ background: darkMode ? "darkblue" : "deepskyblue" }} />;
```

How can we inspect the latest return value of `useDarkMode`. We can put `console.log(darkMode)`, but the log message will be out of the context. `useDebugValue` connects the value with the hook it was called from:

```js
function useDarkMode() {
  const darkMode = getDarkModeValueSomehow();
  useDebugValue(darkMode);
  return darkMode;
}
```

In React devtools we will see this value along with other components props:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8q9dcnymffpkjsc42exw.png)

_here is our hook in the bottom left corner_



## Conclusion

There is nothing else to add in the end. I hope you found this guide useful. Happy coding! 

_If you want to see more content from me, please check also my Twitter account: [@justboriss](https://twitter.com/justboriss)_