Skip to content

goodguydaniel.com

You Can't Call Hooks Inside Conditions? Yes you can

Because rules are made to be broken

JavaScript, React2 min read

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.

"forbidden seagul bending the rules"

source: https://www.flickr.com/photos/sameli/1898511953

Bending the Rules

There's a bunch of reasons why you can't use a React hook conditionally 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?

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

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 (got this from Clean Code by Robert C. Martin).

"code review cartoon"

source: https://me.me/i/the-ouy-vcile-41-wtf-cocle-review-wtf-review-bad-20268515

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.

source: © 2020 by goodguydaniel.com

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.

1function getRandomInt(max) {
2 return Math.floor(Math.random() * Math.floor(max));
3}
4
5function useRandomNumberEverySecond(max = 1000) {
6 const [number, setNumber] = useState(getRandomInt(max));
7
8 useEffect(() => {
9 const interval = setInterval(() => setNumber(getRandomInt(max)), 1000);
10 return () => clearInterval(interval);
11 }, [max]);
12
13 return number;
14}

The Main Component

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

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

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

1function RandomNumberWrapper({ setState }) {
2 const number = useRandomNumberEverySecond();
3
4 useEffect(() => {
5 setState(number);
6 }, [setState, number]);
7
8 return null;
9}

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 reactivity blocks in Svelte. It feels like the conceptual and productivity jump in terms of evolution.

Example Full Code
Expand to see example full code
1import React, { useState, useEffect } from "react";
2
3function getRandomInt(max) {
4 return Math.floor(Math.random() * Math.floor(max));
5}
6
7function useRandomNumberEverySecond(max = 1000) {
8 const [number, setNumber] = useState(getRandomInt(max));
9
10 useEffect(() => {
11 const interval = setInterval(() => setNumber(getRandomInt(max)), 1000);
12 return () => clearInterval(interval);
13 }, [max]);
14
15 return number;
16}
17
18function RandomNumberWrapper({ setState }) {
19 const number = useRandomNumberEverySecond();
20
21 useEffect(() => {
22 setState(number);
23 }, [setState, number]);
24
25 return null;
26}
27
28export default function App() {
29 const [isHookActive, setIsHookActive] = useState(false);
30 const [number, setNumber] = useState(0);
31 return (
32 <div className="App">
33 <h1>Bending the Rules of Hooks</h1>
34 <button onClick={() => setIsHookActive(!isHookActive)}>Click to toggle custom hook usage</button>
35 <h4>{isHookActive ? `hook output is: ${number}` : "hook is not active"}</h4>
36 {isHookActive && <RandomNumberWrapper setState={setNumber} />}
37 </div>
38 );
39}