首发于 React

React Hooks 使用中遇到的坑

React Hooks 注意事项

  • 注意事项一:useState 初始化值,只有第一次有效
import React, { useState } from 'react'
// 子组件
function Child({ userInfo }) {
    // render: 初始化 state
    // re-render: 只恢复初始化的 state 值,不会再重新设置新的值
    //            只能用 setName 修改
    const [ name, setName ] = useState(userInfo.name)
    return <div>
        <p>Child, props name: {userInfo.name}</p>
        <p>Child, state name: {name}</p>
function App() {
    const [name, setName] = useState('sheben')
    const userInfo = { name }
    return <div>
            Parent &nbsp;
            <button onClick={() => setName('社本')}>setName</button>
        <Child userInfo={userInfo}/>
export default App

点击 button 后,我们可以看到父组件传递给子组件的 props 已经改变了,但是子组件中不会重新 setName。

利用这点我们可以把复杂的 state 初始化放到 useState 中:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
实际上 useState 声明的状态应该是函数组件内部的状态,如果和 props 有关,应该在 useEffect 中去改变。
如果一个数据和 props 有关,我们可以在 useMemo、useEffect 中处理这个数据,而不应该使用 useState,因为这样会破坏 React 的单向数据流。
使用 useEffect 和 useMemo 处理 props 的区别。useMemo 依赖的数据不改变,页面是不会重新渲染的。useEffect 中只要父组件重新渲染,子组件就会重新渲染。
  • 注意事项二:capture value
import React, { useState } from 'react'
function App() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setTimeout(() => {
      console.log('You clicked on: ' + count);
    }, 3000);
  return (
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      <button onClick={handleClick}>print count</button>
export default App 

我们来看这个例子,先点击print countbutton,再点击count + 1button,控制台打印的还是之前的 count:0

我们再来看值捕获的另一个例子:

import React, { useState } from "react";
function App() {
  const [count, setCount] = useState(0);
  const incr = () => {
    setTimeout(() => {
      setCount(count + 1);
    }, 3000);
  return (
      <h1>{count}</h1>
      <button onClick={incr}>+1</button>
export default App;

初始页面如下:

当我们在 3s 内快速点击 +1 button 5下,页面会发生什么变化?页面更新如下:

function Index(){
    const [ num ,setNumber ] = React.useState(0)
    const handerClick=()=>{
        for(let i=0; i<5;i++ ){
           setTimeout(() => {
                setNumber(num+1)
                console.log(num)
           }, 1000)
    return <button onClick={ handerClick } >{ num }</button>

这就是 React 的值捕获。

注意区分 capture value 和闭包陷阱
  • 注意事项三:useEffect 依赖 [] 时的闭包问题(闭包陷阱)

React 的闭包陷阱指的是当 useEffect 的依赖为 [] 的时候,当 React 函数式组件重新执行,useEffect hooks 并不会重新执行,这时候它内部的变量依旧是上一次的变量,这就构成了“闭包陷阱” :

import {useState, useEffect} from 'react'
export default function App() {
  const [count, setCount] = useState(0)
  // 模拟 DidMount
  useEffect(() => {
      // 定时任务
      // useEffect 中的 count 永远是第一次的 count
      const timer = setInterval(() => {
          console.log('setInterval...', count)
      }, 1000)
      // 清除定时任务
      return () => clearTimeout(timer)
  }, []) // 依赖为 [],re-render 不会重新执行 effect 函数
  return (<>
      <div>count: {count}</div>
      <div><button onClick={() => setCount(count+1)}>+1</button></div>

我们点击 +1 按钮数次后,页面渲染已经成为 4,而控制台打印的依旧是 0。这就是闭包陷阱。

只有 useEffect 中才会存在闭包陷阱,因为我们重新执行函数式组件的时候,useEffect 不会被重新执行,这时它依赖的就是上一次的变量。

  • 注意事项三:useEffect 依赖了「引用类型」可能会出现死循环(React 是通过 Object.is 来判断依赖是否改变的)
  • 注意事项四:使用 useCallback 缓存函数的时候,如果函数内部使用了外部的变量,则需要把这个变量加进依赖中,否则函数不会检测到该变量的改变。

如何解决 React Hooks 中的闭包陷阱?

主要有三种方式:

  • 使用回调函数赋值
  • 使用 useRef
  • 使用 useReducer

我们来一个一个分析

使用回调函数

当我们使用 setState 时,新的 state 如果是通过计算旧的 state 得出,那么我们可以将回调函数当作参数传递给 setState。该回调函数将接收先前的 state,并返回一个更新后的值:

import React, { useState, useEffect, useRef } from 'react'
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => {
      setCount((c) => c + 1)
    }, 1000);
    return () => clearInterval(timer);
  }, []);
  return <div>{count}</div>;
export default App

使用 useRef

同一个 ref 在 React 组件的整个生命周期中只存在一个引用,因此通过 current 永远是可以访问到引用中最新的函数值,不会存在闭包陈旧值的问题。

import React, { useState, useEffect, useRef } from 'react'
function App() {
    const [count, setCount] = useState(0)
    const countRef = useRef(0)
    // 模拟 DidMount
    useEffect(() => {
        // 定时任务
        const timer = setInterval(() => {
            console.log('setInterval...', countRef.current)
            setCount(countRef.current++)
        }, 1000)
        // 清除定时任务
        return () => clearTimeout(timer)
    }, []) // 依赖为 [],re-render 不会重新执行 effect 函数
    return <div>count: {count}</div>
export default App

使用 useReducer

利用 useReducer 获取的 dispatch 方法在组件的生命周期中保持唯一引用,并且总是能操作到最新的值:

import {useEffect, useReducer} from 'react'
const initialCount = 0;
function reducer(count, action) {
  switch (action.type) {
    case 'increment':
      return count + 1
    case 'decrement':
      return count - 1
    default:
      throw new Error();
function App() {
  const [count, dispatch] = useReducer(reducer, initialCount);
  useEffect(()=>{
    setInterval(() => {
      dispatch({type:'increment'})
    }, 1000);
  },[])