tl;dr

  • Calling hooks conditionally breaks the rules, yes.
  • Wrap the hook with a component and render that component optionally to toggle the usage of the hook.
  • The rules of hooks are somewhat reflections of the constraints of this React API.
  • Example final full code here.


seagull on a transit signal


# Bending the Rules

There’s a bunch of reasons why you can’t use a React hook conditionally. In this short article, I want to share with you a subtle pattern that I accidentally came across when in need of conditionally calling on a custom React hook under a particular condition.

Before I start, I would like you to take a moment to reflect on the following questions?

Why did you end up needing this? And, is it your fault, or is it the library’s fault?

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
seriously, ask yourself, why?
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

When I first tried to answer the question myself, I settled that it would be my fault If I had written the code since the very beginning.

If you pick React hooks API, you should structure the code accordingly not to end up falling into such scenarios where your better option might be breaking the rules. On the other end, if you have hundreds of components combined in a large shared codebase, you might hit a dead end where the most optimal option for the time being it’s not beautiful. Still, it gets the job done and buys you and your team time to get things out faster, ideally you would come back and remove the hack later. Still, most of the time in the real world, we know that’s not going to happen, so you better documenting it properly to alleviate the unavoidable spike on WTFs/minute.

code review cartoon

As always, I think there’s nothing better than an example to explain a concept, let’s look into one.

In this example, we have a custom hook useRandomNumberEverySecond that produces a random number every one second. In a real scenario, you could potentially have a super sophisticated hook that performs asynchronous stuff like data fetching and puts everything together. For the sake of simplicity, we’re just returning a number.

We want only to call on useRandomNumberEverySecond and use its result, if and only if, a flag isHookActive has value true. But, how can we achieve this if we declare hooks at the top level of a component, and we can’t wrap them inside if statements?

My suggested approach is to wrap our hook useRandomNumberEverySecond in a second component, RandomNumberWrapper, that mediates the relationship between our target component and the hook useRandomNumberEverySecond. Let’s see how that looks.


GIF final result example code


As proof of concept, the goal is to have the button in the interface toggle the usage of our custom React hook.

# Custom React Hook

Our custom hook useRandomNumberEverySecond generates a random number every second.

function getRandomInt(max) {
  return Math.floor(Math.random() * Math.floor(max));
}

function useRandomNumberEverySecond(max = 1000) {
  const [number, setNumber] = useState(getRandomInt(max));

  useEffect(() => {
    const interval = setInterval(() => setNumber(getRandomInt(max)), 1000);
    return () => clearInterval(interval);
  }, [max]);

  return number;
}

# The Main Component

Our main component looks like this, where number is provided by our custom hook (when active).

export default function App() {
  const [isHookActive, setIsHookActive] = useState(false);
  const [number, setNumber] = useState(0);
  return (
    <div className="App">
      <h1>Bending the Rules of Hooks</h1>
      <button onClick={() => setIsHookActive(!isHookActive)}>
        Click to toggle custom hook usage
      </button>
      <h4>
        {isHookActive ? `hook output is: ${number}` : "hook is not active"}
      </h4>
      {isHookActive && <RandomNumberWrapper setState={setNumber} />}
    </div>
  );
}

Notice that the component RandomNumberWrapper only renders when isHookActive is true. Now let’s take a look at RandomNumberWrapper.

Now let’s see how our main component consumes the custom hook useRandomNumberEverySecond.

# The Wrapper Component

function RandomNumberWrapper({ setState }) {
  const number = useRandomNumberEverySecond();

  useEffect(() => {
    setState(number);
  }, [setState, number]);

  return null;
}

And that’s it! RandomNumberWrapper blindly proxies whatever data comes from useRandomNumberEverySecond via the callback setState, which then updates the number state property in our main component. You can go ahead and apply this pattern to any hook in your codebase, wrapping up, you need to:

  1. Create a new component to wrap the usage of the hook you intent to use conditionally.
  2. Pass into this new component, a setter that allows you to forward the data back to the parent component.
  3. Conditionally mount the new component in your target component and pass in the setter as a prop to the new component, that’s how you’re going to receive the state updates coming from your custom React hook.


# Closing Notes

I hope you found this pattern helpful! If you’re curious to read more on the subject, I advise checking this excellent blog post entitled “How to Break the Rules of React Hooks”.

After working some time with Observables and RxJS and retrospectively thinking about React hooks and workarounds such as the one explored in this article, I feel that React hooks are a cumbersome and desperate attempt to ship an API that brings real reactivity to the React community. Additionally, after playing around with Svelte for some time, I’m comparing the feeling that I had when transitioning from building UIs with Vanilla JavaScript and jQuery to use a framework that offers UI data bindings, to the feeling I had when moved from React hooks to reactivity blocks in Svelte. It feels like the conceptual and productivity jump in terms of evolution.

# Example Full Code

click to see example full code
import React, { useState, useEffect } from "react";

function getRandomInt(max) {
  return Math.floor(Math.random() * Math.floor(max));
}

function useRandomNumberEverySecond(max = 1000) {
  const [number, setNumber] = useState(getRandomInt(max));

  useEffect(() => {
    const interval = setInterval(() => setNumber(getRandomInt(max)), 1000);
    return () => clearInterval(interval);
  }, [max]);

  return number;
}

function RandomNumberWrapper({ setState }) {
  const number = useRandomNumberEverySecond();

  useEffect(() => {
    setState(number);
  }, [setState, number]);

  return null;
}

export default function App() {
  const [isHookActive, setIsHookActive] = useState(false);
  const [number, setNumber] = useState(0);
  return (
    <div className="App">
      <h1>Bending the Rules of Hooks</h1>
      <button onClick={() => setIsHookActive(!isHookActive)}>
        Click to toggle custom hook usage
      </button>
      <h4>
        {isHookActive ? `hook output is: ${number}` : "hook is not active"}
      </h4>
      {isHookActive && <RandomNumberWrapper setState={setNumber} />}
    </div>
  );
}