Spec-Zone.ru › React

Жизненный цикл реактивных эффектов

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

Вы узнаете

  • В чём отличие жизненного цикла эффекта от жизненного цикла компонента
  • Как рассматривать каждый отдельный эффект изолированно
  • Когда ваш эффект требует повторной синхронизации и почему
  • Как определяются зависимости вашего эффекта
  • Что означает реактивное значение
  • Что означает пустой массив зависимостей
  • Как React проверяет правильность ваших зависимостей с помощью линтера
  • Что делать, если вы не согласны с линтером

Жизненный цикл эффекта

Каждый компонент React проходит через одинаковый жизненный цикл:

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

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

Чтобы проиллюстрировать этот момент, рассмотрите этот эффект, связывающий ваш компонент с чат-сервером:

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

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

Тело вашего эффекта определяет, как начать синхронизацию:

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

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

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

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

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

Примечание

Некоторые эффекты вообще не возвращают функцию очистки. В большинстве случаев вы захотите вернуть её, но если вы этого не сделаете, React будет вести себя так, как будто вы вернули пустую функцию очистки.

Почему синхронизация может потребоваться более одного раза

Представьте, что этот ChatRoom компонент получает roomId свойство, которое пользователь выбирает в раскрывающемся списке. Предположим, что изначально пользователь выбирает "general" комнату как roomId. Ваш приложение отображает "general" чат-комнату:

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

function ChatRoom({ roomId /* "general" */ }) {
  // ...
  return <h1>Welcome to the {roomId} room!</h1>;
}

После отображения пользовательского интерфейса React выполнит ваш эффект, чтобы начать синхронизацию. Он подключается к "general" комнате:

function ChatRoom({ roomId /* "general" */ }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
    connection.connect();
    return () => {
      connection.disconnect(); // Disconnects from the "general" room
    };
  }, [roomId]);
  // ...

Всё хорошо.

Позже пользователь выбирает другую комнату в раскрывающемся списке (например, "travel"). Сначала React обновит пользовательский интерфейс:

function ChatRoom({ roomId /* "travel" */ }) {
  // ...
  return <h1>Welcome to the {roomId} room!</h1>;
}

Подумайте, что должно произойти дальше. Пользователь видит, что "travel" — это выбранная чат-комната в пользовательском интерфейсе. Однако эффект, который был запущен в последний раз, всё ещё подключён к "general" комнате. Значение roomId свойства изменилось, поэтому то, что ваш эффект делал ранее (подключение к "general" комнате), больше не соответствует пользовательскому интерфейсу.

В этот момент вы хотите, чтобы React выполнил две вещи:

  1. Остановить синхронизацию со старой roomId (отключиться от "general" комнаты)
  2. Начать синхронизацию с новой roomId (подключиться к "travel" комнате)

К счастью, вы уже показали React, как это сделать! Тело вашего эффекта определяет, как начать синхронизацию, а функция очистки — как остановить синхронизацию. Всё, что нужно сделать React, — это вызвать их в правильном порядке и с правильными свойствами и состоянием. Давайте посмотрим, как это происходит на практике.

Как React повторно синхронизирует ваш эффект

Вспомните, что ваш ChatRoom компонент получил новое значение для своего roomId свойства. Раньше оно было "general", а теперь "travel". React нужно повторно синхронизировать ваш эффект, чтобы повторно подключиться к другой комнате.

Чтобы остановить синхронизацию, React вызовет функцию очистки, которую ваш эффект вернул после подключения к "general" комнате. Так как roomId было "general", функция очистки отключается от "general" комнаты:

function ChatRoom({ roomId /* "general" */ }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
    connection.connect();
    return () => {
      connection.disconnect(); // Disconnects from the "general" room
    };
    // ...

Затем React выполнит предоставленный вами эффект во время этого рендеринга. На этот раз roomId равно "travel", поэтому он начнёт синхронизацию с "travel" чат-комнатой (пока не будет вызвана его функция очистки):

function ChatRoom({ roomId /* "travel" */ }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // Connects to the "travel" room
    connection.connect();
    // ...

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

Каждый раз, когда ваш компонент повторно рендерится с другим roomId, ваш эффект будет повторно синхронизироваться. Например, предположим, что пользователь меняет roomId с "travel" на "music". React снова остановит синхронизацию вашего эффекта, вызвав его функцию очистки (отключится от "travel" комнаты). Затем он начнёт синхронизацию снова, выполнив его тело со значением нового свойства roomId (подключится к "music" комнате).

Наконец, когда пользователь переходит на другой экран, ChatRoom размонтируется. Теперь нет необходимости оставаться подключенным. React остановит синхронизацию вашего эффекта в последний раз и отключится от "music" чат-комнаты.

Мыслим с точки зрения эффекта

Давайте подведём итоги всему, что произошло с точки зрения ChatRoom компонента:

  1. ChatRoom смонтирован с roomId установленным в "general"
  2. ChatRoom обновлён с roomId установленным в "travel"
  3. ChatRoom обновлён с roomId установленным в "music"
  4. ChatRoom размонтирован

В каждой из этих точек жизненного цикла компонента ваш эффект делал разные вещи:

  1. Ваш эффект подключился к "general" комнате
  2. Ваш эффект отключился от "general" комнаты и подключился к "travel" комнате
  3. Ваш эффект отключился от "travel" комнаты и подключился к "music" комнате
  4. Ваш эффект отключился от "music" комнаты

Теперь давайте посмотрим, что произошло с точки зрения самого эффекта:

  useEffect(() => {
    // Your Effect connected to the room specified with roomId...
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      // ...until it disconnected
      connection.disconnect();
    };
  }, [roomId]);

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

  1. Ваш эффект подключился к "general" комнате (до отключения)
  2. Ваш эффект подключился к "travel" комнате (до отключения)
  3. Ваш эффект подключился к "music" комнате (до отключения)

Ранее вы рассматривали ситуацию с точки зрения компонента. Когда вы смотрели с точки зрения компонента, у вас возникало желание рассматривать эффекты как «обратные вызовы» или «события жизненного цикла», которые срабатывают в определённое время, например, «после рендеринга» или «перед размонтированием». Такой подход быстро усложняется, поэтому лучше его избегать.

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

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

Как React проверяет, что ваш эффект может повторно синхронизироваться

Вот пример, с которым вы можете поработать. Нажмите «Открыть чат», чтобы смонтировать ChatRoom компонент:

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

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <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 ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}

