Хук useEffect позволяет компонентам синхронизироваться с системами вне React.

useEffect(setup, dependencies?)

Справочник

useEffect(setup, dependencies?)

Чтобы создать эффект, вызовите useEffect на верхнем уровне своего компонента.

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}

См. другие примеры ниже.

Параметры

  • setup: Функция установки вашего эффекта. Если нужно, из функции можно вернуть функцию сброса. Сначала React вызовет setup, когда добавит компонент в DOM. Потом при изменении зависимостей будет вызывать после рендеринга сначала функцию сброса со старыми зависимостями (если такая была), и затем функцию установки с новыми зависимостями. В конце React вызовет последнюю полученную функцию сброса, когда удалит компонент из DOM.

  • необязательный dependencies: Список всех реактивных значений, от которых зависит функция setup: пропсы, состояние, переменные и функции, объявленные непосредственно в теле вашего компонента. Если вы настроите свой линтер под React, то он сможет автоматически следить, чтобы все нужные реактивные значения были указаны в зависимостях. Количество зависимостей должно быть всегда одинаковое, а сам список нужно указать прямо в месте передачи параметра как [dep1, dep2, dep3]. React будет сравнивать старые и новые значения зависимостей через Object.is. Если не указать зависимости совсем, то эффект будет запускаться после каждого рендеринга. Важно понимать, как будет отличаться поведение, если передать список с зависимостями, пустой список, или не передать ничего.

Возвращаемое значение

useEffect ничего не возвращает.

Замечания

  • useEffect — это хук, поэтому его нужно вызывать только на верхнем уровне ваших компонентов или хуков. Его нельзя вызывать внутри циклов и условий. Если это всё же для чего-то нужно, выделите этот вызов в отдельный компонент, который затем можно рендерить по условию или в цикле.

  • Если ваша цель не в том, чтобы синхронизировать компонент с некой внешней системой, то возможно, вам и не нужен Эффект.

  • В Строгом режиме (Strict Mode) после первого рендеринга React вызовет setup дважды: один раз вызовет setup и сразу его сброс, и затем вызовет setup как обычно. Это будет происходить только в режиме разработки. Такой тест помогает убедиться, что сброс эффекта “обратен” его установке: он отменяет и откатывает всю ту работу, которую проделала функция setup. Если у вас нет функции сброса, а тест приводит к неправильной работе — значит вам нужна функция сброса.

  • Если эффект зависит от объектов или функций, которые создаются в компоненте, есть риск, что эффект будет запускаться слишком часто. Такие лишние зависимости можно убрать, переместив создание объекта или функции внутрь эффекта. Кроме того можно убрать зависимость от состояния, если эффект просто его обновляет. А не реактивную логику можно вынести за пределы эффекта.

  • Перед тем, как запустить эффект, React сначала даст браузеру возможность отрисовать изменения на экране, а потом запустит ваш эффект. Поэтому если ваш эффект после рендеринга делает ещё какие-то визуальные изменения (например, поправляет положение отрендеренной всплывающей подсказки), то эти изменения могут появиться с задержкой (подсказка на мгновение всплывёт в неправильном месте, и сразу переместится в правильное). Если эта задержка слишком заметна, попробуйте заменить useEffect на useLayoutEffect.

  • Аналогично, если ваш эффект меняет состояние в ответ на действия пользователя (например, должен сработать после клика), то нужно учитывать, что сначала браузер обновит экран, а потом подействуют изменения состояния, которые делает эффект. Обычно это ожидаемое поведение. Но если вам всё же важно обновить состояние до отрисовки браузером, то useEffect нужно заменить на useLayoutEffect.

  • Эффекты запускаются только на клиенте. Они не запускаются во время серверного рендеринга.


Применение

Подключение к внешней системе

Иногда в компоненте нужно установить сетевое соединение, подключиться к браузерному API или сторонней библиотеке, и поддерживать подключение или подписку, пока компонент показан на странице. React не управляет этими системами, они не являются его частью — такие системы называются внешними.

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

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}

В useEffect нужно передать два аргумента:

  1. Функцию установки, которая устанавливает эффект, подключаясь к внешней системе.
    • Установка должна вернуть функцию сброса, которая сбрасывает эффект, отключаясь от внешней системы.
  2. Список зависимостей этих функций, где перечислены все нужные им значения в компоненте.

