简体   繁体   English

React component sometimes renders twice with unchanged state

[英]React component sometimes renders twice with unchanged state

I am using Redux to subscribe to a store and update a component.

This is a simplified example without Redux. It uses a mock-up store to subscribe and dispatch to.

Please, follow the steps below the snippet to reproduce the problem.

Edit: Please skip to the second demo snippet under Update for a more concise and closer to real-life scenario. The question is not about Redux. It's about React's setState function identity causing re-render in certain circumstances even though the state has not changed.

Edit 2: Added even more concise demo under "Update 2".

const {useState, useEffect} = React;

let counter = 0;

const createStore = () => {
	const listeners = [];
	
	const subscribe = (fn) => {
		listeners.push(fn);
		return () => {
			listeners.splice(listeners.indexOf(fn), 1);
		};
	}
	
	const dispatch = () => {
		listeners.forEach(fn => fn());
	};
	
	return {dispatch, subscribe};
};

const store = createStore();

function Test() {
	const [yes, setYes] = useState('yes');
	
	useEffect(() => {
		return store.subscribe(() => {
			setYes('yes');
		});
	}, []);
	
	console.log(`Rendered ${++counter}`);
	
	return (
		<div>
			<h1>{yes}</h1>
			<button onClick={() => {
				setYes(yes === 'yes' ? 'no' : 'yes');
			}}>Toggle</button>
			<button onClick={() => {
				store.dispatch();
			}}>Set to Yes</button>
		</div>
	);
}

ReactDOM.render(<Test />, document.getElementById('root'));
<div id="root"></div>
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>

What is happening

  1. ✅ Click "Set to Yes". Since the value of yes is already "yes", state is unchanged, hence the component is not re-rendered.
  2. ✅ Click "Toggle". yes is set to "no". State has changed, so the component is re-rendered.
  3. ✅ Click "Set to Yes". yes is set to "yes". State has changed again, so the component is re-rendered.
  4. ⛔ Click "Set to Yes" again. State has not changed, but the component is still re-rendered.
  5. ✅ Subsequent clicks on "Set to Yes" do not cause re-rendering as expected.

What is expected to happen

On step 4 the component should not be re-rendered since state is unchanged.

Update

As the React docs state, useEffect is

suitable for the many common side effects, like setting up subscriptions and event handlers...

One such use case could be listening to a browser event such as online and offline.

In this example we call the function inside useEffect once when the component first renders, by passing it an empty array []. The function sets up event listeners for online state changes.

Suppose, in the app's interface we also have a button to manually toggle online state.

Please, follow the steps below the snippet to reproduce the problem.

const {useState, useEffect} = React;

let counter = 0;

function Test() {
	const [online, setOnline] = useState(true);
	
	useEffect(() => {
		const onOnline = () => {
			setOnline(true);
		};
		const onOffline = () => {
			setOnline(false);
		};
		window.addEventListener('online', onOnline);
		window.addEventListener('offline', onOffline);
		
		return () => {
			window.removeEventListener('online', onOnline);
			window.removeEventListener('offline', onOffline);
		}
	}, []);
	
	console.log(`Rendered ${++counter}`);
	
	return (
		<div>
			<h1>{online ? 'Online' : 'Offline'}</h1>
			<button onClick={() => {
				setOnline(!online);
			}}>Toggle</button>
		</div>
	);
}

ReactDOM.render(<Test />, document.getElementById('root'));
<div id="root"></div>
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>

What is happening

  1. ✅ The component is first rendered on the screen, and the message is logged in the console.
  2. ✅ Click "Toggle". online is set to false. State has changed, so the component is re-rendered.
  3. ⛔ Open Dev tools and in the Network panel switch to "offline". online was already false, thus state has not changed, but the component is still re-rendered.

What is expected to happen

On step 3 the component should not be re-rendered since state is unchanged.

Update 2

const {useState, useEffect} = React;

let counterRenderComplete = 0;
let counterRenderStart = 0;


function Test() {
  const [yes, setYes] = useState('yes');

  console.log(`Component function called ${++counterRenderComplete}`);
  
  useEffect(() => console.log(`Render completed ${++counterRenderStart}`));

  return (
    <div>
      <h1>{yes ? 'yes' : 'no'}</h1>
      <button onClick={() => {
        setYes(!yes);
      }}>Toggle</button>
      <button onClick={() => {
        setYes('yes');
      }}>Set to Yes</button>
    </div>
  );
}

