Spec-Zone.ru › React

Удаление зависимостей эффектов

Когда вы пишете эффект, линтер проверяет, включили ли вы в список зависимостей все реактивные значения (например, props и state), которые эффект считывает. Это позволяет эффекту оставаться синхронизированным с последними значениями props и state вашего компонента. Ненужные зависимости могут приводить к слишком частому запуску эффекта или даже создавать бесконечный цикл. Следуйте этому руководству, чтобы проверить эффекты и удалить из них ненужные зависимости.

Вы узнаете

  • Как исправить бесконечные циклы зависимостей эффектов
  • Что делать, если вы хотите удалить зависимость
  • Как считывать значение из эффекта, не «реагируя» на него
  • Как и почему следует избегать зависимостей в виде объектов и функций
  • Почему опасно отключать линтер зависимостей и что делать вместо этого

Зависимости должны соответствовать коду

Когда вы пишете эффект, сначала укажите, как запустить и остановить то, что должен делать ваш эффект:

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

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

Затем, если оставить список зависимостей эффекта пустым ([]), линтер предложит правильные зависимости:

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();
  }, []); // <-- Fix the mistake here!
  return <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} />
    </>
  );
}

Добавьте их согласно рекомендациям линтера:

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

Эффекты «реагируют» на реактивные значения. Поскольку roomId — реактивное значение (оно может измениться при повторном рендеринге), линтер проверяет, указали ли вы его в качестве зависимости. Если roomId получит другое значение, React повторно синхронизирует ваш эффект. Так чат будет подключен к выбранной комнате и «отреагирует» на выпадающий список:

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');
  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} />
    </>
  );
}

Чтобы удалить зависимость, докажите, что она не является зависимостью

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

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

function ChatRoom({ roomId }) { // This is a reactive value
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // This Effect reads that reactive value
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ So you must specify that reactive value as a dependency of your Effect
  // ...
}

Реактивные значения включают props, а также все переменные и функции, объявленные непосредственно внутри компонента. Поскольку roomId — реактивное значение, вы не можете удалить его из списка зависимостей. Линтер этого не позволит:

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

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

И линтер будет прав! Поскольку roomId может меняться со временем, это приведёт к ошибке в коде.

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

const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Not a reactive value anymore

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

Теперь, когда roomId не является реактивным значением (и не может измениться при повторном рендеринге), его не нужно указывать в списке зависимостей:

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

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

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

Поэтому теперь можно указать пустой список зависимостей ([]). Ваш эффект действительно больше не зависит от реактивных значений, поэтому ему действительно больше не нужно повторно запускаться при изменении props или state компонента.

Чтобы изменить зависимости, измените код

Возможно, вы заметили закономерность в работе:

  1. Сначала вы изменяете код эффекта или объявление реактивных значений.
  2. Затем следуете рекомендациям линтера и корректируете список зависимостей, чтобы он соответствовал изменённому коду.
  3. Если список зависимостей вас не устраивает, вы возвращаетесь к первому шагу (и снова меняете код).

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

Это может напоминать решение уравнения. У вас может быть цель (например, удалить зависимость), и нужно «найти» код, соответствующий этой цели. Не всем нравится решать уравнения; то же можно сказать и о написании эффектов! К счастью, ниже приведён список распространённых приёмов, которые можно попробовать.

Ловушка

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

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

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

Вместо этого используйте приведённые ниже приёмы.

Подробнее

Почему так опасно отключать линтер зависимостей?