По мере необходимости React вызовет функции установки и сброса несколько раз:

  1. Когда ваш компонент появится на странице (монтируется), выполнится код установки.
  2. После каждого рендеринга, в котором изменились зависимости:
    • Сначала запустится код сброса со старыми пропсами и состоянием.
    • Затем запустится код установки с новыми пропсами и состоянием.
  3. В конце, когда ваш компонент будет удалён со страницы (размонтируется), выполнится код сброса.

Разберём эту последовательность на примере кода выше.

Когда в примере выше компонент ChatRoom добавится на страницу, эффектом добавления станет подключение к чату, используя начальные значения roomId и serverUrl. Если в процессе рендеринга serverUrl или roomId изменятся (например, пользователь выберет в выпадающем меню другой чат), эффект отключится от предыдущего чата, и подключится к новому чату. А когда компонент будет удалён со страницы, эффект закроет последнее подключение.

В режиме разработки для выявления дефектов React будет запускать один предварительный цикл установки и сброса перед тем, как начинать установку как обычно. Это такой стресс-тест, проверяющий, что логика вашего эффекта реализована правильно. Если вы видите, что тест создаёт проблемы — значит у вас в логике сброса чего-то не хватает. Код сброса должен отменять и откатывать всю ту работу, которую проделал код установки. Эмпирическое правило такое: пользователь не должен замечать разницы, вызвалась установка один раз (как в продакшене) или последовательностью установкасбросустановка (как в режиме разработки). См. решения для типичных ситуаций.

Старайтесь описывать каждый эффект, как отдельный процесс, а так же рассматривать каждую установку вместе с её сбросом, как одно целое. Монтируется компонент, обновляется, или размонтируется — не должно играть роли. Если правильно реализовать сброс — как “зеркальное отражение” установки, — то ваш эффект сможет без последствий устанавливаться и сбрасываться настолько часто, насколько потребуется.

Note

Эффект позволяет компоненту быть всегда синхронизированным с внешней системой (например, с сервисом чата). Под внешней системой здесь подразумевается в принципе любой код, которым не управляет React, как например:

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

Примеры подключения к внешним системам

Example 1 of 5:
Подключение к чат-серверу

В этом примере компонент ChatRoom с помощью эффекта создаёт и поддерживает подключение к внешней системе: чат-серверу, описанному в chat.js. Нажмите “Открыть чат” — появится компонент ChatRoom. Т.к. песочница здесь работает в режиме разработки, то будет дополнительный цикл подключения и отключения — о чём подробнее рассказано здесь. Обратите внимание, как эффект переподключается к чату, если в выпадающем списке выбрать другой roomId или в поле ввода изменить serverUrl. Нажмите “Закрыть чат” — и эффект отключится от чата.

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        Адрес сервера:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Добро пожаловать в {roomId}!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Выберите чат:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Закрыть чат' : 'Открыть чат'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}


Оборачивание эффекта в пользовательский хук

Эффекты – это “лазейка”: они нужны, чтобы “выйти за рамки React”, или когда для задачи нет встроенного решения. Если вам постоянно приходится писать собственные эффекты, то возможно ваши эффекты реализуют повторяющуюся логику, которую можно вынести в отдельный пользовательский хук.

Например, вот пользовательский хук useChatRoom, который “скрывает” логику эффекта за декларативным API:

function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

И вот так им можно пользоваться в разных компонентах:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

В экосистеме React можно найти много других замечательных примеров пользовательских хуков на все случаи жизни.

Подробнее о том, как завернуть эффект в пользовательский хук.

Примеры оборачивания эффекта в пользовательский хук

Example 1 of 3:
Пользовательский хук useChatRoom

Это повторение одного из предыдущих примеров, но логика здесь вынесена в пользовательский хук.

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Адрес сервера:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Добро пожаловать в {roomId}!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Выберите чат:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Закрыть чат' : 'Открыть чат'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}


Управление виджетом, написанным не на React

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

Например, когда у вас есть написанный без React сторонний виджет карты или видео проигрыватель, то в эффекте можно, вызывая их методы, транслировать в них изменения состояния вашего компонента. В данном случае эффект создаёт объект класса MapWidget, который описан в map-widget.js. Когда у компонента Map изменяется проп zoomLevel, эффект вызывает у объекта setZoom(), чтобы соответствующим образом обновилось и состояние виджета:

import { useRef, useEffect } from 'react';
import { MapWidget } from './map-widget.js';

export default function Map({ zoomLevel }) {
  const containerRef = useRef(null);
  const mapRef = useRef(null);

  useEffect(() => {
    if (mapRef.current === null) {
      mapRef.current = new MapWidget(containerRef.current);
    }

    const map = mapRef.current;
    map.setZoom(zoomLevel);
  }, [zoomLevel]);

  return (
    <div
      style={{ width: 200, height: 200 }}
      ref={containerRef}
    />
  );
}

