使用 React Hooks 时要避免的6个错误

简介: 今天来看看在使用React hooks时的一些坑,以及如何正确的使用避免这些坑。

问题概览:

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


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


下面先来看一个例子:

const FetchGame = ({ id }) => {
  if (!id) {
    return '请选择一个游戏';
  }
  const [game, setGame] = useState({ 
    name: '',
    description: '' 
  });
  useEffect(() => {
    const fetchGame = async () => {
      const response = await fetch(`/api/game/${id}`);
      const fetchedGame = await response.json();
      setGame(fetchedGame);
    };
    fetchGame();
  }, [id]);
  return (
    <div>
      <div>Name: {game.name}</div>
      <div>Description: {game.description}</div>
    </div>
  );
}
复制代码


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

当组件执行时,会获取导数据并更新状态。但是这个组件有一个警告:


网络异常,图片无法展示
|


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


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

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


const FetchGame = ({ id }) => {
  const [game, setGame] = useState({ 
    name: '',
    description: '' 
  });
  useEffect(() => {
    const fetchGame = async () => {
      const response = await fetch(`/api/game/${id}`);
      const fetchedGame = await response.json();
      setGame(fetchedGame);
    };
    id && fetchGame();
  }, [id]);
  if (!id) {
    return '请选择一个游戏';
  }
  return (
    <div>
      <div>Name: {game.name}</div>
      <div>Description: {game.description}</div>
    </div>
  );
}
复制代码


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

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


2. 不要使用旧的状态


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

const Increaser = () => {
  const [count, setCount] = useState(0);
  const increase = useCallback(() => {
    setCount(count + 1);
  }, [count]);
  const handleClick = () => {
    increase();
    increase();
    increase();
  };
  return (
    <>
      <button onClick={handleClick}>+</button>
      <div>Counter: {count}</div>
    </>
  );
}
复制代码


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


const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
};
复制代码


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

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


const Increaser = () => {
  const [count, setCount] = useState(0);
  const increase = useCallback(() => {
    setCount(count => count + 1);
  }, [count]);
  const handleClick = () => {
    increase();
    increase();
    increase();
  };
  return (
    <>
      <button onClick={handleClick}>+</button>
      <div>Counter: {count}</div>
    </>
  );
}
复制代码


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

setValue(prevValue => prevValue + someResult)
复制代码


2. 不要创建旧的闭包


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

useEffect(callback, deps)
useCallback(callback, deps)
复制代码


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

const WatchCount = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setInterval(function log() {
      console.log(`Count: ${count}`);
    }, 2000);
  }, []);
  const handleClick = () => setCount(count => count + 1);
  return (
    <>
      <button onClick={handleClick}>+</button>
      <div>Count: {count}</div>
    </>
  );
}
复制代码


最终的输出的结果如下:

网络异常,图片无法展示
|


可以看到,每次打印的count值都是0,和实际的count值并不一样。为什么会这样呢?

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


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

const WatchCount = () => {
  const [count, setCount] = useState(0);
  useEffect(function() {
    const id = setInterval(function log() {
      console.log(`Count: ${count}`);
    }, 2000);
    return () => clearInterval(id);
  }, [count]);
  const handleClick = () => setCount(count => count + 1);
  return (
    <>
      <button onClick={handleClick}>+</button>
      <div>Count: {count}</div>
    </>
  );
}
复制代码


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


4. 不要忘记清理副作用


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

const DelayedIncreaser = () => {
  const [count, setCount] = useState(0);
  const [increase, setShouldIncrease] = useState(false);
  useEffect(() => {
    if (increase) {
      setInterval(() => {
        setCount(count => count + 1)
      }, 1000);
    }
  }, [increase]);
  return (
    <>
      <button onClick={() => setShouldIncrease(true)}>
        +
      </button>
      <div>Count: {count}</div>
    </>
  );
}
const MyApp = () => {
  const [show, setShow] = useState(true);
  return (
    <>
      {show ? <DelayedIncreaser /> : null}
      <button onClick={() => setShow(false)}>卸载</button>
    </>
  );
}
复制代码


这个组件很简单,就是在点击按钮时,状态变量count每秒会增加1。当我们点击+按钮时,它会和我们预期的一样。但是当我们点击“卸载”按钮时,控制台就会出现警告:


网络异常,图片无法展示
|

修复这个问题只需要使用useEffect来清理定时器即可:


useEffect(() => {
    if (increase) {
      const id = setInterval(() => {
        setCount(count => count + 1)
      }, 1000);
      return () => clearInterval(id);
    }
  }, [increase]);
复制代码


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


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


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

const Counter = () => {
  const [counter, setCounter] = useState(0);
  const onClickCounter = () => {
    setCounter(counter => counter + 1);
  };
  const onClickCounterRequest = () => {
    apiCall(counter);
  };
  return (
    <div>
      <button onClick={onClickCounter}>Counter</button>
      <button onClick={onClickCounterRequest}>Counter Request</button>
    </div>
  );
}
复制代码


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


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

const Counter = () => {
  const counter = useRef(0);
  const onClickCounter = () => {
    counter.current++;
  };
  const onClickCounterRequest = () => {
    apiCall(counter.current);
  };
  return (
    <div>
      <button onClick={onClickCounter}>Counter</button>
      <button onClick={onClickCounterRequest}>Counter Request</button>
    </div>
  );
}
复制代码


6. 不要缺少useEffect依赖


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

来看下面的例子:

const Counter = () => {
  const [count, setCount] = useState(0);
  const showCount = (count) => {
    console.log("Count", count);
  };
  useEffect(() => {
    showCount(count);
  }, []);
  return (
      <div>Counter: {count}</div>
  );
}
复制代码


这个组件可能没有什么实际的意义,只是打印了count的值。这时就会有一个警告:

网络异常,图片无法展示
|


这里是说,useEffect缺少一个count依赖,这样是不安全的。我们需要包含一个依赖项或者移除依赖数组。否则useEffect中的代码可能会使用旧的值。


const Counter = () => {
  const [count, setCount] = useState(0);
  const showCount = (count) => {
    console.log("Count", count);
  };
  useEffect(() => {
    showCount(count);
  }, [count]);
  return (
      <div>Counter: {count}</div>
  );
}
复制代码


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

useEffect(() => {
  showCount(996);
}, []);


相关文章
|
2月前
|
前端开发 JavaScript 开发者
深入理解React Hooks:提升前端开发效率的关键
【10月更文挑战第5天】深入理解React Hooks:提升前端开发效率的关键
|
2月前
|
前端开发 JavaScript
React Hooks 全面解析
【10月更文挑战第11天】React Hooks 是 React 16.8 引入的新特性,允许在函数组件中使用状态和其他 React 特性,简化了状态管理和生命周期管理。本文从基础概念入手,详细介绍了 `useState` 和 `useEffect` 的用法,探讨了常见问题和易错点,并提供了代码示例。通过学习本文,你将更好地理解和使用 Hooks,提升开发效率。
70 4
|
2月前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。
|
2月前
|
前端开发 JavaScript API
探索React Hooks:前端开发的革命性工具
【10月更文挑战第5天】探索React Hooks:前端开发的革命性工具
|
20天前
|
前端开发 JavaScript
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
|
1月前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
32 2
|
2月前
|
前端开发 开发者
React 提供的其他重要 Hooks
【10月更文挑战第20天】React 提供了一系列强大的 Hooks,除了 `useRef` 之外,还有许多其他重要的 Hooks,它们共同构成了函数式组件开发的基础。
35 6
|
12天前
|
缓存 前端开发 开发者
深入理解React Hooks,打造高效响应式UI
深入理解React Hooks,打造高效响应式UI
22 0
|
2月前
|
前端开发 JavaScript 开发者
React Hooks
10月更文挑战第13天
36 1
|
2月前
|
前端开发