Отключение линтера приводит к очень неочевидным ошибкам, которые трудно найти и исправить. Вот один пример:

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  function onTick() {
	setCount(count + increment);
  }

  useEffect(() => {
    const id = setInterval(onTick, 1000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Every second, increment by:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}>–</button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}

Допустим, вы хотели запускать эффект «только при монтировании». Вы прочитали, что для этого нужны пустые зависимости ([]), поэтому решили проигнорировать линтер и принудительно указали [] в качестве зависимостей.

Предполагалось, что этот счётчик будет увеличиваться каждую секунду на величину, заданную двумя кнопками. Однако, поскольку вы «солгали» React, сказав, что эффект ни от чего не зависит, React навсегда продолжает использовать функцию onTick из первого рендеринга. Во время этого рендеринга count было равно 0, а increment — 1. Поэтому onTick из того рендеринга всегда вызывает setCount(0 + 1) каждую секунду, и вы всегда видите 1. Такие ошибки сложнее исправлять, когда они распределены по нескольким компонентам.

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

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

Удаление ненужных зависимостей

Каждый раз, когда вы корректируете зависимости эффекта в соответствии с кодом, взгляните на список зависимостей. Имеет ли смысл повторно запускать эффект при изменении любой из этих зависимостей? Иногда ответ — «нет»:

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

Чтобы найти подходящее решение, нужно ответить на несколько вопросов об эффекте. Давайте разберём их по порядку.

Следует ли переместить этот код в обработчик события?

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

Представьте форму. При отправке вы устанавливаете переменную состояния submitted в значение true. Нужно отправить POST-запрос и показать уведомление. Вы поместили эту логику в эффект, который «реагирует» на то, что submitted принимает значение true:

function Form() {
  const [submitted, setSubmitted] = useState(false);

  useEffect(() => {
    if (submitted) {
      // 🔴 Avoid: Event-specific logic inside an Effect
      post('/api/register');
      showNotification('Successfully registered!');
    }
  }, [submitted]);

  function handleSubmit() {
    setSubmitted(true);
  }

  // ...
}

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

function Form() {
  const [submitted, setSubmitted] = useState(false);
  const theme = useContext(ThemeContext);

  useEffect(() => {
    if (submitted) {
      // 🔴 Avoid: Event-specific logic inside an Effect
      post('/api/register');
      showNotification('Successfully registered!', theme);
    }
  }, [submitted, theme]); // ✅ All dependencies declared

  function handleSubmit() {
    setSubmitted(true);
  }  

  // ...
}

Так вы внесли ошибку. Представьте, что сначала вы отправили форму, а затем переключили тему с тёмной на светлую или наоборот. Значение theme изменится, эффект запустится снова и покажет то же уведомление!

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

function Form() {
  const theme = useContext(ThemeContext);

  function handleSubmit() {
    // ✅ Good: Event-specific logic is called from event handlers
    post('/api/register');
    showNotification('Successfully registered!', theme);
  }  

  // ...
}

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

Выполняет ли ваш эффект несколько несвязанных задач?

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

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

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  const [city, setCity] = useState(null);

  useEffect(() => {
    let ignore = false;
    fetch(`/api/cities?country=${country}`)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setCities(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [country]); // ✅ All dependencies declared

  // ...

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

Теперь предположим, что вы добавляете второй список для районов города, который должен загружать areas для выбранного в данный момент city. Вы можете начать с добавления второго вызова fetch для списка районов в тот же эффект:

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);

  useEffect(() => {
    let ignore = false;
    fetch(`/api/cities?country=${country}`)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setCities(json);
        }
      });
    // 🔴 Avoid: A single Effect synchronizes two independent processes
    if (city) {
      fetch(`/api/areas?city=${city}`)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setAreas(json);
          }
        });
    }
    return () => {
      ignore = true;
    };
  }, [country, city]); // ✅ All dependencies declared

  // ...

Однако, поскольку теперь эффект использует переменную состояния city, вам пришлось добавить city в список зависимостей. Это, в свою очередь, вызвало проблему: когда пользователь выбирает другой город, эффект запустится снова и вызовет fetchCities(country). В результате список городов будет загружаться повторно без необходимости.

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

  1. Вы хотите синхронизировать состояние cities с сетью на основе свойства country.
  2. Вы хотите синхронизировать состояние areas с сетью на основе состояния city.