Функция сброса эффекта в данном случае не нужна, т.к. MapWidget просто держит ссылку на узел в DOM и больше никак ресурсами не управляет. А значит и MapWidget, и DOM-узел будут просто автоматически удалены сборщиком мусора JavaScript, когда компонент Map будет удалён из дерева.


Получение данных в эффекте

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

Если вы всё же хотите вручную в эффекте запрашивать данные, то можно написать, например, такой код:

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);

useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);

// ...

Обратите внимание на переменную ignore: она изначально содержит false, а при сбросе эффекта изменяется на true. Этим гарантируется, что в коде не будет “гонки” из-за того, что ответы на сетевые запросы могут приходить не в том порядке, в котором были посланы запросы.

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Алиса');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Алиса">Алиса</option>
        <option value="Боб">Боб</option>
        <option value="Тэйлор">Тэйлор</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Загрузка...'}</i></p>
    </>
  );
}

Получение данных можно переписать на async / await, но функция сброса всё равно будет нужна:

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Алиса');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    async function startFetching() {
      setBio(null);
      const result = await fetchBio(person);
      if (!ignore) {
        setBio(result);
      }
    }

    let ignore = false;
    startFetching();
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Алиса">Алиса</option>
        <option value="Боб">Боб</option>
        <option value="Тэйлор">Тэйлор</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Загрузка...'}</i></p>
    </>
  );
}

Чтобы регулярно делать запросы прямо в эффектах, придётся каждый раз писать много повторяющегося кода. Из-за чего в будущем будет сложнее добавить туда оптимизации вроде кэширования или серверного рендеринга. Поэтому проще делать запросы через специальный хук — написать собственный, либо найти готовый, поддерживаемый сообществом.

Deep Dive

Какие есть альтернативы получению данных в эффекте?

Вызывать fetch в эффекте — это распространённый способ получать данные. Особенно в полностью клиентских приложениях. Однако у этого весьма “наколеночного” подхода есть недостатки:

  • На сервере эффекты не запускаются. Это значит, что в полученном серверным рендерингом HTML будет только состояние загрузки без данных. Клиентское устройство скачает весь ваш JavaScript, отрендерит приложение, и обнаружит, что теперь нужно ещё и данные загрузить. Это не самый эффективный подход.
  • Запрашивая данные прямо в эффектах, легко создать “водопад загрузки”. Сначала рендерится родительский компонент, получает данные, рендерит дочерние компоненты, которые затем начинают запрашивать свои данные. Если сетевое соединение не быстрое, то такой процесс будет сильно медленнее, чем загружать все данные параллельно.
  • Получение данных прямо в эффекте обычно не предполагает предзагрузку или кэширование. Если, например, компонент размонтировался и потом снова монтируется, то ему нужно будет заново загрузить данные.
  • Это неудобно. Если не хочется столкнуться с багами вроде гонок, то каждый раз придётся писать некоторое количество весьма шаблонного кода.

Эти недостатки не являются какой-то особенностью именно React. Такие же проблемы получения данных при монтировании будут и с любой другой библиотекой. Как и маршрутизацию, нельзя просто взять и правильно реализовать получение данных — поэтому мы рекомендуем следующие подходы:

  • Если вы используете фреймворк, то возьмите встроенный в него механизм получения данных. В современные React-фреймворки уже встроены эффективные механизмы получения данных без перечисленных недостатков.
  • Если нет, то попробуйте написать или использовать готовый клиентский кэш. React Query, useSWR, и React Router 6.4+ — примеры популярных готовых решений с открытым исходным кодом. Создать собственное решение тоже можно: под капотом будут эффекты, но также и логика для дедупликации запросов, кэширования ответов и предотвращения водопадов (через предзагрузку или через перенос на уровень маршрутов требований, какие данные будут нужны).

Получать данные прямо в эффекте — всё ещё вполне приемлемый вариант, если ничто из перечисленного вам не подходит.


Указание реактивных зависимостей

Обратите внимание, что “выбрать” зависимости эффекта нельзя. В зависимостях эффекта должно быть указано каждое реактивное значение, которое в нём используется. Список зависимостей эффекта можно определить по окружающему коду:

function ChatRoom({ roomId }) { // Это реактивное значение
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // И это реактивное значение

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Эффект их читает
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ И поэтому они должны быть указаны в его зависимостях
// ...
}