ReactDOM.render(<Test />, document.getElementById('root'));

 
<div id="root"></div>
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>

What is happening

  1. ✅ Click "Set to Yes". Since the value of yes is already true, state is unchanged, hence the component is not re-rendered.
  2. ✅ Click "Toggle". yes is set to false. State has changed, so the component is re-rendered.
  3. ✅ Click "Set to Yes". yes is set to true. State has changed again, so the component is re-rendered.
  4. ⛔ Click "Set to Yes" again. State has not changed, despite that the component starts the rendering process by calling the function. Nevertheless, React bails out of rendering somewhere in the middle of the process, and effects are not called.
  5. ✅ Subsequent clicks on "Set to Yes" do not cause re-rendering (function calls) as expected.

Question

Why is the component still re-rendered? Am I doing something wrong, or is this a React bug?

The answer is that React uses a set of heuristics to determine whether it can avoid calling the rendering function again.答案是 React 使用一组启发式方法来确定它是否可以避免再次调用渲染函数。 These heuristics may change between versions and aren't guaranteed to always bail out when the state is the same.这些启发式方法可能会在版本之间发生变化,并且不能保证在状态相同时总是退出。 The only guarantee React provides is that it won't re-render child components if the state was the same<\/strong> . React 提供的唯一保证是,如果状态相同<\/strong>,它不会重新渲染子组件<\/strong>。

Your rendering functions should be pure.你的渲染函数应该是纯的。 Therefore, it shouldn't matter how many times they run.因此,它们运行多少次并不重要。 If you're calculating something expensive in your render function and are concerned about calling it more than necessary, you can wrap that calculation in useMemo<\/code> .如果您在渲染函数中计算一些昂贵的东西并且担心调用它超出必要,您可以将该计算包装在useMemo<\/code> 。

Generally speaking, there's no use in "counting renders" in React.一般来说,React 中的“计数渲染”是没有用的。 When exactly React calls your function is up to React itself, and the exact timing will keep changing between versions. React 何时调用您的函数取决于 React 本身,并且确切的时间将在版本之间不断变化。 It's not part of the contract.这不是合同的一部分。

"

It seems like this is an expected behavior.这似乎是一种预期的行为。

From the React docs :来自React 文档

Bailing out of a state update摆脱状态更新

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects.如果您将 State Hook 更新为与当前状态相同的值,React 将退出而不渲染子级或触发效果。 (React uses the Object.is comparison algorithm .) (React 使用Object.is比较算法。)

Note that React may still need to render that specific component again before bailing out.请注意,React 可能仍需要在退出之前再次渲染该特定组件。 That shouldn't be a concern because React won't unnecessarily go “deeper” into the tree.这不应该是一个问题,因为 React 不会不必要地“深入”到树中。 If you're doing expensive calculations while rendering, you can optimize them with useMemo .如果您在渲染时进行昂贵的计算,您可以使用useMemo对其进行优化。

So, React does re-render the component on step 4 of the first demo and step 3 of the second one.因此,React确实在第一个演示的第 4 步和第二个演示的第 3 步重新渲染了组件。 Hence it executes all the code inside the function and it calls React.createElement() for each child of the component.因此,它执行函数内的所有代码,并为组件的每个子组件调用React.createElement()

However it does not render any descendant of the component and does not fire the effects.但是,它不会渲染组件的任何后代,也不会触发效果。

This is only true for function components.这仅适用于功能组件。 For a pure class component, the render method never gets called if the state has not changed.对于纯类组件,如果状态没有改变, render方法永远不会被调用。

There's nothing we can do to avoid the re-run.我们无法避免重新运行。 Memoizing the function with memo() will not help either, since it only checks for props changes , not the state.使用memo()记忆函数也无济于事,因为它只检查 props 的变化,而不是状态。 So we just have to take this situation into account.所以我们只需要考虑这种情况。

This doesn't answer the question why and when React runs the function but bails out, and when it doesn't run the function at all.这并不能回答 React 为什么以及何时运行该函数但退出,以及何时它根本不运行该函数的问题。 If you know the reason, please add your answer.如果您知道原因,请添加您的答案。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM