The React website introduces the limitations of Hook:

Never call hooks in loops, conditions, or nested functions. Be sure to always call them at the top of your React functions and before any returns. By following this rule, you can ensure that hooks are called in the same order every time you render. This allows React to keep the hook state correct between multiple useState and useEffect calls. (If you’re curious about this, we explain it in more depth below.)

This limitation can sometimes affect our development experience. For example, when a function component returns an if statement early and a Hook call occurs later, the React esLint rule will also warn you.

function App(){
  if (xxx) {
    return null;
  }

  // ❌ React Hook "useState" is called recess.
  // React Hooks must be called in the exact same order in every component render.
  useState();
  
  return 'Hello'
}
Copy the code

This is actually quite common, and there are times when we don’t want the component to continue rendering after a certain condition is met. But because of this limitation, we can only push all Hook calls to the top of the function, adding extra overhead.

As the source code of React is too complex, this article will use the source code of Preact, which is similar in principle but much simpler, as the starting point to debug and explain.

Reasons for restrictions

The React team didn’t create this restriction out of thin air; it was forced by the React Hook implementation design.

Preact’s Hook implementation, for example, uses arrays and subscripts to find hooks (React uses linked lists, but the principle is similar).

// The currently running component
let currentComponent

// Global index of current hook
let currentIndex

// The first call currentIndex is 0
useState('first') 

// The second call currentIndex is 1
useState('second')
Copy the code

[state, useState] = [state, useState] = [state, useState] = [state, useState]

So if the condition is called, for example the first useState will only be called with a probability of 0.5:

// The currently running component
let currentComponent

// Global index of current hook
let currentIndex

// The first call currentIndex is 0
if (Math.random() > 0.5) {
  useState('first')}// The second call currentIndex is 1
useState('second')
Copy the code

When Preact renders the component for the first time, assuming math.random () returns a random value of 0.6, the first Hook is executed with the _hooks state saved on the component:

_hooks: [
  { value: 'first'.update: function },
  { value: 'second'.update: function},]Copy the code

Graphically, the search process looks like this:

If math.random () returns a random value of 0.3 on the second rendering and only the second useState is executed, its global currentIndex will be 0, Trying to get the state of first in _hooks[0] would cause render confusion.

Yes, the value should have been second, but instead was referred to first, rendering completely wrong!

Take this example:

export default function App() {
  if (Math.random() > 0.5) {
    useState(10000)}const [value, setValue] = useState(0)

  return (
    <div>
      <button onClick={()= > setValue(value + 1)}>+</button>
      {value}
    </div>)}Copy the code

The result is this:

Crack limit

Is there a way to beat the limits?

If we want to crack the global index increment bug, we can consider storing Hook state in a different way.

If you don’t use subscript storage, isn’t it possible to use a globally unique key to hold the Hook, so that you can bypass the subscript chaos?

For example, the useState API was modified to look like this:

export default function App() {
  if (Math.random() > 0.5) {
    useState(10000.'key1');
  }
  const [value, setValue] = useState(0."key2");

  return (
    <div>
      <button onClick={()= > setValue(value + 1)}>+</button>
      {value}
    </div>
  );
}
Copy the code

Thus, there are no exceptions to the previous hooks by _hooks[‘key’].

In other words, the original storage mode is:

_hooks: [
  { value: 'first'.update: function },
  { value: 'second'.update: function},]Copy the code

After transforming:

_hooks: [
  key1: { value: 'first'.update: function },
  key2: { value: 'second'.update: function},]Copy the code

Note that arrays support the key value nature of objects themselves; you do not need to modify the _hooks structure.

Transform the source code

To modify Preact’s source code, locate its Hook package under hooks/ SRC /index.js, find the useState method:

export function useState(initialState) {
  currentHook = 1;
  return useReducer(invokeOrReturn, initialState, undefined);
}
Copy the code

It calls useReducer, so add a new key parameter to pass through:

+ export function useState(initialState, key) {
  currentHook = 1;
+ return useReducer(invokeOrReturn, initialState, undefined, key);
}
Copy the code

UseReducer obtains Hook state from global index:

