头图

原文:https://juejin.cn/post/697243...
作者:Tonychen

Infinite Chain Of Update

实际使用中有时候会碰到 Infinite Chain Of Update 这个报错,其实就是你的一段代码引发了「死循环更新」。下面我们来看几个例子👇

依赖数组问题

比如说在使用 useEffect 时没有传入依赖数组👇

// count 会无限 + 1
function App() {
    const [count, setCount] = useState(0)

    useEffect(() => {
        setCount(count + 1)
    })
}

为什么说 count 会无限更新?这里的逻辑是这样的👇

  • 组件更新
  • 执行 useEffect
  • 更新 count 并触发组件更新
  • 执行 useEffect
  • ……

2021-06-09-1053-4.png

解决方法很简单,只要给 useEffect 传一个空数组作为第三个参数,下次更新时 useEffect 便不会执行。

// 正常渲染
function App() {
    const [count, setCount] = useState(0)

    useEffect(() => {
        setCount(count + 1)
    }, [])
}

监听了被更新的值

这个算是新手 hooks 玩家经常会遇到、老手也有些头疼的问题。

  • 案例1

useEeffect 中更新的 state 间接影响了被监听的变量,举个例子🌰

function App() {
    const [obj, setObj] = useState({a: 0})
    const {a} = obj
    useEffect(() => {
        setObj({
            ...obj,
            a: 1
        })
    }, [a, obj])
}

上面这段代码在实际运行的时候就会导致死循环,为什么呢?因为在 setObj 的时候改变的是 obj 这个值,而 useEffect 监听了这个值,从而 导致了死循环……

2021-06-09-1053-3.svg

怎么解决呢?由于是 obj 变化引起的 infinite loop ,那么其实只要不监听 obj 就没有这回事了

🤪,这里可以利用一下 setState 的「回调函数」用法👇

function App() {
    const [obj, setObj] = useState({a: 0})
    const {a} = obj;
    useEffect(() => {
        setObj((state) => ({
            ...state,
            a: 1
        }))
    }, [a])
}
  • 案例2

有时候你需要根据不同的「状态」来决定组件显示什么,那么通常就需要利用一个 state 来控制若干种「状态」的显示,从状态 1 到状态 2 的转化是异步的。一个简单的做法就是用 useEffect 来监听它。

2021-06-09-1053-2.svg

如果说这个状态有一部分依赖外部传入,另外一部分根据这个外部传入的状态的变化来进行对应的处理。举个例子👇

export function App({outsider, wait}) {
    const [state, setState] = useState('INIT')
    useEffect(() => {
        // 根据 ousider 处理 state 的值
        if (outsider === true) {
                setState('PENDING')
        } else {
            if (state === 'PENDING') {
                setTimeout(() => {
                    setState('RESOLVED')
                }, wait)
            }
        }
    }, [outsider, state])
    return (
        // 根据 state 来渲染不同的组件/样式
    )
}

实际运行起来的话又是 infinite loop 了,可能你第一时间我想的一样,就是采用「案例1」的解法。但是注意,这里是有异步处理的,所以这里只能说是利用 useRef 来做一下简单的处理。

export function App({outsider, wait}) {
    const [state, setState] = useState('INIT')
    const stateRef = useRef(state)
    useEffect(() => {
        // 根据 ousider 处理 state 的值
        if (outsider === true) {
            setState('PENDING')
            stateRef.current = 'PENDING'
        } else {
            if (stateRef.current === 'PENDING') {
                setTimeout(() => {
                    setState('RESOLVED')
                    stateRef.current = 'RESOLVED'
                }, wait)
            }
        }
    }, [outsider])
    return (
        // 根据 state 来渲染不同的组件/样式
    )
}

这样一来在 useEffect 中就不需要依赖 state ,而且能够根据 state 当前的值做出一些操作😄

小结

在写 hooks 的时候,需要经常注意代码中是否有依赖 statesetState 的地方,通常直觉上的写法是会带来 infinite loop 的。

2021-06-09-1053.svg

获取不到最新的值

新手 hooks 经常会碰到这类问题,下面是一个简单的例子👇

import { useCallback, useEffect, useState } from "react";

let timeout = null;
export default function App() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(count + 1);
    timeout = setTimeout(() => {
      console.log("timeout", count);
      setCount(count + 1);
    }, 1000);
  }, [count]);

  useEffect(() => {
    return () => {
      clearTimeout(timeout);
    };
  }, []);

  return (
    <div className="App">
      <p>{count}</p>
      <button onClick={handleClick}>click me</button>
    </div>
  );
}

运行之后你会发现,每次 console.log 打印出来的都是上一次 count + 1 前的结果,而这其实就和 useState 实现有关系了,这里仅截取源码中的一小部分实现👇

image.png

可以看出,从 useState 中解构出来的是原数据的值而非引用,所以在上面的例子中,在 setTimeout 里拿不到最新的 count 值。

参考资料

Setting State based on Previous State in useEffect - Its a trap!


tonychen
1.2k 声望272 粉丝