Popular patterns and anti-patterns with React Hooks
Tags: react, hook, refactoring
Published: 2021-03-11 18:25:58

---
title: Popular patterns and anti-patterns with React Hooks
published: true
description: what I learned while refactoring a big hooks codebase
tags: react, hook, refactoring
//cover_image: https://direct_url_to_image.jpg
---

It's been more than 2 years since [Hooks API](https://reactjs.org/docs/hooks-intro.html) was added to React. Many projects already adopted the new API and there was enough time to see how the new patterns work in production. In this article I am going to walk you through my list of learnings after maintaining a large hooks-based codebase.

## Learning #1. All standard rules apply

Hooks require developers to learn new patterns and follow some [rules of hooks](https://reactjs.org/docs/hooks-rules.html). This sometimes makes people think that new pattern dismisses all previous good practices. However, hooks are just yet another way of creating reusable building blocks. If you are creating a custom hook, you still need to apply basic software development practices:

1. **Single-responsibility principle.** One hook should encapsulate a single piece of functionality. Instead of creating a single super-hook, it is better to split it into multiple smaller and independent ones
2. **Clearly defined API.** Similar to [normal functions/methods](https://softwareengineering.stackexchange.com/questions/145055/are-there-guidelines-on-how-many-parameters-a-function-should-accept), if a hook takes too many arguments, it is a signal that this hook needs refactoring to be better encapsulated. There were recommendations of avoiding React components having too many props, same for React hooks – they also should have minimal number of arguments.
3. **Predictable behavior**. The name of a hook should correspond to its functionality, no additional unexpected behaviours.

Even though these recommendations may look very obvious, it is still important to ensure that you follow them when you are creating your custom hooks.

## Learning #2. Dealing with hook dependencies.

Several React hooks introduce a concept of "dependencies" – list of things which should cause a hook to update. Most often this can be seen in [`useEffect`](https://reactjs.org/docs/hooks-reference.html#useeffect), but also in `useMemo` and `useCallback`. There is a [ESLint rule](https://www.npmjs.com/package/eslint-plugin-react-hooks#installation) to help you managing an array of dependencies in your code, however this rule can only check the structure of the code and not your intent. Managing hook dependencies is the most tricky concept and requires a lot of attention from a developer. To make your code more readable and maintainable, you could reduce the number of hook dependencies.

Your hooks-based code could become easier with this simple trick. For example, let's consider a custom hook `useFocusMove`:

```js
function Demo({ options }) {
  const [ref, handleKeyDown] = useFocusMove({
    isInteractive: (option) => !option.disabled,
  });
  return (
    <ul onKeyDown={handleKeyDown}>
      {options.map((option) => (
        <Option key={option.id} option={option} />
      ))}
    </ul>
  );
}
```

This custom hook takes a dependency on `isInteractive`, which can be used inside the hook implementation:

```js
function useFocusMove({ isInteractive }) {
  const [activeItem, setActiveItem] = useState();

  useEffect(() => {
    if (isInteractive(activeItem)) {
      focusItem(activeItem);
    }
    // update focus whenever active item changes
  }, [activeItem, isInteractive]);

  // ...other implementation details...
}
```

ESLint rule requires `isInteractive` argument to be added to `useEffect` dependencies, because the rule does not know where this custom hook is used and if this argument is ever changing or not. However, as a developer, we know that once defined this function always has the same implementation and adding it to the dependencies array only clutters the code. Standard "factory function" pattern comes to the rescue:

```js
function createFocusMove({ isInteractive }) {
  return function useFocusMove() {
    const [activeItem, setActiveItem] = useState();

    useEffect(() => {
      if (isInteractive(activeItem)) {
        focusItem(activeItem);
      }
    }, [activeItem]); // no ESLint rule violation here :)

    // ...other implementation details...
  };
}

// usage
const useFocusMove = createFocusMove({
  isInteractive: (option) => !option.disabled,
});
function Demo({ options }) {
  const [ref, handleKeyDown] = useFocusMove();
  // ...other code unchanged...
}
```

The trick here is to separate run-time and develop-time parameters. If something is changing during component lifetime, it is a run-time dependency and goes to the dependencies array. If it is once decided for a component and never changes in runtime, it is a good idea to try factory function pattern and make hooks dependencies management easer.

## Learning #3. Refactoring useEffect

`useEffect` hook us a place for imperative DOM interactions inside your React components. Sometimes they could become very complex and adding dependencies array on top of that makes it more difficult tor read and maintain the code. This could be solved via extracting the imperative DOM logic outside the hook code. For example, consider a hook `useTooltipPlacement`:

```js
function useTooltipPosition(placement) {
  const tooltipRef = useRef();
  const triggerRef = useRef();
  useEffect(() => {
    if (placement === "left") {
      const triggerPos = triggerRef.current.getBoundingElementRect();
      const tooltipPos = tooltipPos.current.getBoundingElementRect();
      Object.assign(tooltipRef.current.style, {
        top: triggerPos.top,
        left: triggerPos.left - tooltipPos.width,
      });
    } else {
      // ... and so on of other placements ...
    }
  }, [tooltipRef, triggerRef, placement]);
  return [tooltipRef, triggerRef];
}
```

The code inside `useEffect` is getting very long and hard to follow and track if the hook dependencies are used properly. To make this simper, we could extract the effect content into a separate function:

```js
// here is the pure DOM-related logic
function applyPlacement(tooltipEl, triggerEl, placement) {
  if (placement === "left") {
    const triggerPos = tooltipEl.getBoundingElementRect();
    const tooltipPos = triggerEl.getBoundingElementRect();
    Object.assign(tooltipEl.style, {
      top: triggerPos.top,
      left: triggerPos.left - tooltipPos.width,
    });
  } else {
    // ... and so on of other placements ...
  }
}

// here is the hook binding
function useTooltipPosition(placement) {
  const tooltipRef = useRef();
  const triggerRef = useRef();
  useEffect(() => {
    applyPlacement(tooltipRef.current, triggerRef.current, placement);
  }, [tooltipRef, triggerRef, placement]);
  return [tooltipRef, triggerRef];
}
```

Our hook has become one line long and easy to track the dependencies. As a side bonus we also got a pure DOM implementation of the positioning which could be used and tested outside of React :)

## Learning #4. useMemo, useCallback and premature optimisations

[`useMemo` hook documentation says](https://reactjs.org/docs/hooks-reference.html#usememo):

> You may rely on useMemo as a performance optimisation

For some reason, developers read this part as "you must" instead of "you may" and attempt to memoize everything. This may sound like a good idea on a quick glance, but it appears to be more tricky when it comes to details.

To make benefits from memoization, it is required to use `React.memo` or `PureComponent` wrappers to prevent components from unwanted updates. It also needs very fine tuning and validation that there are no properties changing more often than they should. Any single incorrect propety might break all memoization like a house of cards:

{% twitter 1320888155504103424 %}

This is a good time to recall YAGNI approach and focus memoization efforts only in a few hottest places of your app. In the remaining parts of the code it is not worth adding extra complexity with `useMemo`/`useCallback`. You could benefit from writing more simple and readable code using plain functions and apply memoization patterns later when their benefits become more obvious.

Before going the memoization path, I could also recommend you checking the article ["Before You memo()"](https://overreacted.io/before-you-memo/), where you can find some alternatives to memoization.

## Learning #5. Other React API still exist

> If you have a hammer, everything looks like a nail

The introduction of hooks, made some other React patterns obsolete. For example, `useContext` hook appeared to be more convenient than [Consumer component](https://reactjs.org/docs/context.html#contextconsumer).

However, other React features still exist and should not be forgotten. For example, let's take this hook code:

```js
function useFocusMove() {
  const ref = useRef();
  useEffect(() => {
    function handleKeyDown(event) {
      // actual implementation is extracted outside as shown in learning #3 above
      moveFocus(ref.current, event.keyCode);
    }
    ref.current.addEventListener("keydown", handleKeyDown);
    return () => ref.current.removeEventListener("keydown", handleKeyDown);
  }, []);
  return ref;
}

// usage
function Demo() {
  const ref = useFocusMove();
  return <ul ref={ref} />;
}
```

It may look like a proper use-case for hooks, but why couldn't we delegate the actual event subscription to React instead of doing manually? Here is an alternative version:

```js
function useFocusMove() {
  const ref = useRef();
  function handleKeyDown(event) {
    // actual implementation is extracted outside as shown in learning #3 above
    moveFocus(ref.current, event.keyCode);
  }
  return [ref, handleKeyDown];
}

// usage
function Demo() {
  const [ref, handleKeyDown] = useFocusMove();
  return <ul ref={ref} onKeyDown={handleKeyDown} />;
}
```

The new hook implementation is shorter and has an advantage as hook consumers can now decide where to attach the listener, in case if they have more complex UI.

This was only one example, there could be many other scenarios, but the primary point remains the same – there are many React patterns (high-order components, render props, and others) which still exist and make sense even if hooks are available.

## Conclusion

Basically, all learnings above go to one fundamental aspect: **keep the code short and easy to read**. You will be able to extend and refactor it later in the future. Follow the standard programming patterns and your hook-based codebase will live long and prosper.