Когда serverUrl или roomId будут изменяться, эффект будет переподключаться к чату с новыми значениями.

Реактивными значениями считаются пропсы, переменные и функции, объявленные непосредственно в теле вашего компонента. Т.к. значения roomId и serverUrl оба являются реактивными, то их нельзя опустить в списке зависимостей. Если их не указать, то правильно настроенный под React линтер укажет на это, как на ошибку, которую нужно поправить:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
// ...
}

Чтобы удалить зависимость из списка, нужно “доказать” линтеру, что она не обязана там быть. Например, можно вынести serverUrl из компонента, чтобы показать, что это значение не реактивно и не изменяется во время рендеринга:

const serverUrl = 'https://localhost:1234'; // Значение больше не реактивное

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Все зависимости указаны
// ...
}

Т.к. теперь значение serverUrl не реактивное (и не может изменяться во время рендеринга), то его не нужно указывать как зависимость. Если ваш эффект не пользуется никакими реактивными значениями, то его список зависимостей должен быть пустым ([]):

const serverUrl = 'https://localhost:1234'; // Значение больше не реактивное
const roomId = 'music'; // Значение больше не реактивное

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Все зависимости указаны
// ...
}

Изменения пропсов или состояния компонента не будут перезапускать эффект, у которого список зависимостей пуст.

Pitfall

Возможно в вашей существующей кодовой базе в некоторых эффектах есть вот такое отключение линтера:

useEffect(() => {
// ...
// 🔴 Не отключайте так линтер:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

Когда список зависимостей не соответствует коду, велик шанс появления багов. Отключая линтер, вы “обманываете” React о том, от каких значений зависит эффект. Лучше покажите, что они не нужны.

Примеры указания реактивных зависимостей

Example 1 of 3:
Передача массива с зависимостями

Если указать какие-то зависимости, то эффект будет запускаться после первого рендеринга, а также после каждого рендеринга, в котором зависимости изменились.

useEffect(() => {
// ...
}, [a, b]); // Перезапустится, если a или b изменились

В примере ниже значения serverUrl и roomId реактивны, и поэтому должны быть указаны в зависимостях. В результате чего, если в выпадающем меню выбрать другой чат, либо ввести другой адрес сервера, то компонент переподключится к чату. А вот сообщение message в эффекте не используется (и соответственно зависимостью не является), и поэтому редактирование сообщения не приводит к переподключению к чату.

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);

  return (
    <>
      <label>
        Адрес сервера:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Добро пожаловать в {roomId}!</h1>
      <label>
        Ваше сообщение:{' '}
        <input value={message} onChange={e => setMessage(e.target.value)} />
      </label>
    </>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Выберите чат:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
        <button onClick={() => setShow(!show)}>
          {show ? 'Закрыть чат' : 'Открыть чат'}
        </button>
      </label>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId}/>}
    </>
  );
}


Обновление в эффекте состояния на основе предыдущего состояния

Если вы захотите обновлять в эффекте состояние на основе его предыдущего значения, то можете наткнуться на проблему:

function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // Хотим увеличивать `count` каждую секунду...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... но из-за того, что `count` в зависимостях, интервал постоянно перезапускается.
// ...
}

Т.к. count — это реактивное значение, то оно должно быть указано в списке зависимостей. Но из-за этого эффекту приходится сбрасываться и запускаться заново каждый раз, когда count обновляется. Выглядит не идеально.

Можно сделать лучше, если передавать в setCount функцию обновления состояния c => c + 1:

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ Передаём функцию обновления
    }, 1000);
    return () => clearInterval(intervalId);
  }, []); // ✅ Теперь `count` нет в зависимостях.

  return <h1>{count}</h1>;
}

Поскольку вместо count + 1 теперь передаётся c => c + 1, то больше нет нужды указывать count в зависимостях эффекта. А значит эффекту больше не приходится сбрасывать и заново устанавливать интервал каждый раз, когда изменяется count.


Устранение лишних зависимостей от объектов

Если ваш эффект зависит от объекта или функции, которые создаются во время рендеринга, то возможно ваш эффект срабатывает слишком часто. Например, вот этот эффект делает переподключение на каждый рендеринг, т.к. объект options каждый раз новый:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

const options = { // 🚩 Этот объект пересоздаётся при каждом рендеринге
serverUrl: serverUrl,
roomId: roomId
};

