Why hooks cannot be invoked conditionally in ReactJS?
Let's take an example:
import {useState} from "react";
const App = () => {
const [first, setFirst] = useState(1);
const [second, setSecond] = useState(2);
// do something with the states
};
How does React know which state to return?
The question is how does React know, that the state first
and the setter setFirst
is
associated with useState(1)
? This is a valid question because useState
only takes in a value, and doesn't get passed with any identifier that associates
it with the corresponding state, at least that's what we can conclude by seeing the declaration.
The answer to this question is simple, React stores states and related setters in an Array
.
We know that arrays are indexed from 0 and the ordering of elements in the
array matters when we are trying to fetch a particular element from within it.
Let's visualize this better by looking at useState
's implementation:
let componentHooks = [];
let hookIndex = 0;
const useState = value => {
let stateAndSetter = componentHooks[hookIndex];
if (stateAndSetter) {
// Not the first time we are seeing this hook.
// Thus return its details and move to next hook.
hookIndex++;
return stateAndSetter;
}
const setState = newValue => {
stateAndSetter[0] = newValue;
reRenderDOM();
};
// first time initialization
stateAndSetter = [value, setState];
componentHooks[hookIndex] = stateAndSetter;
hookIndex++;
return stateAndSetter;
};
The above code is the simplified version of useState
. As you can see, we just add the
state and setter pair to an Array, and fetch the array elements based on the index of
the hook in the order of invocation. So for useState(1)
the hookIndex
is 0
and for useState(2)
the hookIndex
is 1
.
Rules of writing hooks in ReactJS
There are just two rules that we need to keep in mind:
- All hooks ought to be invoked at the top level of the component.
- Hooks should NOT be invoked conditionally.
Why hooks ought to follow certain rules?
As we saw in the first section, if we change the order of the hooks, then the hook index internally will also change. How's that a problem though? Take the following example:
let isFirstRender = true;
const App = () => {
if (isFirstRender) {
const [firstState, setFirstState] = useState(1);
isFirstRender = false;
}
const [secondState, setSecondState] = useState(2);
const [thirdState, setThirdState] = useState(3);
return (
// triggers second render when clicked
<button onClick={() => setSecondState(10)}>Click Here</button>
)
};
- After first render the
componentHooks
array would look like so:
componentHooks = [
[1, setFirstState], // index 0
[2, setSecondState], // index 1
[3, setThirdState], // index 2
]
- But on second render, the data becomes inconsistent, like so:
componentHooks = [
[10, setSecondState], // index 0 is bad data
[2, setSecondState], // index 1
[3, setThirdState], // index 2
]
Our expectation was that the index 1
in componentHooks
would be updated, but rather index 0
got updated
in the second render, triggered by the button click. This happened because of the conditional hook
invocation.
In the second render, the conditional statement didn't get executed and thus hookIndex
didn't change, as it stayed at index 0
which pointed to [1, setFirstState]
.
After the second render, we overwrote the 0th
index losing reference to the firstState
and
corresponding setter, which would create a lot of bugs in our App. And this is why
we ought to follow the rules specified by the React team while invoking hooks!