Обратите внимание, что когда компонент монтируется впервые, вы видите три лога:

  1. ✅ Connecting to "general" room at https://localhost:1234... (только для отладки)
  2. ❌ Disconnected from "general" room at https://localhost:1234. (только для отладки)
  3. ✅ Connecting to "general" room at https://localhost:1234...

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

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

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

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

Как React знает, что нужно повторно синхронизировать эффект

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

function ChatRoom({ roomId }) { // The roomId prop may change over time
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // This Effect reads roomId 
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId]); // So you tell React that this Effect "depends on" roomId
  // ...

Вот как это работает:

  1. Вы знали, что roomId — это свойство, которое может меняться со временем.
  2. Вы знали, что ваш эффект считывает roomId (поэтому его логика зависит от значения, которое может измениться позже).
  3. Вот почему вы указали его как зависимость вашего эффекта (чтобы он повторно синхронизировался, когда roomId изменится).

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

Например, если вы передали ["general"] во время начального рендеринга, а затем передали ["travel"] во время следующего рендеринга, React сравнит "general" и "travel". Это разные значения (сравнение выполняется с помощью Object.is), поэтому React повторно синхронизирует ваш эффект. С другой стороны, если ваш компонент повторно рендерится, но roomId не изменился, ваш эффект останется подключённым к той же комнате.

Каждый эффект представляет собой отдельный процесс синхронизации

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

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

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    logVisit(roomId);
  }, [roomId]);

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

Каждый эффект в вашем коде должен представлять собой отдельный и независимый процесс синхронизации.

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

Эффекты «реагируют» на реактивные значения

Ваш эффект считывает две переменные (serverUrl и roomId), но вы указали только roomId в качестве зависимости:

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

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

Почему serverUrl не нужно указывать как зависимость?

Потому что serverUrl никогда не меняется из-за повторного рендеринга. Он всегда одинаковый, независимо от того, сколько раз компонент повторно рендерится и почему. Поскольку serverUrl никогда не меняется, нет смысла указывать его в качестве зависимости. В конце концов, зависимости действуют только тогда, когда они меняются со временем!

С другой стороны, roomId может быть другим при повторном рендеринге. Свойства, состояние и другие значения, объявленные внутри компонента, являются реактивными, потому что они рассчитываются во время рендеринга и участвуют в потоке данных React.

Если serverUrl была переменной состояния, она была бы реактивной. Реактивные значения должны быть включены в зависимости:

function ChatRoom({ roomId }) { // Props change over time
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // State may change over time

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // Your Effect reads props and state
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]); // So you tell React that this Effect "depends on" on props and state
  // ...
}

Включив serverUrl в качестве зависимости, вы гарантируете, что эффект повторно синхронизируется после его изменения.

Попробуйте изменить выбранную чат-комнату или отредактировать URL-адрес сервера в этом песочнице:

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>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <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} />
    </>
  );
}

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

Что означает эффект с пустыми зависимостями

Что произойдёт, если вы перенесёте как serverUrl, так и roomId за пределы компонента?

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

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, []); // ✅ All dependencies declared
  // ...
}

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

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

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

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

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom />}
    </>
  );
}

Однако, если вы думаете с точки зрения эффекта, вам совсем не нужно думать о монтировании и размонтировании. Важно то, что вы указали, что делает ваш эффект для начала и остановки синхронизации. Сегодня у него нет реактивных зависимостей. Но если вы когда-нибудь захотите, чтобы пользователь изменил roomId или serverUrl со временем (и они станут реактивными), ваш код эффекта не изменится. Вам нужно будет только добавить их в зависимости.

