IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    React Hooks 快速入门与开发体验(二)

    krimeshu发表于 2023-08-27 05:13:46
    love 0
    <前情提要/>

    回顾

    之前我们学习了 useState 和 useEffect 两个基础 React Hook。

    通过它们,可以实现以前的类组件的大部分功能:属性值传入、自身状态维持、状态更新触发、生命周期回调。

    并且让你可以:

    1. 在业务中常见的简单场景下,使用更简单的代码实现组件;
    2. 通过副作用聚合同一数据在不同生命周期的操作,便于不同组件、项目之间复用。

    二、不良实践:副作用无限触发

    一切看起来都很美好,虽然我们基本还不知道这两个 Hook 内部是怎么样神奇的实现了维持状态和生命周期回调,但通过简单的项目 Demo 就能看到它们确实按照我们预期的效果跑起来了。

    去深挖黑盒的内部构造也是很有意思的,不过现在还为时尚早。

    为什么?不只是因为还有其它 Hook 没有讲到,而且现有的两个 Hook 我们也没有彻底理解。

    只需要对之前的 Demo 稍微做一点小修改,出乎你预料的麻烦事就要发生了……

    1. 无限触发的计数器

    我们将之前 useState 的例子做个小改动,将点击计数 count 改为渲染次数计数 renderCount。

    然后设置一个副作用,不传入依赖数组,使之在每次渲染完成后都执行,执行时将 renderCount 加一来实现计数功能:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function App() {
    const [renderCount, setRenderCount] = useState(0);

    useEffect(() => setRenderCount(renderCount + 1));

    return (
    <div>
    <p>App rendered {renderCount} times</p>
    </div>
    );
    }

    将例子跑起来后,你就会看到——页面上的 renderCount 计数在不停地疯狂飙升,控制台里也出现了来自 React 的警告:

    1
    Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

    为什么会这样?我们看看刚才的副作用:

    1
    useEffect(() => setRenderCount(renderCount + 1));

    组件渲染完毕后,副作用中的 setRenderCount 会导致 renderCount 这个 state 的变化,从而触发组件重渲染。而重渲染又会再次触发 setRenderCount……从而无限循环触发,导致运行的情况与我们想要的效果不太一样。

    2. 添加依赖

    也就是说,我们避免 renderCount 这个 state 触发渲染就能解决问题了。

    添加一个依赖数组,对于组件内除了 renderCount 之外的其它 state 发生改变,再执行副作用就能达到这个效果。

    不过目前除了 renderCount 之外,不存在其它 state,所以我们的依赖数组现在是空的。

    假如增加一个名为 title 的 state:

    1
    2
    3
    4
    const [renderCount, setRenderCount] = useState(0);
    const [title, setTitle] = useState('Hello world!');

    useEffect(() => setRenderCount(renderCount + 1), [title]);

    这里其实还有个隐患,某些情况下直接使用 renderCount 取到的可能不是最新值,最好还是通过回调的方式取到最新值再处理:

    1
    useEffect(() => setRenderCount(renderCount => renderCount + 1), [title]);

    但这样终究有些繁琐,每次增加 state 后找到这里添加依赖只是一项潜规则,参与项目的人越多、修改次数越多,出错的概率就越大。

    3. 使用引用

    之所以 renderCount 能触发渲染,是因为它是个 state,所以如果它不是 state 不触发渲染就能解决问题了?

    1
    2
    3
    4
    const renderCount = 0;
    const [title, setTitle] = useState('Hello world!');

    useEffect(() => renderCount = renderCount + 1);

    这样写的话,renderCount 的改变确实不会触发渲染了,但同样它也没法按照我们的意愿改变了——

    函数式组件本身相当于 render,每次组件重新渲染都会被执行,而 renderCount 作为其中一个普通的局部变量,每次都会被赋值为 0 而非上一次修改的值。导致不管重新渲染几次,页面上的计数始终为0。

    正确的方法是使用另一个 Hook —— useRef:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function App() {
    const renderCount = useRef(0);

    useEffect(() => renderCount.current += 1));

    return (
    <div>
    <p>App rendered {renderCount.current} times</p>
    </div>
    );
    }

    这样,就算增加别的 state,也不需要修改现有代码即可保持逻辑的正常执行。

    此外,我们还可以直接使用 useState 保持一个对象状态,再通过其中的子字段实现计数,原理与 useRef 一样。但是需要注意 setState 时必须使用原对象而非新对象(比如使用解构赋值创建新对象),否则会导致此对象的 state 依赖对比不通过,触发重渲染从而又导致无限更新。


    小结

    问题的根本在于副作用内更新 state 时,state 的变化直接或间接地影响了副作用自身的触发条件,从而导致副作用被无限触发。

    想要尽量避免这样的情况,需要遵循以下原则:

    1. 不轻易在副作用内更新 state;
    2. 为副作用设置好依赖数组;
    3. 触发 state 联动更新时,注意副作用自身依赖条件是否被影响;
    4. 使用官方推荐的 eslint-plugin-react-hooks 插件,辅助开发。


沪ICP备19023445号-2号
友情链接