💡 Это подробное руководство, призванное пролить свет на поведение хука useEffect. Это руководство предназначено как для начинающих, так и для опытных пользователей. Это руководство не требует предварительного опыта работы с useHooks, мы рассмотрим все по мере продвижения, начиная с основ, а затем постепенно увеличивая сложность.

Что такое useEffect?

ЭффектHookпозволяет выполнять побочные эффекты в функциональных компонентах:

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);
  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

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

Извлечение данных, настройка подписки и изменение модели DOM в компонентах React вручную — все это примеры побочных эффектов. Независимо от того, привыкли ли вы называть эти операции «побочными эффектами» (или просто «эффектами»), вы, вероятно, выполняли их в своих компонентах раньше.

Реагировать на документацию

Я настоятельно рекомендую прочитать документацию по реакции, так как она дает отличное объяснение хука.

Как предотвратить бесконечный рендеринг

Давайте создадим пример для реагирующего приложения, которое отображает объект пользователя в виде строки, где начальное состояние пользователя — null, и мы извлекаем данные из внешнего API. Мы хотим отобразить объект пользователя в тегах ‹pre› в приложении или User unknown, если пользователь нулевой. Я добавил два журнала консоли, чтобы помочь нам понять поведение хука и когда запускается рендеринг.

Сколько раз приведенный ниже фрагмент будет отображать страницу?

import {useState, useEffect} from "react";

const App = () => {
	const [user, setUser] = useState(null)
	console.log('Rendering App');

	useEffect(() => {
	    console.log('Running useEffect');
	    fetch('<https://jsonplaceholder.typicode.com/users/1>')
			.then(response => response.json())
			.then(json => setUser(json))
	})

	return (
		<div>
			{user ?
			<>
			    <pre>{JSON.stringify(user)}</pre>
			</>
			:
			    <p>User unknown</p>
			}

		</div>
	);
}

export default App;

Ответ на это навсегда! Но почему?!? Почему он бесконечно перерисовывает приложение?

Давайте посмотрим, как выполняется наш код.

  1. Первоначально мы начинаем с состояния пользователя, установленного на null. Это то, что мы определили как наше начальное состояние.
  2. Затем наш useEffect извлекает данные из внешнего API и обновляет их с помощью setUser, после чего мы входим в цикл, из которого не можем выбраться из-за постоянного повторного рендеринга приложения.

Одна из причин этого заключается в том, что мы не указали массив зависимостей для нашего useEffect.

Поведение эффектов по умолчанию – запуск эффекта после каждого завершенного рендеринга. Таким образом, эффект всегда создается заново при изменении одной из его зависимостей.

Чтобы реализовать это, передайте второй аргумент в useEffect, который представляет собой массив значений, от которых зависит эффект. Наш обновленный пример теперь выглядит так:

Реагировать на документацию

В конце концов, после добавления массива зависимостей с пользователем как частью массива код должен выглядеть так.

import {useState, useEffect} from "react";

const App = () => {
	const [user, setUser] = useState(null)
	console.log('Rendering App');

	useEffect(() => {
	    console.log('Running useEffect');
	    fetch('<https://jsonplaceholder.typicode.com/users/1>')
			.then(response => response.json())
			.then(json => setUser(json))
	}, [user])

	return (
		<div>
		{user ?
		<>
		    <pre>{JSON.stringify(user)}</pre>
		</>
		:
		    <p>User unknown</p>
		}

		</div>
	);
}

export default App;

Но знаете что? Он по-прежнему бесконечно перерисовывается. Итак, что здесь происходит? Почему в документации по реакции, в контрпримере из вводной части, мы не имеем дело с этим? Должно работать, да???

Чтобы ответить на этот вопрос, нам сначала нужно понять ссылки javascript и то, как они работают. Это важный шаг для понимания того, как работают useEffect, useMemo и другие концепции React.

Терминология:

  • скаляр — отдельное значение или единица данных (например, целое число, логическое значение, строка).
  • составной — состоит из нескольких значений (например, массив, объект, набор)
  • примитив — прямое значение, а не ссылка на что-то, что содержит реальное значение.

В JavaScript скаляры являются примитивами.

Источник: Средний

Это все скаляры, оцениваемые и сравниваемые по значению.

Этот механизм сравнения не работает при оценке составных переменных, таких как объекты и массивы, которые оцениваются по ссылке.

Как вы можете ясно видеть на изображении выше, reference1 и reference2 — это массивы, которые содержат ту же самую строку, что и их единственный элемент. Тогда почему не ссылка1 === ссылка2? Почему это ложно? Потому что массивы и объекты оцениваются по ссылке.

Если мы создадим новую константу reference3 и присвоим ей значение reference1, мы фактически укажем экземпляру reference3 память, выделенную для reference1, тем самым сделав их равными.

Источник: Джек Херрингтон

Объяснение

Вот почему наш сниппет входит в бесконечный цикл. Поскольку мы настроили пользователя в нашем useEffect так, чтобы он был тем, кого мы захватываем из запроса API, это вызовет цикл рендеринга, в котором useEffect запустится снова, сравнивая текущего пользователя в нашем состоянии с тем, который мы получаем из нашего API. . Поскольку объекты сравниваются по ссылке, приложение войдет в бесконечный цикл.

Как мы можем это исправить?

Есть несколько способов исправить это, но пока что самым простым было бы строковое преобразование объекта.

Как объяснялось ранее, строки являются скалярами, поэтому, если мы преобразуем нашего пользователя в строковую версию объекта, это должно решить проблему.

import {useState, useEffect, useRef} from "react";

const App = () => {
	const [user, setUser] = useState(null)
	console.log('Rendering App');

	useEffect(() => {
	    console.log('Running useEffect');
	    fetch('<https://jsonplaceholder.typicode.com/users/1>')
			.then(response => response.json())
			.then(json => {
				setUser(JSON.stringify(json))
			})
	}, [user])

	return (
		<div>
			{user ?
				<>
					<pre>{user}</pre>
				</>
				:
				<p>User unknown</p>
			}

		</div>
	);
}

export default App;

Если вы это сделали, поздравляю, надеюсь, вы лучше поняли хук useEffect и теперь все имеет больше смысла.