3.精通React-使用ReactHooks时要避免的6个错误-《前端知识进阶》

admin 2025-11-01 15:31:33 编程 来源:ZONE.CI 全球网 0 阅读模式
  • 1. 不要改变 hooks 的调用顺序
  • 3. 不要使用旧的状态
  • 2. 不要创建旧的闭包
  • 4. 不要忘记清理副作用
  • 5. 不要在不需要重新渲染时使用useState
  • 6. 不要缺少useEffect依赖

    今天来看看在使用React hooks时的一些坑,以及如何避开这些坑。

    问题概览:

    1. 不要改变 hooks 的调用顺序;
    2. 不要使用旧的状态;
    3. 不要创建旧的闭包;
    4. 不要忘记清理副作用;
    5. 不要在不需要重新渲染时使用useState;
    6. 不要缺少useEffect依赖。

      1. 不要改变 hooks 的调用顺序

      下面先来看一个例子:

      1. const FetchGame = ({ id }) => {
      2. if (!id) {
      3. return '请选择一个游戏';
      4. }
      5. const [game, setGame] = useState({
      6. name: '',
      7. description: ''
      8. });
      9. useEffect(() => {
      10. const fetchGame = async () => {
      11. const response = await fetch(`/api/game/${id}`);
      12. const fetchedGame = await response.json();
      13. setGame(fetchedGame);
      14. };
      15. fetchGame();
      16. }, [id]);
      17. return (
      18. <div>
      19. <div>Name: {game.name}</div>
      20. <div>Description: {game.description}</div>
      21. </div>
      22. );
      23. }

      这个组件接收一个参数id,在useEffect中会使用这个id作为参数去请求游戏的信息。并将获取的数据保存在状态变量game中。

    当组件执行时,会获取导数据并更新状态。但是这个组件有一个警告:image.png这里是告诉我们,钩子的执行是不正确的。因为当id为空时,组件会提示,并直接退出。如果id存在,就会调用useState和useEffect这两个hook。这样有条件的执行钩子时就可能会导致意外并且难以调试的错误。实际上,React hooks内部的工作方式要求组件在渲染时,总是以相同的顺序来调用hook。

    这也就是React官方文档中所说的:不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。

    解决这个问题最直接的办法就是按照官方文档所说的,确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们:

    1. const FetchGame = ({ id }) => {
    2. const [game, setGame] = useState({
    3. name: '',
    4. description: ''
    5. });
    6. useEffect(() => {
    7. const fetchGame = async () => {
    8. const response = await fetch(`/api/game/${id}`);
    9. const fetchedGame = await response.json();
    10. setGame(fetchedGame);
    11. };
    12. id && fetchGame();
    13. }, [id]);
    14. if (!id) {
    15. return '请选择一个游戏';
    16. }
    17. return (
    18. <div>
    19. <div>Name: {game.name}</div>
    20. <div>Description: {game.description}</div>
    21. </div>
    22. );
    23. }

    这样,无论传入的id是否为空,useState和useEffect总会以相同的顺序来调用,这样就不会出错啦~

    React官方文档中的Hook规则:《Hook 规则》,可以使用插件eslint-plugin-react-hooks来帮助我们检查这些规则。

    3. 不要使用旧的状态

    先来看一个计数器的例子:

    1. const Increaser = () => {
    2. const [count, setCount] = useState(0);
    3. const increase = useCallback(() => {
    4. setCount(count + 1);
    5. }, [count]);
    6. const handleClick = () => {
    7. increase();
    8. increase();
    9. increase();
    10. };
    11. return (
    12. <>
    13. <button onClick={handleClick}>+</button>
    14. <div>Counter: {count}</div>
    15. </>
    16. );
    17. }

    这里的handleClick方法会在点击按钮后执行三次增加状态变量count的操作。那么点击一次是否会增加3呢?事实并非如此。点击按钮之后,count只会增加1。问题就在于,当我们点击按钮时,相当于下面的操作:

    1. const handleClick = () => {
    2. setCount(count + 1);
    3. setCount(count + 1);
    4. setCount(count + 1);
    5. };

    当第一次调用setCount(count + 1)时是没有问题的,它会将count更新为1。接下来第2、3次调用setCount时,count还是使用了旧的状态(count为0),所以也会计算出count为1。发生这种情况的原因就是状态变量会在下一次渲染才更新。

    解决这个问题的办法就是,使用函数的方式来更新状态:

    1. const Increaser = () => {
    2. const [count, setCount] = useState(0);
    3. const increase = useCallback(() => {
    4. setCount(count => count + 1);
    5. }, [count]);
    6. const handleClick = () => {
    7. increase();
    8. increase();
    9. increase();
    10. };
    11. return (
    12. <>
    13. <button onClick={handleClick}>+</button>
    14. <div>Counter: {count}</div>
    15. </>
    16. );
    17. }

    这样改完之后,React就能拿到最新的值,当点击按钮时,就会每次增加3。所以需要记住:如果要使用当前状态来计算下一个状态,就要使用函数的式方式来更新状态:

    1. setValue(prevValue => prevValue + someResult)

    2. 不要创建旧的闭包

    众所周知,React Hooks是依赖闭包实现的。当使用接收一个回调作为参数的钩子时,比如:

    1. useEffect(callback, deps)
    2. useCallback(callback, deps)

    此时,我们就可能会创建一个旧的闭包,该闭包会捕获过时的状态或者prop变量。这么说可能有些抽象,下面来看一个例子,这个例子中,useEffect每2秒会打印一次count的值:

    1. const WatchCount = () => {
    2. const [count, setCount] = useState(0);
    3. useEffect(() => {
    4. setInterval(function log() {
    5. console.log(`Count: ${count}`);
    6. }, 2000);
    7. }, []);
    8. const handleClick = () => setCount(count => count + 1);
    9. return (
    10. <>
    11. <button onClick={handleClick}>+</button>
    12. <div>Count: {count}</div>
    13. </>
    14. );
    15. }

    最终的输出的结果如下:image.png可以看到,每次打印的count值都是0,和实际的count值并不一样。为什么会这样呢?

    在第一次渲染时应该没啥问题,闭包log会将count打印出0。从第二次开始,每次当点击按钮时,count会增加1,但是setInterval仍然调用的是从初次渲染中捕获的count为0的旧的log闭包。log方法就是一个旧的闭包,因为它捕获的是一个过时的状态变量count。

    这里的解决方案就是,当count发生变化时,就重置定时器:

    1. const WatchCount = () => {
    2. const [count, setCount] = useState(0);
    3. useEffect(function() {
    4. const id = setInterval(function log() {
    5. console.log(`Count: ${count}`);
    6. }, 2000);
    7. return () => clearInterval(id);
    8. }, [count]);
    9. const handleClick = () => setCount(count => count + 1);
    10. return (
    11. <>
    12. <button onClick={handleClick}>+</button>
    13. <div>Count: {count}</div>
    14. </>
    15. );
    16. }

    这样,当状态变量count发生变化时,就会更新闭包。为了防止闭包捕获到旧值,就要确保在提供给hook的回调中使用的prop或者state都被指定为依赖性。

    4. 不要忘记清理副作用

    有很多副作用,比如fetch请求、setTimeout等都是异步的,如果不需要这些副作用或者组件在卸载时,不要忘记清理这些副作用。下面来看一个计数器的例子:

    1. const DelayedIncreaser = () => {
    2. const [count, setCount] = useState(0);
    3. const [increase, setShouldIncrease] = useState(false);
    4. useEffect(() => {
    5. if (increase) {
    6. setInterval(() => {
    7. setCount(count => count + 1)
    8. }, 1000);
    9. }
    10. }, [increase]);
    11. return (
    12. <>
    13. <button onClick={() => setShouldIncrease(true)}>
    14. +
    15. </button>
    16. <div>Count: {count}</div>
    17. </>
    18. );
    19. }
    20. const MyApp = () => {
    21. const [show, setShow] = useState(true);
    22. return (
    23. <>
    24. {show ? <DelayedIncreaser /> : null}
    25. <button onClick={() => setShow(false)}>卸载</button>
    26. </>
    27. );
    28. }

    这个组件很简单,就是在点击按钮时,状态变量count每秒会增加1。当我们点击+按钮时,它会和我们预期的一样。但是当我们点击“卸载”按钮时,控制台就会出现警告:image.png修复这个问题只需要使用useEffect来清理定时器即可:

    1. useEffect(() => {
    2. if (increase) {
    3. const id = setInterval(() => {
    4. setCount(count => count + 1)
    5. }, 1000);
    6. return () => clearInterval(id);
    7. }
    8. }, [increase]);

    当我们编写一些副作用时,我们需要知道这个副作用是否需要清除。

    5. 不要在不需要重新渲染时使用useState

    在React hooks 中,我们可以使用useState hook来进行状态的管理。虽然使用起来比较简单,但是如果使用不恰当,就可能会出现意想不到的问题。来看下面的例子:

    1. const Counter = () => {
    2. const [counter, setCounter] = useState(0);
    3. const onClickCounter = () => {
    4. setCounter(counter => counter + 1);
    5. };
    6. const onClickCounterRequest = () => {
    7. apiCall(counter);
    8. };
    9. return (
    10. <div>
    11. <button onClick={onClickCounter}>Counter</button>
    12. <button onClick={onClickCounterRequest}>Counter Request</button>
    13. </div>
    14. );
    15. }

    在上面的组件中,有两个按钮,第一个按钮会触发计数器加一,第二个按钮会根据当前的计数器状态发送一个请求。可以看到,状态变量counter并没有在渲染阶段使用。所以,每次点击第一个按钮时,都会有不需要的重新渲染。

    因此,当遇到这种需要在组件中使用一个变量在渲染中保持其状态,并且不会触发重新渲染时,那么useRef会是一个更好的选择,下面来对上面的例子使用useRef进行改编:

    1. const Counter = () => {
    2. const counter = useRef(0);
    3. const onClickCounter = () => {
    4. counter.current++;
    5. };
    6. const onClickCounterRequest = () => {
    7. apiCall(counter.current);
    8. };
    9. return (
    10. <div>
    11. <button onClick={onClickCounter}>Counter</button>
    12. <button onClick={onClickCounterRequest}>Counter Request</button>
    13. </div>
    14. );
    15. }

    6. 不要缺少useEffect依赖

    useEffect是React Hooks中最常用的Hook之一。默认情况下,它总是在每次重新渲染时运行。但这样就可能会导致不必要的渲染。我们可以通过给useEffect设置依赖数组来避免这些不必要的渲染。

    来看下面的例子:

    1. const Counter = () => {
    2. const [count, setCount] = useState(0);
    3. const showCount = (count) => {
    4. console.log("Count", count);
    5. };
    6. useEffect(() => {
    7. showCount(count);
    8. }, []);
    9. return (
    10. <div>Counter: {count}</div>
    11. );
    12. }

    这个组件可能没有什么实际的意义,只是打印了count的值。这时就会有一个警告:image.png这里是说,useEffect缺少一个count依赖,这样是不安全的。我们需要包含一个依赖项或者移除依赖数组。否则useEffect中的代码可能会使用旧的值。

    1. const Counter = () => {
    2. const [count, setCount] = useState(0);
    3. const showCount = (count) => {
    4. console.log("Count", count);
    5. };
    6. useEffect(() => {
    7. showCount(count);
    8. }, [count]);
    9. return (
    10. <div>Counter: {count}</div>
    11. );
    12. }

    如果useEffect中没有用到状态变量count,那么依赖项为空也会是安全的:

    1. useEffect(() => {
    2. showCount(996);
    3. }, []);

    今天的分享就到这里,如果觉得有用就来个三连吧~

    以太坊cppgolang区别 编程

    以太坊cppgolang区别

    以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
    progolang 编程

    progolang

    Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
    golangn个发送者 编程

    golangn个发送者

    Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
    golang技能图谱 编程

    golang技能图谱

    从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
    评论:0   参与:  4