Разделите логику на два эффекта, каждый из которых реагирует на свойство, с которым его нужно синхронизировать:

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  useEffect(() => {
    let ignore = false;
    fetch(`/api/cities?country=${country}`)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setCities(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [country]); // ✅ All dependencies declared

  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);
  useEffect(() => {
    if (city) {
      let ignore = false;
      fetch(`/api/areas?city=${city}`)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setAreas(json);
          }
        });
      return () => {
        ignore = true;
      };
    }
  }, [city]); // ✅ All dependencies declared

  // ...

Теперь первый эффект запускается повторно только при изменении country, а второй — при изменении city. Вы разделили их по назначению: две разные вещи синхронизируются двумя отдельными эффектами. У двух отдельных эффектов разные списки зависимостей, поэтому они не будут случайно запускать друг друга.

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

Считываете ли вы состояние, чтобы вычислить следующее состояние?

Этот эффект обновляет переменную состояния messages новым массивом каждый раз, когда приходит новое сообщение:

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages([...messages, receivedMessage]);
    });
    // ...

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

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages([...messages, receivedMessage]);
    });
    return () => connection.disconnect();
  }, [roomId, messages]); // ✅ All dependencies declared
  // ...

Но добавление messages в список зависимостей создаёт проблему.

Каждый раз при получении сообщения setMessages() вызывает повторный рендеринг компонента с новым массивом messages, содержащим полученное сообщение. Однако, поскольку теперь эффект зависит от messages, это также приведёт к повторной синхронизации эффекта. Таким образом, каждое новое сообщение будет приводить к повторному подключению чата. Пользователю это не понравится!

Чтобы исправить эту проблему, не считывайте messages внутри эффекта. Вместо этого передайте в setMessages функцию обновления:

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages(msgs => [...msgs, receivedMessage]);
    });
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...

Обратите внимание: теперь эффект вообще не считывает переменную messages. Нужно лишь передать функцию обновления, например msgs => [...msgs, receivedMessage]. React помещает функцию обновления в очередь и передаст ей аргумент msgs во время следующего рендеринга. Поэтому самому эффекту больше не нужно зависеть от messages. Благодаря этому исправлению получение сообщения в чате больше не будет приводить к повторному подключению чата.

Хотите считывать значение, не «реагируя» на его изменения?

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

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [isMuted, setIsMuted] = useState(false);

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages(msgs => [...msgs, receivedMessage]);
      if (!isMuted) {
        playSound();
      }
    });
    // ...

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

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [isMuted, setIsMuted] = useState(false);

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages(msgs => [...msgs, receivedMessage]);
      if (!isMuted) {
        playSound();
      }
    });
    return () => connection.disconnect();
  }, [roomId, isMuted]); // ✅ All dependencies declared
  // ...

Проблема в том, что каждый раз при изменении isMuted (например, когда пользователь нажимает переключатель «Без звука») эффект будет повторно синхронизироваться и подключаться к чату. Это нежелательное поведение! (В этом примере даже отключение линтера не поможет: если так сделать, isMuted «застрянет» со старым значением.)

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

import { useState, useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [isMuted, setIsMuted] = useState(false);

  const onMessage = useEffectEvent(receivedMessage => {
    setMessages(msgs => [...msgs, receivedMessage]);
    if (!isMuted) {
      playSound();
    }
  });

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      onMessage(receivedMessage);
    });
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...

События эффекта позволяют разделить эффект на реактивные части (которые должны «реагировать» на реактивные значения, такие как roomId, и их изменения) и нереактивные части (которые только считывают последние значения, например когда onMessage считывает isMuted). Теперь, когда вы считываете isMuted внутри события эффекта, его не нужно указывать в списке зависимостей эффекта. В результате чат не будет повторно подключаться при включении и отключении настройки «Без звука», что и решает исходную проблему!

Обёртывание обработчика события из props

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

function ChatRoom({ roomId, onReceiveMessage }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      onReceiveMessage(receivedMessage);
    });
    return () => connection.disconnect();
  }, [roomId, onReceiveMessage]); // ✅ All dependencies declared
  // ...

Предположим, что родительский компонент при каждом рендеринге передаёт другую функцию onReceiveMessage:

