Реактивный компонент иногда рендерит дважды с неизменным состоянием - PullRequest
2 голосов
/ 27 марта 2020

Я использую Redux для подписки на магазин и обновления компонента.

Это упрощенный пример без Redux. Он использует макет для подписки и отправки на.

Пожалуйста, следуйте инструкциям ниже фрагмента, чтобы воспроизвести проблему.

Редактировать : Пожалуйста, перейдите к второй демонстрационный фрагмент под Обновление для более краткого и приближенного к реальному сценарию. Вопрос не о Redux. Речь идет об идентичности функции setState в React, вызывающей повторную визуализацию при определенных обстоятельствах, даже если состояние не изменилось.

Редактировать 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 установлено в «нет». Состояние изменилось, поэтому компонент повторно отображается.
  3. ✅ Нажмите «Установить на Да». yes установлено на «да». Состояние снова изменилось, поэтому компонент повторно отображается.
  4. ⛔ Снова нажмите «Установить на Да». Состояние не изменилось, но компонент является по-прежнему перерисован.
  5. ✅ Последующие нажатия кнопки «Установить на Да» не вызывают повторную визуализацию, как ожидалось.

Что должно произойти

На шаге 4 компонент не должен повторно отображаться, так как состояние не изменяется.

Обновление

Как состояние React docs , useEffect равно

, подходящее для многих распространенных побочных эффектов, таких как настройка подписок и обработчиков событий ...

Одним из таких вариантов использования может быть прослушивание события браузера, такого как online и offline.

. В этом примере мы вызываем функцию внутри 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. component Компонент сначала отображается на экране, а сообщение регистрируется в консоли.
  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 ]

2 голосов
/ 29 марта 2020

Ответ в том, что React использует набор эвристик, чтобы определить, можно ли избежать повторного вызова функции рендеринга. Эта эвристика может меняться в зависимости от версии и не всегда может помочь при одинаковом состоянии. Единственная гарантия, которую предоставляет React, заключается в том, что не будет повторно отображать дочерние компоненты, если состояние было таким же .

Ваши функции рендеринга должны быть чистыми. Поэтому не должно иметь значения, сколько раз они бегут. Если вы вычисляете что-то дорогое в своей функции рендеринга и беспокоитесь о том, чтобы вызывать его больше, чем нужно, вы можете заключить этот расчет в useMemo.

Вообще говоря, в «React» нет смысла «считать рендеры» , Когда именно React вызывает, ваша функция зависит от самого React, и точное время будет меняться между версиями. Это не часть контракта.

2 голосов
/ 28 марта 2020

Кажется, что это ожидаемое поведение.

Из React docs :

Выход из состояния обновления

Если вы обновите State Hook до того же значения, что и текущее состояние, React будет спасаться, не рендерив дочерние или стреляющие эффекты. (React использует Object.is алгоритм сравнения .)

Обратите внимание, что React, возможно, все же потребуется снова отрендерить этот указанный c компонент, прежде чем вывести его из строя. Это не должно вызывать беспокойства, потому что React не излишне go «углубится» в дерево. Если вы выполняете дорогостоящие вычисления во время рендеринга, вы можете оптимизировать их с помощью useMemo.

Итак, React выполняет повторную визуализацию компонента на шаге 4 первой демонстрации и шаг 3 второго. Следовательно, он выполняет весь код внутри функции и вызывает React.createElement() для каждого дочернего элемента компонента.

Однако он не создает никакого потомка компонента и не запускает эффекты.

Это верно только для компонентов функций. Для компонента чистого класса метод render никогда не вызывается, если состояние не изменилось.

Мы ничего не можем сделать, чтобы избежать повторного запуска. Пометка функции с memo() также не поможет, так как проверяет только изменения реквизита , а не состояние. Поэтому мы просто должны принять во внимание эту ситуацию.

Это не отвечает на вопрос, почему и когда React запускает функцию, но выдает ее, а когда она вообще не запускает функцию. Если вы знаете причину, пожалуйста, добавьте свой ответ.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...