// Global index
let currentIndex

export function useReducer(reducer, initialState, init) {
  const hookState = getHookState(currentIndex++, 2);
  hookState._reducer = reducer;

  return hookState._value;
}
Copy the code

If there is a key, pass in the key value first:

// global index let currentIndex+ export function useReducer(reducer, initialState, init, key) {
+ const hookState = getHookState(key || currentIndex++, 2);
   hookState._reducer = reducer;

   return hookState._value;
}
Copy the code

Finally, modify the getHookState method:

function getHookState(index, type) { const hooks = currentComponent.__hooks || (currentComponent.__hooks = { _list: [], _pendingEffects: [], }); // The key can be string or symbol+  if (typeof index !== 'number') {
+ if (! hooks._list[index]) {
+ hooks._list[index] = {};
+}
+ } else {if (index >= hooks._list.length) { hooks._list.push({}); }} // return hooks._list[index]; }Copy the code

When a key is passed in, the new state is initialized by subscript instead of pushing the new state into the array. The original method of writing the state is hooks._list[index].

At this point, the transformation is complete.

Let’s try something new:

export default function App() {
  if (Math.random() > 0.5) {
    useState(10000.'key1');
  }
  const [value, setValue] = useState(0.'key2');

  return (
    <div>
      <button onClick={()= > setValue(value + 1)}>+</button>
      {value}
    </div>
  );
}
Copy the code

Automatic compilation

In fact, the React team considered adding a key to each call. In Dan Abramov’s why sequential calls are important to React Hooks. The proposal has been explained in detail in.

Multiple bugs cause this proposal to be rejected, especially when it comes to custom hooks, such as extracting a useFormInput:

const valueKey = Symbol(a);function useFormInput() {
  const [value, setValue] = useState(valueKey);
  return {
    value,
    onChange(e){ setValue(e.target.value); }}; }Copy the code

Then call it multiple times in the component:

function Form() {
  / / use the Symbol
  const name = useFormInput(); 
  // Use the same Symbol again
  const surname = useFormInput(); 
  // ...
  return (
    <>
      <input {. name} / >
      <input {. surname} / >{/ *... * /}</>)}Copy the code

At this point, the method of finding Hook state by key will conflict.

However, my idea is whether it is possible to use the compilation capability of Babel plug-in to automatically inject a key into every Hook call at compile time. The pseudo-code is as follows:

traverse(node) {
  if (isReactHookInvoking(node)) {
    addFunctionParameter(node, getUniqKey(node))
  }
}
Copy the code

Generate code like this:

function Form() {
+ const name = useFormInput('key_1');
+ const surname = useFormInput('key_2');/ /... return ( <> <input {... name} /> <input {... surname} /> {/* ... * /}} < / a >)+ function useFormInput(key) {
+ const [value, setValue] = useState(key);return { value, onChange(e) { setValue(e.target.value); }}; }Copy the code

The key generation strategy can be either a random value or a Symbol injection. It doesn’t matter. Maybe there are some places that I haven’t considered well. Students with any ideas on this are welcome to discuss with me on wechat sshsunlight. Of course, it’s ok to simply make friends.

conclusion

This article is only an exploratory one:

  • Introduce the general principle and limitations of Hook implementation
  • Explore ways to modify the source code mechanism to circumvent restrictions

Actually, the original intention is to help everyone better understand Hook.

I don’t want React to remove these restrictions. I think it’s a design trade-off.

If a Hook can be called from any subfunction, from any conditional expression, the code becomes more difficult to understand and maintain.

If you really want to be more flexible with similar Hook capabilities, Vue3’s underlying principle of reactive collection dependencies can get around these limitations perfectly, but being more flexible also inevitably adds more maintenance risks.

Thank you for your

My name is SSH, and I am currently working in the Web Infra team of Bytedance. Currently, the team is still short in Beijing, Shanghai, Guangzhou, Shenzhen and Hangzhou (especially Beijing).

For this reason, I have formed a recruitment community with a good atmosphere, where we can discuss our ideas and questions about the interview. You are also welcome to join and send your resume to me at any time.