<ChatRoom
  roomId={roomId}
  onReceiveMessage={receivedMessage => {
    // ...
  }}
/>

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

function ChatRoom({ roomId, onReceiveMessage }) {
  const [messages, setMessages] = useState([]);

  const onMessage = useEffectEvent(receivedMessage => {
    onReceiveMessage(receivedMessage);
  });

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      onMessage(receivedMessage);
    });
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...

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

Разделение реактивного и нереактивного кода

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

Решение снова заключается в том, чтобы вынести нереактивный код в событие эффекта:

function Chat({ roomId, notificationCount }) {
  const onVisit = useEffectEvent(visitedRoomId => {
    logVisit(visitedRoomId, notificationCount);
  });

  useEffect(() => {
    onVisit(roomId);
  }, [roomId]); // ✅ All dependencies declared
  // ...
}

Логика должна быть реактивной относительно roomId, поэтому вы считываете roomId внутри эффекта. Однако вы не хотите, чтобы изменение notificationCount регистрировало дополнительное посещение, поэтому считываете notificationCount внутри события эффекта. Подробнее о том, как считывать последние значения props и state в эффектах с помощью событий эффекта.

Некоторое реактивное значение меняется непреднамеренно?

Иногда вы хотите, чтобы ваш Effect «реагировал» на определённое значение, но это значение меняется чаще, чем вам хотелось бы, и может не отражать никаких реальных изменений с точки зрения пользователя. Например, предположим, что вы создаёте объект options в теле компонента, а затем читаете этот объект внутри Effect:

function ChatRoom({ roomId }) {
  // ...
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    // ...

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

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

Важно указать его в качестве зависимости! Так, например, если roomId изменится, ваш Effect повторно подключится к чату с новым значением options. Однако в приведённом выше коде есть и проблема. Чтобы увидеть её, попробуйте ввести текст в поле в песочнице ниже и проследите за происходящим в консоли:

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

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

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

  // Temporarily disable the linter to demonstrate the problem
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

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

  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} />
    </>
  );
}

В песочнице выше поле ввода обновляет только переменную состояния message. С точки зрения пользователя это не должно влиять на подключение к чату. Однако каждый раз при обновлении message компонент перерисовывается. При перерисовке компонента код внутри него запускается заново.

При каждой перерисовке компонента ChatRoom создаётся новый объект options. React видит, что объект options — это другой объект, не тот, который был создан при предыдущей отрисовке (options). Поэтому React повторно синхронизирует ваш Effect (который зависит от options), и при вводе текста чат подключается заново.

Эта проблема касается только объектов и функций. В JavaScript каждый вновь созданный объект и каждая вновь созданная функция считаются отличными от всех остальных. Неважно, что их содержимое может быть одинаковым!

// During the first render
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// During the next render
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// These are two different objects!
console.log(Object.is(options1, options2)); // false

Зависимости-объекты и зависимости-функции могут приводить к повторной синхронизации Effect чаще, чем это необходимо.

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

Переместите статические объекты и функции за пределы компонента

Если объект не зависит ни от свойств, ни от состояния, его можно переместить за пределы компонента:

const options = {
  serverUrl: 'https://localhost:1234',
  roomId: 'music'
};

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

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

Так вы докажете линтеру, что это значение не является реактивным. Оно не может измениться в результате перерисовки, поэтому его не нужно указывать в качестве зависимости. Теперь перерисовка ChatRoom не приведёт к повторной синхронизации Effect.

То же самое работает и для функций:

function createOptions() {
  return {
    serverUrl: 'https://localhost:1234',
    roomId: 'music'
  };
}

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

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ All dependencies declared
  // ...

Поскольку createOptions объявлена за пределами компонента, она не является реактивным значением. Поэтому её не нужно указывать в зависимостях Effect, и она никогда не вызовет повторную синхронизацию Effect.

Переместите динамические объекты и функции внутрь Effect

Если объект зависит от какого-либо реактивного значения, которое может измениться в результате перерисовки, например свойства roomId, его нельзя переместить за пределы компонента. Однако его создание можно переместить внутрь кода Effect:

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]); // ✅ All dependencies declared
  // ...