Все переменные, объявленные в теле компонента, являются реактивными

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

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

function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
  const settings = useContext(SettingsContext); // settings is reactive
  const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl is reactive
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // Your Effect reads roomId and serverUrl
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]); // So it needs to re-synchronize when either of them changes!
  // ...
}

В этом примере serverUrl не является свойством или переменной состояния. Это обычная переменная, которую вы вычисляете во время рендеринга. Но она вычисляется во время рендеринга, поэтому может меняться при повторном рендеринге. Вот почему она реактивна.

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

Другими словами, эффекты «реагируют» на все значения из тела компонента.

Подробный разбор

Могут ли глобальные или изменяемые значения быть зависимостями?

Изменяемые значения (включая глобальные переменные) не являются реактивными.

Изменяемое значение, например, location.pathname, не может быть зависимостью. Оно изменяемое, поэтому оно может изменяться в любое время вне потока данных рендеринга React. Изменение его не вызовет повторного рендеринга вашего компонента. Следовательно, даже если вы укажете его в зависимостях, React не будет знать, что нужно повторно синхронизировать эффект, когда оно изменится. Это также нарушает правила React, поскольку чтение изменяемых данных во время рендеринга (когда вы вычисляете зависимости) нарушает чистоту рендеринга. Вместо этого вы должны считывать и подписываться на внешнее изменяемое значение с помощью useSyncExternalStore.

Изменяемое значение, например, ref.current, или вещи, которые вы читаете из него, также не могут быть зависимостями. Объект ref, возвращаемый useRef, сам может быть зависимостью, но его свойство current намеренно изменяемое. Это позволяет вам отслеживать что-то, не вызывая повторного рендеринга. Но поскольку изменение его не вызывает повторного рендеринга, это не реактивное значение, и React не будет знать, что нужно повторно запустить ваш эффект, когда оно изменится.

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

React проверяет, что вы указали каждое реактивное значение в качестве зависимости

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

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

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

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Something's wrong here!

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <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} />
    </>
  );
}

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

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

function ChatRoom({ roomId }) { // roomId is reactive
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]); // ✅ All dependencies declared
  // ...
}

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

Примечание

В некоторых случаях React знает, что значение никогда не изменяется, даже если оно объявлено внутри компонента. Например, функция set, возвращаемая useState, и объект ref, возвращаемый useRef, являются стабильными — они гарантированно не меняются при повторном рендеринге. Стабильные значения не являются реактивными, поэтому вы можете опустить их из списка. Включение их разрешено: они не изменятся, поэтому это не имеет значения.

Что делать, когда вы не хотите повторно синхронизироваться

В предыдущем примере вы исправили ошибку линтинга, указав roomId и serverUrl в качестве зависимостей.

Однако вы можете вместо этого «доказать» линтеру, что эти значения не являются реактивными значениями, т.е. что они не могут измениться в результате повторного рендеринга. Например, если serverUrl и roomId не зависят от рендеринга и всегда имеют одинаковые значения, вы можете перенести их за пределы компонента. Теперь они не нужны как зависимости:

const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, []); // ✅ All dependencies declared
  // ...
}

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

function ChatRoom() {
  useEffect(() => {
    const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
    const roomId = 'general'; // roomId is not reactive
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, []); // ✅ All dependencies declared
  // ...
}

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

Вы не можете «выбрать» свои зависимости. Ваши зависимости должны включать каждое реактивное значение, которое вы читаете в эффекте. Линтер это проверяет. Иногда это может привести к проблемам, таким как бесконечные циклы и слишком частой ресинхронизации вашего эффекта. Не пытайтесь решить эти проблемы, подавляя линтер! Вот что попробовать вместо этого:

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

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

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

Проблема

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

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

useEffect(() => {
  // ...
  // 🔴 Avoid suppressing the linter like this:
  // eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

На следующих страницах вы узнаете, как исправить этот код, не нарушая правил. Стоит всегда это исправлять!

Резюме

  • Компоненты могут устанавливаться, обновляться и отключаться.
  • Каждый эффект имеет отдельный жизненный цикл от окружающего компонента.
  • Каждый эффект описывает отдельный процесс синхронизации, который может начинаться и заканчиваться.
  • Когда вы пишете и читаете эффекты, думайте с точки зрения каждого отдельного эффекта (как начать и остановить синхронизацию), а не с точки зрения компонента (как он устанавливается, обновляется или отключается).
  • Значения, объявленные внутри тела компонента, являются «реактивными».
  • Реактивные значения должны ресинхронизировать эффект, так как они могут со временем изменяться.
  • Линтер проверяет, что все реактивные значения, используемые внутри эффекта, указаны как зависимости.
  • Все ошибки, отмеченные линтером, являются обоснованными. Всегда есть способ исправить код, чтобы он не нарушал правила.

Попробуйте выполнить некоторые задачи

Задача 1 из 5:
Исправить переподключение при каждом нажатии клавиши

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

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

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

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

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

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

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <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} />
    </>
  );
}

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/learn/lifecycle-of-reactive-effects

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API