useEffect(() => {
const connection = createConnection(options); // И используется в эффекте
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 В результате при каждом рендеринге изменяются зависимости
// ...

Постарайтесь не делать созданный во время рендеринга объект зависимостью. Лучше создавайте нужный объект внутри эффекта:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Добро пожаловать в {roomId}!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Выберите чат:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Т.к. теперь объект options создаётся внутри эффекта, то сам эффект теперь зависит только от строки roomId.

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


Устранение лишних зависимостей от функций

Если ваш эффект зависит от объекта или функции, которые создаются во время рендеринга, то возможно ваш эффект срабатывает слишком часто. Например, вот этот эффект делает переподключение на каждый рендеринг, т.к. функция createOptions каждый раз новая:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

function createOptions() { // 🚩 Эта функция пересоздаётся при каждом рендеринге
return {
serverUrl: serverUrl,
roomId: roomId
};
}

useEffect(() => {
const options = createOptions(); // И используется в эффекте
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 В результате при каждом рендеринге изменяются зависимости
// ...

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

Постарайтесь не делать созданную во время рендеринга функцию зависимостью. Лучше объявите нужную функцию внутри эффекта:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    function createOptions() {
      return {
        serverUrl: serverUrl,
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Добро пожаловать в {roomId}!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Выберите чат:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Т.к. теперь функция createOptions определена внутри эффекта, то сам эффект теперь зависит только от строки roomId. Благодаря этой правке, больше не происходит переподключение к чату, когда пользователь печатает в поле ввода. В отличие от пересоздаваемых функций, строки вроде roomId не будут считаться изменившимися, пока не изменятся по значению. Подробнее об устранении зависимостей.


Чтение в эффекте актуальных пропсов и состояния

Under Construction

Здесь описано экспериментальное API, которое ещё не опубликовано в стабильном релизе React.

По умолчанию если в эффекте читается реактивное значение, то его нужно указать в зависимостях. Благодаря этому эффект будет “реагировать” на каждое изменение значения. И для большинства зависимостей как раз такое поведение вам и нужно.

Но иногда вам может понадобиться читать в эффекте реактивное значение, но не “реагировать” на него. Например, представьте себе, что нужно при каждом посещении страницы записывать в лог текущее количество товаров в корзине:

function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ Все зависимости указаны
// ...
}

Что если запись в лог нужно делать только при изменении url, а при изменении shoppingCart — не нужно? Убрать shoppingCart из зависимостей так, чтобы не сломать правила реактивности, нельзя. Зато можно для некоторого куска кода обозначить, что он не должен “реагировать” на изменения, даже когда вызывается в эффекте. Для этого вы можете с помощью хука useEffectEvent объявить Событие эффекта, и поместить в него код, читающий shoppingCart:

function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});

useEffect(() => {
onVisit(url);
}, [url]); // ✅ Все зависимости указаны
// ...
}

Событие эффекта — это не реактивное значение, и поэтому не должно указываться в зависимостях эффекта. Благодаря этому в событие можно помещать нереактивный код (где можно читать последние значения пропсов и состояния). Поскольку чтение shoppingCart теперь происходит внутри onVisit, то изменение shoppingCart не будет перезапускать эффект.

Подробнее о том, как события эффектов позволяют отделить нереактивный код от реактивного.


Отображение разного содержимого на сервере и на клиенте

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

При этом изредка вам может понадобиться отобразить на клиенте другое содержимое. Например, если ваше приложение читает данные из localStorage — что в принципе невозможно делать на сервере. В таком случае разное отображение можно реализовать вот так:

function MyComponent() {
const [didMount, setDidMount] = useState(false);

useEffect(() => {
setDidMount(true);
}, []);

if (didMount) {
// ... возвращаем особый клиентский JSX ...
} else {
// ... возвращаем первоначальный JSX ...
}
}

Пока приложение загружается, пользователю будут показываться результаты первоначального рендеринга. После загрузки и гидратации сработает эффект, и didMount выставится в true, из-за чего запустится повторный рендеринг. Так первоначальное содержимое сменится на особое клиентское. На сервере эффекты не срабатывают — поэтому при первоначальном рендеринге на сервере didMount оставался false.

Не стоит злоупотреблять этим паттерном. Учтите, что пользователи с медленным соединением могут довольно долго наблюдать первоначальное содержимое — может даже несколько секунд. В такой момент нежелательно делать заметные дёрганные изменения во внешнем виде компонентов. Часто избежать резких переходов можно, отдельно стилизовав первоначальное содержимое под такую ситуацию.


Устранение неполадок

Мой эффект запускается дважды, когда компонент монтируется

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

Это такой стресс-тест, проверяющий, что логика вашего эффекта реализована правильно. Если вы видите, что тест создаёт проблемы — значит у вас в логике сброса чего-то не хватает. Код сброса должен отменять и откатывать всю ту работу, которую проделал код установки. Эмпирическое правило такое: пользователь не должен замечать разницы, вызвалась установка один раз (как в продакшене) или последовательностью установкасбросустановка (как во время разработки).

Изучите подробнее, как этот тест помогает выявлять дефекты, и как исправить логику эффекта.


Мой эффект запускается при каждом рендеринге

Для начала убедитесь, что не забыли указать массив зависимостей:

useEffect(() => {
// ...
}); // 🚩 Нет массива зависимостей: эффект срабатывает при каждом рендеринге!

Если вы указали массив зависимостей, но эффект всё равно постоянно перезапускается — значит при каждом рендеринге изменяется какая-то из зависимостей.

Чтобы отладить проблему, выведите массив зависимостей в консоль:

useEffect(() => {
// ..
}, [serverUrl, roomId]);

console.log([serverUrl, roomId]);

Затем выберите в консоли два массива из двух разных проходов рендеринга, по каждому кликните правой кнопкой мыши и выберите в меню “Сохранить как глобальную переменную” (“Store as a global variable”). Если, допустим, первый массив сохранился в переменную temp1, а второй — в temp2, то теперь можно в консоли браузера вот так проверить на равенство каждую отдельную зависимость:

Object.is(temp1[0], temp2[0]); // Совпадает ли в массивах первая зависимость?
Object.is(temp1[1], temp2[1]); // Совпадает ли в массивах вторая зависимость?
Object.is(temp1[2], temp2[2]); // ... и так для каждой зависимости ...

Так вы найдёте, какая зависимость изменяется при каждом рендеринге. Поправить её обычно можно одним из следующих способов:

В крайнем случае (если ни один из способов выше не подошёл) можно обернуть вычисление зависимости в useMemo, либо в useCallback, если это функция.


Мой эффект без конца перезапускается

Если ваш эффект перезапускается в бесконечном цикле, то скорее всего имеют место быть оба следующих факта:

  • Ваш эффект изменяет состояние.
  • Изменение состояния перезапускает рендеринг, и в процессе изменяются зависимости эффекта.

Перед тем, как приступить к исправлению, задайтесь вопросом: подключается ли к внешней системе ваш эффект (к DOM, к сети, к стороннему виджету, и т.п.)? Зачем этому эффекту изменять состояние? Он так синхронизируется с внешней системой? Или вы так просто пытаетесь управлять потоком данных в приложении?

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

Если вы действительно пытаетесь синхронизироваться с внешней системой, то подумайте, зачем и при каких именно условиях эффект должен изменять состояние. Влияют ли изменения на визуальное отображение компонента? Если нужно отслеживать какие-то данные, не участвующие в рендеринге, то возможно вам больше подойдёт реф (без перезапуска рендеринга). Если нужно обновлять состояние (с перезапуском рендеринга), убедитесь, что обновление не происходит чаще, чем нужно.

Наконец, если эффект обновляет состояние ровно тогда, когда это нужно, но всё равно без конца перезапускается — значит обновление состояния приводит к изменению какой-то зависимости эффекта. Почитайте, как можно отладить изменения в зависимостях.


Моя логика сброса запустилась, хотя компонент не размонтируется

Сброс эффекта происходит не только при размонтировании, но и после каждого рендеринга, в котором изменились зависимости. Кроме того, в режиме разработки React делает при монтировании один дополнительный запуск установки и сброса.

Если для вашего кода сброса нет соответствующего кода установки — обычно это признак проблем в коде:

useEffect(() => {
// 🔴 Не делайте так: логика сброса не соответствует логике установки.
return () => {
doSomething();
};
}, []);

Ваша логика сброса должна быть “симметрична” логике установки, отменяя и откатывая всю ту работу, которую проделала установка:

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);

Изучите, в чём отличие жизненного цикла эффекта по сравнению с жизненным циклом компонента.


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

Если вам нужно, чтобы браузер не отрисовывал экран, пока не сработает эффект, то замените useEffect на useLayoutEffect. Однако помните, что для подавляющего большинства эффектов такое не должно быть нужно. Замена будет нужна только там, где критически важно, чтобы эффект сработал до отрисовки браузером: например, чтобы эффект вычислил размеры и расположение для всплывающей подсказки до того, как её увидит пользователь.