React组件有时会在状态不变的情况下渲染两次

时间:2020-03-27 10:02:09

标签: javascript reactjs use-effect use-state

我正在使用Redux订阅商店并更新组件。

这是没有Redux的简化示例。它使用实体店进行订阅和调度。

请按照以下代码段中的步骤重现该问题。

编辑:请跳到更新下的第二个演示代码段,以获取更简洁,更接近实际的场景。问题是关于Redux的不是。这与React的setState函数身份有关,即使状态没有改变,它也会在某些情况下导致重新渲染。

编辑2 :在“ 更新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>

发生了什么

  1. ✅单击“设置为是”。由于yes的值已经为“是”,因此状态未更改,因此该组件不会重新呈现。
  2. ✅单击“切换”。 yes设置为“ no”。状态已更改,因此重新渲染了该组件。
  3. ✅单击“设置为是”。 yes设置为“是”。状态再次更改,因此该组件被重新呈现。
  4. ⛔再次单击“设置为是”。状态未更改,但组件仍被重新渲染
  5. ✅随后单击“设置为是”不会导致重新渲染。

预计会发生什么

在第4步中,由于状态未更改,因此不应重新渲染组件。

更新

作为React docs状态,useEffect

适合许多常见的副作用,例如设置 订阅和事件处理程序...

一个这样的用例就是监听诸如onlineoffline之类的浏览器事件。

在此示例中,当组件首次呈现时,我们通过向其传递一个空数组useEffect来调用[]内部的函数。该功能为在线状态更改设置事件侦听器。

假设,在应用程序的界面中,我们还有一个按钮可以手动切换在线状态。

请按照以下代码段中的步骤重现该问题。

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>

发生了什么

  1. ✅首先将组件呈现在屏幕上,并将消息记录在控制台中。
  2. ✅单击“切换”。 online设置为false。状态已更改,因此重新渲染了该组件。
  3. ⛔打开Dev工具,然后在“网络”面板中切换到“离线”。 online已经false,因此状态未更改,但是组件仍被重新渲染。

预计会发生什么

在第3步中,由于状态不变,因此不应重新渲染组件。

更新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>

发生了什么

  1. ✅单击“设置为是”。由于yes的值已经是true,因此状态不变,因此该组件不会重新呈现。
  2. ✅单击“切换”。 yes设置为false。状态已更改,因此重新渲染了该组件。
  3. ✅单击“设置为是”。 yes设置为true。状态再次更改,因此该组件被重新呈现。
  4. ⛔再次单击“设置为是”。尽管组件通过调用该函数启动了渲染过程,但状态未更改。尽管如此,React仍无法在过程的中间进行渲染,因此不会调用效果。
  5. ✅随后单击“设置为是”不会引起预期的重新渲染(函数调用)。

问题

为什么仍重新渲染组件?我是在做错什么,还是React的错误?

2 个答案:

答案 0 :(得分:5)

答案是React使用一组启发式方法来确定它是否可以避免再次调用呈现函数。这些启发式方法可能会在版本之间发生变化,并且不能保证在状态相同时总是能够纾困。 React提供的唯一保证是如果状态相同,它不会重新渲染子组件

您的渲染函数应该是纯函数。因此,它们运行多少次无关紧要。如果您要在渲染函数中计算一些昂贵的东西,并且担心不必要地调用它,则可以将该计算结果包装在useMemo中。

通常来说,在React中“计数渲染”是没有用的。当确切地调用React时,您的功能取决于React本身,确切的时间将在版本之间不断变化。这不是合同的一部分。

答案 1 :(得分:2)

这似乎是预期的行为。

来自React docs

退出状态更新

如果将状态挂钩更新为相同 值作为当前状态,React将纾困而不渲染 儿童或射击效果。 (反应使用Object.is comparison algorithm。)

请注意,React可能仍需要再次渲染该特定组件 救助之前。不用担心,因为React不会 不必要地“深入”到树上。如果您做的很昂贵 渲染时进行计算,您可以使用useMemo对其进行优化。

因此,React 会在第一个演示的第4步和第二个演示的第3步重新渲染组件。因此,它执行函数内部的所有代码,并为组件的每个子组件调用React.createElement()

但是它不会渲染组件的任何后代,也不会触发效果。

仅对功能组件有效。对于纯类组件,如果状态未更改,则永远不会调用render方法。

我们无所不能避免重新运行。用memo()记忆功能也无济于事,因为it only checks for props changes而不是状态。所以我们只需要考虑这种情况。

这没有回答问题,为什么和什么时候React运行该函数但是失败了,什么时候根本不运行该函数。如果您知道原因,请添加您的答案。