今天来看看在使用 React hooks 时的一些坑,以及如何避开这些坑。
问题概览: 
不要改变 hooks 的调用顺序; 
不要使用旧的状态; 
不要创建旧的闭包; 
不要忘记清理副作用; 
不要在不需要重新渲染时使用 useState; 
不要缺少 useEffect 依赖。 
 
1. 不要改变 hooks 的调用顺序  下面先来看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 >    ); } 
useEffect中会使用这个 id 作为参数去请求游戏的信息。并将获取的数据保存在状态变量 game 中。
当组件执行时,会获取导数据并更新状态。但是这个组件有一个警告:
这里是告诉我们,钩子的执行是不正确的。因为当 id 为空时,组件会提示,并直接退出。如果 id 存在,就会调用useState和useEffect这两个 hook。这样有条件的执行钩子时就可能会导致意外并且难以调试的错误。实际上,React hooks 内部的工作方式要求组件在渲染时,总是以相同的顺序来调用 hook。
这也就是 React 官方文档中所说的:不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。 
解决这个问题最直接的办法就是按照官方文档所说的,确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们: 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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. 不要使用旧的状态 先来看一个计数器的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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。问题就在于,当我们点击按钮时,相当于下面的操作:
1 2 3 4 5 const  handleClick  = (  setCount (count + 1 );   setCount (count + 1 );   setCount (count + 1 ); }; 
setCount(count + 1)时是没有问题的,它会将count更新为 1。接下来第 2、3 次调用setCount时,count还是使用了旧的状态(count为 0),所以也会计算出count为 1。发生这种情况的原因就是状态变量会在下一次渲染才更新。
解决这个问题的办法就是,使用函数的方式来更新状态: 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 >      </>    ); } 
如果要使用当前状态来计算下一个状态,就要使用函数的式方式来更新状态: 
1 setValue (prevValue  =>
3. 不要创建旧的闭包 众所周知,React Hooks 是依赖闭包实现的。当使用接收一个回调作为参数的钩子时,比如:
1 2 useEffect (callback, deps)useCallback (callback, deps)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const  WatchCount  = (  const  [count, setCount] = useState (0 );   useEffect (() =>  {     setInterval (function  log (       console .log (`Count: ${count} ` );     }, 2000 );   }, []);   const  handleClick  = (setCount (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 发生变化时,就重置定时器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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  =>1 );   return  (     <>        <button  onClick ={handleClick} > +</button >        <div > Count: {count}</div >      </>    ); } 
4. 不要忘记清理副作用 有很多副作用,比如fetch请求、setTimeout等都是异步的,如果不需要这些副作用或者组件在卸载时,不要忘记清理这些副作用。下面来看一个计数器的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const  DelayedIncreaser  = (  const  [count, setCount] = useState (0 );   const  [increase, setShouldIncrease] = useState (false );   useEffect (() =>  {     if  (increase) {       setInterval (() =>  {         setCount (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 >      </>    ); } 
修复这个问题只需要使用 useEffect 来清理定时器即可:
1 2 3 4 5 6 7 8 useEffect (() =>  {    if  (increase) {       const  id = setInterval (() =>  {         setCount (count  =>1 )       }, 1000 );       return  () =>  clearInterval (id);     }   }, [increase]); 
5. 不要在不需要重新渲染时使用 useState 在 React hooks 中,我们可以使用useState hook 来进行状态的管理。虽然使用起来比较简单,但是如果使用不恰当,就可能会出现意想不到的问题。来看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const  Counter  = (  const  [counter, setCounter] = useState (0 );   const  onClickCounter  = (     setCounter (counter  =>1 );   };   const  onClickCounterRequest  = (     apiCall (counter);   };   return  (     <div >        <button  onClick ={onClickCounter} > Counter</button >        <button  onClick ={onClickCounterRequest} > Counter Request</button >      </div >    ); } 
counter并没有在渲染阶段使用。所以,每次点击第一个按钮时,都会有不需要的重新渲染。
因此,当遇到这种需要在组件中使用一个变量在渲染中保持其状态,并且不会触发重新渲染时,那么useRef会是一个更好的选择,下面来对上面的例子使用 useRef 进行改编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 设置依赖数组来避免这些不必要的渲染。
来看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const  Counter  = (  const  [count, setCount] = useState (0 );   const  showCount  = (count ) => {     console .log ("Count" , count);   };   useEffect (() =>  {     showCount (count);   }, []);   return  (       <div > Counter: {count}</div >    ); } 
这里是说,useEffect 缺少一个 count 依赖,这样是不安全的。我们需要包含一个依赖项或者移除依赖数组。否则 useEffect 中的代码可能会使用旧的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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,那么依赖项为空也会是安全的:
1 2 3 useEffect (() =>  {  showCount (996 ); }, []);