假设我有这个简单的虚拟组件:
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;博士
我认为当React调用组件的渲染方法来计算下一个渲染周期的差异时,您正在将组件生命周期的
当 React 更新 DOM 时。 参见组件生命周期图:
函数体是“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
、
componentDidUpdate
和componentWillUnmount
生命周期方法的函数组件。无论依赖关系如何,在组件安装时都保证至少运行一次。该效果将运行一次并排队状态更新。 React 将“看到”排队的值与当前状态值相同,并且不会
触发重新渲染。
这里的要点是不要将无意和意外的副作用编码到你的React组件中,因为这会导致和/或导致错误代码。
作为对 Drew Reese 答案的支持,这里有一个小游乐场,可以让您亲眼看到当 setState 直接位于组件主体中时 React 的行为与在 useEffect 或条件中仅在第一次渲染完成后触发时的行为不同。
正如 Drew Reese 所说,如果 setState(),即使是相同的原始值,发生在第一次渲染完成之前,React
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)