您的位置:

深入浅出React Hooks - 以react-hooks/exhaustive-deps为中心

一、简介

React Hooks是React 16.8推出的新特性,是一种更加优雅、简洁的处理组件状态和逻辑的方式,干掉了原有的Class组件,让函数组件具有了更加强大的能力。

react-hooks/exhaustive-deps是React Hooks中的一个重要概念,用于在useEffect Hook中控制副作用执行的依赖项,使代码更加健壮、稳定。

二、为什么需要控制副作用执行的依赖项

在使用useEffect Hook时,React会在DOM更新前执行useEffect副作用函数中的代码,以保证页面的正确性和流畅性。由于useEffect是基于函数式编程的思想实现,React并不能像Class组件中的生命周期函数一样自动清理副作用代码的引用,这就需要我们手动控制副作用执行的依赖项。

举个例子:一个简单的计数器函数组件,每点击一次按钮,计数器就会+1,并在useEffect中输出计数器的值:

import React, { useState, useEffect } from 'react';
  
function Counter() {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    console.log('count:', count)
  })
  
  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

但是,运行后我们发现,每点击一次按钮,控制台就会输出当前的count值,即使count并没有改变。这是因为,useEffect的执行是基于引用依赖,而每次点击按钮的时候,函数组件会被重新渲染,useEffect重新执行,此时的count依然是引用上一个count值的地址,所以console输出的值并没有改变。

这时候,就需要我们想办法控制useEffect的依赖项,规避这种无用执行的问题。

三、如何控制副作用执行的依赖项

1. 不指定依赖项

当useEffect的依赖项传空数组[]时,表明useEffect不依赖任何state或prop,仅在组件挂载和卸载时执行副作用代码。

useEffect(() => {
  console.log('component did mount');
  
  // 清理操作
  return () => {
    console.log('component will unmount');
  }
}, []) // 传空数组

这种方式适用于只需要在组件挂载和卸载时执行副作用代码,而不需要根据state或prop的变化执行的场景。

2. 指定部分依赖项

当useEffect的依赖项为指定的state或prop时,每次state或prop发生变化时,useEffect都会执行副作用代码。

useEffect(() => {
  console.log('count:', count)
}, [count]) // 指定count为依赖项

这种方式适用于只需要根据指定的state或prop变化来执行副作用代码的场景。

3. 指定所有依赖项

当useEffect的依赖项为所有state或prop时,每次任意一个state或prop发生变化时,useEffect都会执行副作用代码。在依赖项比较多的情况下,这种方式会造成性能问题,应当慎用。

useEffect(() => {
  console.log('effect:', count, size)
}, [count, size]) // 指定所有依赖项

四、控制依赖项出错的问题

虽然指定依赖项可以避免掉无用执行的问题,但是如果指定的依赖项不全、不正确的话,依然会带来错误和性能问题。

继续看一个例子:一个定时器函数组件,每1秒钟计数器+1,然后在useEffect中输出计数器的值。当props.count发生变化的时候,就会清空计数器。

import React, { useState, useEffect } from 'react';

function Timer(props) {
  const { count } = props;
  const [num, setNum] = useState(0);
  let timer = null;

  useEffect(() => {
    startTimer();
    return () => clearInterval(timer);
  }, []);

  useEffect(() => {
    console.log('num:', num);
    if (count !== 0) {
      setNum(0);
    }
  }, [count]);

  function startTimer() {
    timer = setInterval(() => {
      setNum(num => num + 1);
    }, 1000);
  }

  return (
    
   

num: {num}

); }

运行后,会发现计数器并没有清空,而是继续在计数。这是因为在useEffect中指定了[count],但是每次count发生变化时,startTimer也会被调用,导致num不断增加。

为了避免这个问题,我们需要分析每个state和prop的依赖关系,保证指定的依赖项要么覆盖了所有state和prop,要么根据依赖关系添加必要的依赖项。

useEffect(() => {
  startTimer();
  return () => clearInterval(timer);
}, [startTimer]);

useEffect(() => {
  console.log('num:', num);
  if (count !== 0) {
    setNum(0);
  }
}, [count, setNum]);

五、总结

react-hooks/exhaustive-deps是React Hooks中一个重要的概念,它可以帮助我们避免无用的副作用执行和性能问题,提高页面的健壮性和流畅性。在使用时,需要根据实际场景指定正确的依赖项,防止出现类似上面的错误问题。

完整代码示例:

import React, { useState, useEffect } from 'react';

function Timer(props) {
  const { count } = props;
  const [num, setNum] = useState(0);
  let timer = null;

  useEffect(() => {
    startTimer();
    return () => clearInterval(timer);
  }, [startTimer]);

  useEffect(() => {
    console.log('num:', num);
    if (count !== 0) {
      setNum(0);
    }
  }, [count, setNum]);

  function startTimer() {
    timer = setInterval(() => {
      setNum(num => num + 1);
    }, 1000);
  }

  return (
    
   

num: {num}

); }