修复React中 "超过最大更新深度 "的错误的方法

331 阅读4分钟

React是一个优秀的框架,但它可能有一些棘手的 "问题"。其中之一就是当你不小心造成了一个无限的渲染循环时,往往会导致 "超过最大更新深度 "的隐晦错误。

无限次渲染循环

你很可能已经让自己陷入了一个无限的渲染循环。这里我将讨论最常见的原因以及如何解决它们。

原因1:调用一个函数,而不是向其传递一个引用

你可能会因为不小心调用了一个状态设置函数而不是向其传递一个引用而陷入无限的渲染循环。考虑一下下面的代码。

function App() {
  const [terms, setTerms] = useState(false);

  const acceptTerms = () => {
    setTerms(true);
  };

  return (
    <>
      <label>
        <input type="checkbox" onChange={acceptTerms()} /> Accept the Terms
      </label>
    </>
  );
}

我们立即调用acceptTerms ,而不是将一个函数的引用传递给变化处理程序。这意味着,当组件渲染时,我们立即调用acceptTerms ,导致组件重新渲染,然后立即调用acceptTerms ,如此反复。为了摆脱这种束缚,我们需要传递一个对acceptTerms 的引用,而不是立即调用它。

function App() {
  const [terms, setTerms] = useState(false);

  const acceptTerms = () => {
    setTerms(true);
  };

  return (
    <>
      <label>
        <input type="checkbox" onChange={acceptTerms} /> Accept the Terms
      </label>
    </>
  );
}

原因2:一个更新其自身依赖数组中的变量的效果

React的一个很好的特点是它是反应式的。我们可以使用useEffect钩子,在变量被更新的基础上采取一个行动。然而,这可能会适得其反:如果useEffect 钩子更新了一个触发效果重新运行的变量,那么它就会不断地更新和重新运行,造成无限循环。让我们考虑下面的例子。

function App() {
  const [views, setViews] = useState(0);

  useEffect(() => {
    setViews(views + 1);
  }, [views]);

  return <>Some content</>;
}

在这种情况下,useEffect 钩子将在views 更新时运行。但是,当钩子运行时,views ,然后被更新,反过来,导致效果再次运行。这就成了一个无限的循环。

防止这种情况发生的一个方法是在你的状态设置器中使用一个回调函数

setViews((v) => v + 1);

这将允许你安全地从依赖数组中删除views 变量。

function App() {
  const [views, setViews] = useState(0);

  useEffect(() => {
    setViews((v) => v + 1);
  }, []);

  return <>Some content</>;
}

现在,这个效果只在初始渲染时运行,当views 更新时就不会再运行。

如果你不能清理依赖数组

可能有一些原因让你无法清理依赖关系数组。这不是最好的办法,但你可以在其中添加一个额外的变量,以限制效果是否被重新运行。例如,你可以创建一个isInitialRender 有状态变量。

function App() {
  const [views, setViews] = useState(0);
  const [isInitialRender, setIsInitialRender] = useState(true);

  useEffect(() => {
    if (isInitialRender) {
      setIsInitialRender(false);
      setViews(v + 1);
    }
  }, [views, isInitialRender]);

  return <>Some content</>;
}

现在只有当isInitialRender 为真时,setViews 函数才会被调用,这意味着当效果重新运行时,views 变量将不会被递增,我们也不会重新触发这个效果。

原因3:你的效果取决于在组件内部声明的一个函数

useEffect 钩子使用参考平等来确定其依赖数组中是否有任何变量发生变化。这意味着,即使一个对象看起来与另一个对象相同,效果也可能被重新触发,因为这些对象在内存中实际上是不同的。

这方面的一个很好的例子是当一个效果依赖于一个在组件本身中声明的函数时。比如说。

function App() {
  const [views, setViews] = useState(0);

  const incrementViews = () => {
    setViews((v) => v + 1);
  };

  useEffect(() => {
    incrementViews();
  }, [incrementViews]);

  return <>Some content</>;
}

尽管incrementViews看起来它在每次渲染时都是同一个函数,但实际上每次渲染时都会在内存中创建一个新函数。

React对这个问题有几个解决方案。

在效果中移动函数

一个快速的解决方案是把函数移到useEffect 钩子里面。

function App() {
  const [views, setViews] = useState(0);

  useEffect(() => {
    const incrementViews = () => {
      setViews((v) => v + 1);
    };
    incrementViews();
  }, []);

  return <>Some content</>;
}

我们可以看到,现在incrementViews 是在useEffect 钩子的范围内,而新的函数只会在效果运行时(在组件安装时)被创建。由于这个函数在效果里面,我们甚至不需要把它包括在依赖关系数组中

useCallback钩子

useCallback钩子的工作方式很像useEffect ,第一个参数是一个函数,第二个参数是一个依赖关系数组。不同的是,useCallback 钩子不会创建一个新的函数*,除非*其依赖数组中的一个变量发生变化。

我们可以在这里看到useCallback 钩子的动作。

function App() {
  const [views, setViews] = useState(0);

  const incrementViews = useCallback(() => {
    setViews((v) => v + 1);
  }, []);

  useEffect(() => {
    incrementViews();
  }, [incrementViews]);

  return <>Some content</>;
}

现在,incrementViews总是引用内存中的同一个对象。

总结

React非常棒,因为它对状态变化的反应非常好。有时候,如果你不小心的话,它的反应就太好了希望这些提示能解决你目前遇到的任何问题。