在渲染期间直接在组件主体中将状态更新为相同的值会导致无限循环

问题描述 投票:0回答:3

假设我有这个简单的虚拟组件:

const Component = () => {

  const [state, setState] = useState(1);

  setState(1);

  return <div>Component</div>
}

在此代码中,我直接在组件主体中将状态更新为与之前相同的值。但是,即使值保持不变,这也会导致太多的重新渲染。 据我所知,在

React.useState

中,如果状态值更新为与之前相同的值 -

React 不会重新渲染组件
。那么为什么会发生在这里呢?

但是

,如果我尝试使用 useEffect 做类似的事情,而不是直接在组件主体中:

const Component = () => {

  const [state, setState] = useState(1);

  useEffect(() => {
    setState(1);
  }, [state])

  return <div>Component</div>
}

不会

导致任何无限循环,并且完全符合以下规则:如果状态保持不变,React 不会重新渲染组件。 所以我的问题是:

为什么当我直接在组件主体中执行它时会导致无限循环,而在

useEffect中则不会?

有人对此有一些“幕后”解释吗?

TL;博士
javascript reactjs react-hooks infinite-loop
3个回答
10
投票
第一个示例是无意的副作用,将无条件触发重新渲染,而第二个示例是有意的副作用,并允许 React 组件生命周期按预期运行。

回答

我认为当React调用组件的渲染方法来计算下一个渲染周期的差异时,您正在将组件生命周期的

“渲染阶段”

与我们在“提交”期间通常所说的“渲染周期”混为一谈阶段”

当 React 更新 DOM 时。 参见组件生命周期图:

请注意,在 React 函数组件中,整个

函数体是“render”方法,函数的返回值是我们想要刷新或提交到 DOM 的值。现在我们都应该知道,React 组件的“render”方法被认为是一个没有副作用的纯函数。换句话说,渲染的结果是状态和道具的纯函数。 在第一个示例中,排队状态更新是一个无意的副作用

,它是在正常组件生命周期之外调用的(即挂载、更新、卸载)。 const Component = () => { const [state, setState] = useState(1); setState(1); // <-- unintentional side-effect return <div>Component</div>; }; 它在“渲染阶段”触发重新渲染。 React 组件永远没有机会完成渲染周期,因此没有什么可以“比较”或摆脱的,因此渲染循环发生了。

排队状态更新的另一个示例是
有意的副作用

useEffect 钩子在渲染周期结束时运行,之后下一个 UI 更改刷新或提交到 DOM。

const Component = () => {
  const [state, setState] = useState(1);

  useEffect(() => {
    setState(1); // <-- intentional side-effect
  }, [state]);

  return <div>Component</div>;
}
useEffect

钩子是
大致
相当于类组件的

componentDidMount

componentDidUpdatecomponentWillUnmount
生命周期方法的函数组件。无论依赖关系如何,在组件安装时都保证至少运行一次。该效果将运行一次并排队状态更新。 React 将“看到”排队的值与当前状态值相同,并且
不会
触发重新渲染。
这里的要点是
不要

将无意和意外的副作用编码到你的React组件中,因为这会导致和/或导致错误代码。

作为对 Drew Reese 答案的支持,这里有一个小游乐场,可以让您亲眼看到当 setState 直接位于组件主体中时 React 的行为与在 useEffect 或条件中仅在第一次渲染完成后触发时的行为不同。

正如 Drew Reese 所说,如果 setState(),即使是相同的原始值,发生在第一次渲染完成之前,React

0
投票
就会陷入无限循环。不知何故,它在第一次渲染期间没有差异任何内容,因此将 setState() 设置为与初始化时相同的值并不重要。

const { useState, useEffect, useRef } = React; const Component = () => { const rerenderCountRef = useRef(0); const [state, setState] = useState('same'); rerenderCountRef.current++; console.log(rerenderCountRef.current) document.getElementById('render-count').innerHTML = rerenderCountRef.current // Doesn't cause any rerenders useEffect(() => { setState('same'); }); // 👇 Will cause an infinite rerender loop. (Uncomment to see the error, in dev console) // /* <= uncomment */ setState('same'); // Use the debugger to see that nothing ever actually has a chance to get committed to the DOM (the React area remains blank) // when setState is called in the body of the component // /* <= uncomment */ debugger // Will not cause an infinite rerender if (rerenderCountRef.current > 1) { // setState('same'); /* <= uncommment */ } return ( < div > < h2 > React < /h2> I'm not erroring! 😁 < br / > I've rendered {rerenderCountRef.current} times. < br / > { /** Doesn't cause a rerender **/ } < button onClick = { () => { setState('same'); } } > Set state to same primitive value < /button> < / div > ); }; ReactDOM.render( < Component / > , document.getElementById('root'))

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script> <div style="display: flex; font-family: sans-serif; gap: 20px;"> <div style="border: solid 1px blue;" id="root"></div> <div style="border: 1px dashed green"> <h2>Outside React</h2> Render count: <span id="render-count">0</span> </div>
    
调用

setState(1)

时,您还会触发重新渲染,因为这本质上就是钩子的工作方式。这是对底层机制的很好解释:

-1
投票

React.useState如何触发重新渲染?


    

© www.soinside.com 2019 - 2024. All rights reserved.