问题
遇到这样一个问题,我在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;
然后开始使用的时候没有报错,直到我点击了一下按钮报错:
错误提示本次渲染的hook比上次渲染的更多
原因
看到这个报错,我很快就想到了react官方文档的hook的规则:
而我的代码中,使用了条件渲染,这个违反了hook规则:
解决
在实际的项目中,情况要比我举例的demo复杂的多,而且要很快的解决这个问题上线,且影响要小,那么最快的解决方案是,将该条件判断通过传参的形式,传到RenderDemo1内部,进行条件判断:
内部判断:
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的使用原理,真正从源码的角度理解;