Теперь, когда options объявлено внутри Effect, оно больше не является его зависимостью. Единственное реактивное значение, используемое Effect, — это roomId. Поскольку roomId — не объект и не функция, можно быть уверенным, что оно не будет непреднамеренно отличаться. В JavaScript числа и строки сравниваются по содержимому:

// During the first render
const roomId1 = 'music';

// During the next render
const roomId2 = 'music';

// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true

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

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>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} />
    </>
  );
}

Однако при изменении раскрывающегося списка roomId чат подключается заново, как и ожидалось.

То же самое работает и для функций:

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]); // ✅ All dependencies declared
  // ...

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

Читайте примитивные значения из объектов

Иногда объект может передаваться через свойства:

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

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

Здесь есть риск, что родительский компонент будет создавать объект во время отрисовки:

<ChatRoom
  roomId={roomId}
  options={{
    serverUrl: serverUrl,
    roomId: roomId
  }}
/>

Из-за этого Effect будет подключаться заново при каждой перерисовке родительского компонента. Чтобы исправить это, считайте данные из объекта вне Effect и не используйте объекты и функции в качестве зависимостей:

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

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

Логика становится немного однообразной: вы считываете значения из объекта вне Effect, а затем создаёте внутри Effect объект с теми же значениями. Но так гораздо нагляднее видно, от какой информации на самом деле зависит Effect. Если родительский компонент непреднамеренно создаст объект заново, чат не подключится повторно. Однако если options.roomId или options.serverUrl действительно изменятся, чат подключится заново.

Вычисляйте примитивные значения с помощью функций

Тот же подход может подойти и для функций. Например, предположим, что родительский компонент передаёт функцию:

<ChatRoom
  roomId={roomId}
  getOptions={() => {
    return {
      serverUrl: serverUrl,
      roomId: roomId
    };
  }}
/>

Чтобы не делать её зависимостью и не вызывать повторное подключение при перерисовках, вызовите её вне Effect. Так вы получите значения roomId и serverUrl, которые не являются объектами и которые можно читать внутри Effect:

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

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

Это работает только с чистыми функциями, поскольку их безопасно вызывать во время отрисовки. Если ваша функция — обработчик события, но вы не хотите, чтобы её изменения приводили к повторной синхронизации Effect, вместо этого оберните её в Effect Event.

Краткое содержание

  • Зависимости всегда должны соответствовать коду.
  • Если вас не устраивают зависимости, измените код.
  • Отключение линтера приводит к очень запутанным ошибкам, поэтому всегда старайтесь этого избегать.
  • Чтобы удалить зависимость, нужно «доказать» линтеру, что она не нужна.
  • Если какой-то код должен выполняться в ответ на определённое взаимодействие, переместите его в обработчик события.
  • Если разные части Effect должны выполняться повторно по разным причинам, разделите их на несколько Effect.
  • Если нужно обновить состояние на основе его предыдущего значения, передайте функцию обновления.
  • Если нужно прочитать последнее значение, не «реагируя» на него, извлеките Effect Event из Effect.
  • В JavaScript объекты и функции считаются разными, если они были созданы в разное время.
  • Старайтесь не использовать объекты и функции в качестве зависимостей. Переместите их за пределы компонента или внутрь Effect.

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

Задача 1 из 4:
Исправьте сбрасывающийся интервал

Этот Effect создаёт интервал, который срабатывает каждую секунду. Вы заметили странное поведение: кажется, что интервал уничтожается и создаётся заново при каждом срабатывании. Исправьте код так, чтобы интервал не создавался заново постоянно.

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    console.log('✅ Creating an interval');
    const id = setInterval(() => {
      console.log('⏰ Interval tick');
      setCount(count + 1);
    }, 1000);
    return () => {
      console.log('❌ Clearing an interval');
      clearInterval(id);
    };
  }, [count]);

  return <h1>Counter: {count}</h1>
}

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/learn/removing-effect-dependencies

Spec-Zone.ru

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