hook 在条件渲染中为啥会报错

238 阅读3分钟

问题

遇到这样一个问题,我在common的index文件里声明了多个小组件,在外部引用使用:

common的index.tsx:

import { useEffect } from "react";

export const RenderDemo1 = (nn: string) => {
  useEffect(() => {
    console.log("useDemo");
  }, []);

  return <div>我是RenderDemo1{nn}</div>;
};

引用例子:

import { useState } from "react";
import { RenderDemo1 } from "./common";

const Demo1 = () => {

  const [ showRenderDemo1, setShowRenderDemo1 ] = useState(false);

  return (
    <div>
      <button onClick={() => setShowRenderDemo1(!showRenderDemo1)}>Toggle RenderDemo1</button>
      {
        showRenderDemo1 && RenderDemo1('N')
      }
    </div>
  );
};

export default Demo1;

然后开始使用的时候没有报错,直到我点击了一下按钮报错:

image.png

错误提示本次渲染的hook比上次渲染的更多

原因

看到这个报错,我很快就想到了react官方文档的hook的规则:

image.png

而我的代码中,使用了条件渲染,这个违反了hook规则:

image.png

解决

在实际的项目中,情况要比我举例的demo复杂的多,而且要很快的解决这个问题上线,且影响要小,那么最快的解决方案是,将该条件判断通过传参的形式,传到RenderDemo1内部,进行条件判断

image.png

内部判断:

import { useEffect } from "react";

export const RenderDemo1 = (nn: string, showRenderDemo1: boolean) => {
  useEffect(() => {
    console.log("useDemo");
  }, []);

  return <div>{showRenderDemo1 && `我是RenderDemo1${nn}`}</div>;
};

这样,这个问题就解决了。

探究

本次的问题虽然解决了,但是我更加好奇,为啥hook必须要放在顶层呢?

我首先查阅了官方文档,但是官方文档并没有一个具体的解释,只是告诉了这个规则,并且知道如果触发了规则,会有什么样的报错

新的问题

在看到这里的时候,我观察到另外一个问题:react的hook没有标识,那么当使用多个hook的时候,react如何识别对应的hook

以useState举例:

import { useState } from "react";
import { RenderDemo1 } from "./common";

const Demo1 = () => {

  const [ showRenderDemo1, setShowRenderDemo1 ] = useState(false);
  const [ showRenderDemo2, setShowRenderDemo2 ] = useState(false);

  return (
    <div>
      <button onClick={() => setShowRenderDemo1(!showRenderDemo1)}>Toggle RenderDemo1</button>
      {
        RenderDemo1('N', showRenderDemo1)
      }
       <button onClick={() => setShowRenderDemo2(!showRenderDemo2)}>Toggle RenderDemo1</button>
      {
        RenderDemo1('M', showRenderDemo2)
      }
    </div>
  );
};

export default Demo1;

上述代码中有两个useState,但是怎么知道哪个state对应哪个useState,比如说react怎么知道showRenderDemo1是第一个useState的值

新的答案

顺序

如果每次渲染hook的调用顺序都是一样的,那么react就知道当前的state对应的是哪个hook

手动实现useState

如果还是不清楚的话,我们这里手动实现一个简易版的useState:

  • 1、声明state、setState、stateIndex全局变量,用数组的形式去存储;
  • 2、声明一个函数,用于创建setState,并且用闭包将内部的值缓存下来;
  • 3、创建一个自定义的render函数,用于模拟刷新视图;
  • 4、创建自定义的useState,并将其给暴露出去;
const state: any[] = []; // 保存所有的state
const setState: any[] = []; // 保存所有的setState
let stateIndex = 0; // 当前state的索引

const createSetState = (index: number) => { // 通过索引获取对应的setState
  return (newValue: any) => { // 闭包
    state[index] = newValue;
    render(); // 重新渲染视图
  };
}

let root: ReactDOMClient.Root;

const render = () => { // 模拟刷新视图
  // 每次调用render都要重置stateIndex,否则对应的索引无限递增将无法正确匹配state和setState之间的关系
  stateIndex = 0;
  // 模拟ReactDOM.createRoot方法
  const rootElement = document.getElementById('root');
  // 只创建一次根节点
  if (!root) {
    root = ReactDOMClient.createRoot(rootElement);
  }
  root.render(<Demo1/>); // 自己渲染的视图组件
};

function useMyState(initialValue: any) { // 自定义useState
  const currentIndex = stateIndex;
  state[currentIndex] = state[currentIndex] || initialValue; // 初始化state 为 initialValue 
  setState[currentIndex] = setState[currentIndex] || createSetState(currentIndex);  // 初始化setState 

  stateIndex++;
  return [state[currentIndex], setState[currentIndex]];
}

这里我们通过全局变量来保存所有的state的状态,用数组来维护多个hook的顺序和更新,用render函数来模拟视图更新;

真实的源码要比这个复杂很多,使用的是环形链表来存储hook,这也是为什么,hook不能放在条件渲染的原因

总结

这是我在遇到hook在条件渲染使用后报错的一次思考,通过对hook的简单手动实现,我理解了报错背后的原因,后续我会继续深究hook的使用原理,真正从源码的角度理解;