HOW TO React

Возможно, вам не нужен эффект

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

Вы узнаете

  • Почему и как удалять ненужные эффекты из ваших компонентов
  • Как кэшировать ресурсоемкие вычисления без использования эффектов
  • Как сбрасывать и настраивать состояние компонента без использования эффектов
  • Как совместно использовать логику между обработчиками событий
  • Какую логику следует перенести в обработчики событий
  • Как уведомлять родительские компоненты об изменениях

Как удалить ненужные эффекты

Существует два распространенных случая, в которых вам не нужны эффекты:

  • Вам не нужны эффекты для преобразования данных для рендеринга. Например, предположим, вы хотите отфильтровать список перед его отображением. У вас может возникнуть соблазн написать эффект, который обновляет переменную состояния при изменении списка. Однако это неэффективно. Когда вы обновляете состояние, React сначала вызовет функции вашего компонента, чтобы вычислить, что должно отображаться на экране. Затем React «зафиксирует» эти изменения в DOM, обновив экран. Затем React запустит ваши эффекты. Если ваш эффект также немедленно обновляет состояние, это запускает весь процесс с нуля! Чтобы избежать ненужных циклов рендеринга, преобразуйте все данные на верхнем уровне ваших компонентов. Этот код будет автоматически запускаться заново при каждом изменении ваших пропсов или состояния.
  • Вам не нужны эффекты для обработки пользовательских событий. Например, предположим, вы хотите отправить POST-запрос /api/buy и показать уведомление, когда пользователь купит товар. В обработчике события нажатия кнопки «Купить» вы точно знаете, что произошло. К моменту запуска эффекта вы не знаете, что сделал пользователь (например, какая кнопка была нажата). Вот почему вы обычно обрабатываете пользовательские события в соответствующих обработчиках событий.

Вам нужны эффекты для [синхронизации](/learn/synchronizing-with-effects# what-are-effects-and-how-are-they-different-from-events) с внешними системами. Например, вы можете написать эффект, который синхронизирует виджет jQuery с состоянием React. Вы также можете извлекать данные с помощью эффектов: например, вы можете синхронизировать результаты поиска с текущим поисковым запросом. Имейте в виду, что современные фреймворки предоставляют более эффективные встроенные механизмы извлечения данных, чем написание эффектов непосредственно в ваших компонентах.

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

Обновление состояния на основе props или state

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

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // 🔴 Избегайте: избыточного состояния и ненужного эффекта
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}

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

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ✅ Хорошо: вычисляется во время рендеринга
  const fullName = firstName + ' ' + lastName;
  
// ...
}

Если что-то можно вычислить из существующих пропсов или состояния, не помещайте это в состояние. Вместо этого вычисляйте это во время рендеринга. Это делает ваш код быстрее (вы избегаете лишних «каскадных» обновлений), проще (вы удаляете часть кода) и менее подверженным ошибкам (вы избегаете ошибок, вызванных рассинхронизацией различных переменных состояния друг с другом). Если этот подход кажется вам новым, в «Мышление в React» объясняется, что должно входить в state.

Кэширование ресурсоемких вычислений

Этот компонент вычисляет visibleTodos, принимая todos, полученные через пропсы, и фильтруя их в соответствии с пропсом filter. У вас может возникнуть соблазн сохранить результат в state и обновлять его с помощью Effect:

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  
// 🔴 Избегайте: избыточного состояния и ненужного эффекта
  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  // ...
}

Как и в предыдущем примере, это и ненужно, и неэффективно. Сначала удалите состояние и эффект:

function TodoList({ todos, filter }) {
  
const [newTodo, setNewTodo] = useState('');
  // ✅ Это нормально, если getFilteredTodos() не работает медленно.
  const visibleTodos = getFilteredTodos(todos, filter);
  // ...
}

Обычно этот код работает нормально! Но, возможно, getFilteredTodos() работает медленно или у вас много todos. В таком случае вы не хотите пересчитывать getFilteredTodos(), если изменилась какая-то несвязанная переменная состояния, такая как newTodo.

Вы можете кэшировать (или «мемоизировать») ресурсоемкий вычисление, обернув его в хук useMemo:

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

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  const visibleTodos = useMemo(() => {
    // ✅ Не запускается повторно, пока не изменятся todos или filter
  return getFilteredTodos(todos, filter);
  }, [todos, filter]);
  // ...
}

Или, написано в одной строке:

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  
// ✅ Не запускает getFilteredTodos() повторно, пока не изменится todos или filter
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
  // ...
}

Это сообщает React, что вы не хотите, чтобы внутренняя функция запускалась заново, если не изменились либо todos, либо filter. React запомнит возвращаемое значение getFilteredTodos() во время первоначального рендеринга. Во время последующих рендерингов он будет проверять, отличаются ли todos или filter. Если они такие же, как в прошлый раз, useMemo вернет последний сохраненный результат. Но если они отличаются, React снова вызовет внутреннюю функцию (и сохранит ее результат).

Функция, которую вы обертываете в useMemo, выполняется во время рендеринга, поэтому это работает только для чистых вычислений.

Как определить, является ли вычисление ресурсоемким?

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

console.time('filter array');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter array');

Выполните действие, которое вы измеряете (например, введя текст в поле ввода). Затем вы увидите в консоли такие записи, как filter array: 0.15ms. Если общее зарегистрированное время составляет значительную величину (скажем, 1ms или более), возможно, имеет смысл меморизировать этот вычисление. В качестве эксперимента вы можете обернуть вычисление в useMemo, чтобы проверить, уменьшилось ли общее зарегистрированное время для этого взаимодействия или нет:

console.time('filter array');
const visibleTodos = useMemo(() => {
  return getFilteredTodos(todos, filter); // Пропускается, если todos и filter не изменились
}, [todos, filter]);
console.timeEnd('filter array');

useMemo не ускорит первый рендеринг. Он помогает только пропустить ненужную работу при обновлениях.

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

Также обратите внимание, что измерение производительности на этапе разработки не даст вам наиболее точных результатов. (Например, когда включен Strict Mode, вы увидите, что каждый компонент прорисовывается дважды, а не один раз.) Чтобы получить наиболее точные результаты, скомпилируйте приложение для производства и протестируйте его на устройстве, подобном тем, что есть у ваших пользователей.

Сброс всего состояния при изменении проп

Этот компонент ProfilePage получает проп userId. Страница содержит поле ввода комментария, и вы используете переменную состояния comment для хранения его значения. Однажды вы замечаете проблему: при переходе от одного профиля к другому состояние comment не сбрасывается. В результате легко случайно опубликовать комментарий в профиле не того пользователя . Чтобы исправить эту проблему, вам нужно очищать переменную состояния comment при каждом изменении userId:

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');
  
// 🔴 Не рекомендуется: сброс состояния при изменении проп в эффекте
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}

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

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

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // ✅ Это и любое другое состояние ниже будет сброшено при изменении ключа автоматически
  const [comment, setComment] = useState('');
  // ...
}

Обычно React сохраняет состояние, когда один и тот же компонент рендерится в одном и том же месте. *Передавая userId в качестве key компоненту Profile, вы просите React рассматривать два компонента Profile с разными значениями userId как два разных компонента, которые не должны делиться состоянием. * Всякий раз, когда ключ (который вы установили как userId) изменяется, React будет пересоздавать DOM и сбрасывать состояние компонента Profile и всех его дочерних элементов. Теперь поле comment будет автоматически очищаться при переходе между профилями.

Обратите внимание, что в этом примере экспортируется только внешний компонент ProfilePage, который виден другим файлам в проекте. Компоненты, отображающие ProfilePage, не должны передавать ему ключ: они передают userId как обычный проп. Тот факт, что ProfilePage передает его как key внутреннему компоненту Profile, является деталью реализации.

Настройка части состояния при изменении пропа

Иногда вам может понадобиться сбросить или скорректировать часть состояния при изменении проп, но не всё.

Этот компонент List получает список items в качестве проп и сохраняет выбранный элемент в переменной состояния selection. Вы хотите сбросить selection в null всякий раз, когда проп items получает другой массив:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);
  // 🔴 Не рекомендуется: корректировать состояние при изменении проп в эффекте
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

Это тоже не идеальный вариант. Каждый раз, когда items изменяется, List и его дочерние компоненты сначала будут рендериться с устаревшим значением selection. Затем React обновит DOM и запустит эффекты. В конце концов, вызов setSelection(null) вызовет очередной рендеринг List и его дочерних компонентов, запуская весь этот процесс заново.

Начните с удаления эффекта. Вместо этого настройте состояние непосредственно во время рендеринга:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);
  
// Лучше: настраивайте состояние во время рендеринга
  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}

Сохранение информации из предыдущих рендеров таким образом может быть сложно понять, но это лучше, чем обновлять то же состояние в эффекте. В приведенном выше примере setSelection вызывается непосредственно во время рендеринга. React перерисует List немедленно после того, как он завершит работу с помощью оператора return. React еще не прорисовал дочерние элементы List и не обновил DOM, поэтому это позволяет дочерним элементам List пропустить рендеринг устаревшего значения selection.

Когда вы обновляете компонент во время рендеринга, React отбрасывает возвращенный JSX и немедленно повторяет попытку рендеринга. Чтобы избежать очень медленных каскадных повторных попыток, React позволяет обновлять только состояние того же компонента во время рендеринга. Если вы обновите состояние другого компонента во время рендеринга, вы увидите ошибку. Условие типа items !== prevItems необходимо для предотвращения циклов. Вы можете корректировать состояние таким образом, но любые другие побочные эффекты (такие как изменение DOM или установка таймаутов) должны оставаться в обработчиках событий или эффектах, чтобы сохранить чистоту компонентов.

Хотя этот паттерн более эффективен, чем Effect, большинству компонентов он тоже не нужен. Независимо от того, как вы это делаете, корректировка состояния на основе props или другого состояния затрудняет понимание и отладку потока данных. Всегда проверяйте, можно ли сбросить все состояние с помощью ключа или вычислить все во время рендеринга. Например, вместо того, чтобы сохранять (и сбрасывать) выбранный элемент, вы можете сохранить ID выбранного элемента:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  // ✅ Лучший вариант: вычислять все во время рендеринга
  const selection = items.find(item => item.id === selectedId) ?? null;
  // ...
}

Теперь нет необходимости «корректировать» состояние вообще. Если элемент с выбранным ID находится в списке, он остается выбранным. Если его нет, то selection, вычисленное во время рендеринга, будет null, так как не найден ни один подходящий элемент. Это поведение отличается, но, возможно, лучше, поскольку большинство изменений в items сохраняют выбор.

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

Допустим, у вас есть страница продукта с двумя кнопками («Купить» и «Оформить заказ»), которые обе позволяют купить этот продукт. Вы хотите показывать уведомление всякий раз, когда пользователь добавляет товар в корзину. Вызов showNotification() в обработчиках кликов обеих кнопок кажется повторяющимся, поэтому у вас может возникнуть соблазн поместить эту логику в Effect:

function ProductPage({ product, addToCart }) {
  // 🔴 Избегайте: логика, специфичная для события, внутри Effect
  useEffect(() => {
    if (product.isInCart) {
      showNotification(`Added ${product.name} to the shopping cart!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }
  function handleCheckoutClick() {
    addToCart(product);
    navigateTo('/checkout');
  }
  // ...
}

Этот эффект не нужен. Кроме того, он, скорее всего, приведет к ошибкам. Например, предположим, что ваше приложение «запоминает» корзину между перезагрузками страницы. Если вы добавите товар в корзину один раз и обновите страницу, уведомление появится снова. Оно будет появляться каждый раз, когда вы обновляете страницу этого товара. Это происходит потому, что product.isInCart уже будет равен true при загрузке страницы, поэтому приведенный выше эффект вызовет showNotification().

Если вы не уверены, должен ли какой-то код находиться в эффекте или в обработчике события, спросите себя, почему этот код должен выполняться. Используйте эффекты только для кода, который должен выполняться * потому что компонент был отображен пользователю.* В этом примере уведомление должно появляться потому, что пользователь нажал кнопку, а не потому, что была отображена страница! Удалите эффект и поместите общую логику в функцию, вызываемую из обоих обработчиков событий:

function ProductPage({ product, addToCart })
 
{
  // ✅ Хорошо: логика, специфичная для события, вызывается из обработчиков событий
  function buyProduct() {
    addToCart(product);
    showNotification(`Добавлено ${product.name} в корзину!`);
  }

  function handleBuyClick() {
    buyProduct();
  }

  function handleCheckoutClick() {
    buyProduct();
    navigateTo('/checkout');
  }
  // ...
}

Это одновременно удаляет ненужный Effect и исправляет ошибку.

Отправка POST-запроса

Этот компонент Form отправляет два вида POST-запросов. Он отправляет аналитическое событие при монтировании. Когда вы заполняете форму и нажимаете кнопку «Отправить», он отправляет POST-запрос на конечную точку /api/register:

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  
// ✅ Хорошо: эта логика должна выполняться, поскольку компонент был отображен
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);

  // 🔴 Не рекомендуется: логика, связанная с конкретным событием, внутри эффекта
  const [jsonToSubmit, setJsonToSubmit] = useState(null);
  useEffect(() => {
    
if (jsonToSubmit !== null) {
      post('/api/register', jsonToSubmit);
    }
  }, [jsonToSubmit]);

  function handleSubmit(e) {
    e.preventDefault();
    setJsonToSubmit({ firstName, lastName });
  }
  // ...
}

Давайте применим те же критерии, что и в предыдущем примере.

POST-запрос аналитики должен оставаться в эффекте. Это связано с тем, что причина отправки события аналитики заключается в том, что форма была отображена. (В процессе разработки он сработает дважды, но см. здесь, как с этим справиться.)

Однако POST-запрос /api/register не вызван тем, что форма была отображена. Вы хотите отправить запрос только в один конкретный момент времени: когда пользователь нажимает кнопку. Это должно происходить только при этом конкретном взаимодействии. Удалите второй Effect и перенесите этот POST-запрос в обработчик события:

function Form () {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // ✅ Хорошо: эта логика запускается, потому что компонент был отображен
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);
  
function handleSubmit(e) {
    e.preventDefault();
    // ✅ Хорошо: логика, связанная с событием, находится в обработчике события
    post('/api/register', { firstName, lastName });
  }
  // ...
}

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

Цепочки вычислений

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

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);

  // 🔴 Избегайте: цепочки эффектов, которые изменяют состояние исключительно для того, чтобы вызвать друг друга
  useEffect(() => {
    if (card !== null && card.gold) {
      setGoldCardCount(c => c + 1);
    }
  }, [card]);

  useEffect(() => {
    if (goldCardCount > 3) {
      setRound(r => r + 1)
      setGoldCardCount(0);
    }
  }, [goldCardCount]);

  useEffect(() => {
    if (round > 5) {
      setIsGameOver(true);
    }
  }, [round]);

  useEffect(() => {
    alert('Хорошая игра!');
  }, [isGameOver]);

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Игра уже закончилась.');
    } else {
      setCard(nextCard);
    }
  }

  // ...

В этом коде есть две проблемы.

Первая проблема заключается в том, что он очень неэффективен: компонент (и его дочерние элементы) должны перерисовываться между каждым вызовом set в цепочке. В приведенном выше примере в худшем случае (setCard → render → setGoldCardCount → render → setRound → render → setIsGameOver → render) происходит три ненужных перерисовки дерева ниже.

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

В этом случае

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

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // ✅ Вычислите все, что можно, во время рендеринга
  const isGameOver = round > 5;

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Игра уже закончилась.');
    }

    // ✅ Вычислите все следующие состояния в обработчике события
    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount < 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) {
          alert('Хорошая игра!');
        }
      }
    }
  }

  // ...

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

Помните, что внутри обработчиков событий состояние ведет себя как моментальный снимок. Например, даже после вызова setRound(round + 1) переменная round будет отражать значение на момент нажатия пользователем кнопки. Если вам нужно использовать следующее значение для вычислений, определите его вручную, например, const nextRound = round + 1.

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

Инициализация приложения

Некоторая логика должна выполняться только один раз при загрузке приложения.

У вас может возникнуть соблазн поместить ее в эффект в компоненте верхнего уровня:

function App() {
  // 🔴 Не рекомендуется: эффекты с логикой, которая должна выполняться только один раз
  useEffect (() => {
    loadDataFromLocalStorage();
    checkAuthToken();
  }, []);
  // ...
}

Однако вы быстро обнаружите, что она [запускается дважды при разработке.](/learn/synchronizing-with-effects# how-to-handle-the-effect-firing-twice-in-development) Это может вызвать проблемы — например, возможно, это приведет к недействительности токена аутентификации, поскольку функция не была предназначена для двукратного вызова. В целом, ваши компоненты должны быть устойчивыми к повторному монтированию. Это включает ваш компонент верхнего уровня App.

Хотя на практике в производственной среде он может никогда не монтироваться повторно, соблюдение одних и тех же ограничений во всех компонентах упрощает

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

let didInit = false;

function App() {
  useEffect(() => {
    if (! didInit) {
      didInit = true;
      // ✅ Выполняется только один раз при каждой загрузке приложения
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
  // ...
}

Вы также можете запустить его во время инициализации модуля и перед рендерингом приложения:

if (typeof window !== 'undefined') { // Проверяем, запущено ли приложение в браузере.
   // ✅ Выполняется только один раз при каждой загрузке приложения
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  
// ...
}

Код на верхнем уровне выполняется один раз при импорте вашего компонента — даже если он в итоге не будет отображен. Чтобы избежать замедления работы или неожиданного поведения при импорте произвольных компонентов, не злоупотребляйте этим шаблоном. Сохраняйте логику инициализации для всего приложения в модулях корневых компонентов, таких как App.js, или в точке входа вашего приложения.

Уведомление родительских компонентов об изменениях состояния

Допустим предположим, вы пишете компонент Toggle с внутренним состоянием isOn, которое может быть либо true, либо false. Есть несколько способов его переключения (нажатием или перетаскиванием). Вы хотите уведомлять родительский компонент при каждом изменении внутреннего состояния Toggle, поэтому вы экспортируете событие onChange и вызываете его из эффекта:

function Toggle( { onChange }) {
  const [isOn, setIsOn] = useState(false);

  // 🔴 Не рекомендуется: обработчик onChange запускается слишком поздно
  useEffect( () => {
    onChange(isOn);
  }, [isOn, onChange])

  function handleClick() {
    setIsOn(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      setIsOn(true);
    } else {
      setIsOn (false);
    }
  }

  // ...
}

Как и раньше, это не идеальный вариант. Компонент Toggle сначала обновляет свое состояние, а React обновляет экран. Затем React запускает эффект, который вызывает функцию onChange, переданную из родительского компонента. Теперь родительский компонент обновит свое собственное состояние, запуская еще один цикл рендеринга. Было бы лучше сделать все за один проход.

Удалите Effect и вместо этого обновите состояние обоих компонентов в рамках одного обработчика события:

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  function updateToggle(nextIsOn) {
    
// ✅ Хорошо: выполнять все обновления во время события, которое их вызвало
    setIsOn(nextIsOn);
    onChange(nextIsOn);
  }

  function handleClick() {
    updateToggle(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      updateToggle(true);
    } else {
      updateToggle(false);
    }
  }

  // ...
}

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

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

// ✅ Также хорошо: компонент полностью контролируется своим родителем
function Toggle({ isOn, onChange }) {
  function handleClick() {
    onChange(!isOn);
  }
  
function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      onChange(true);
    } else {
      onChange(false);
    }
  }

  // ...
}

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

Передача данных родителю

Этот компонент Child извлекает некоторые данные, а затем передает их компоненту Parent в эффекте:

function Parent() {
  const [data, setData] = useState(null);
  // ...
  return <Child onFetched={setData} />;
}

function Child({ onFetched }) {
  const data = useSomeAPI();
  // 🔴 Не рекомендуется: передача данных родительскому компоненту в эффекте
  useEffect(() => {
    if (data) {
      onFetched(data);
    }
  }, [onFetched, data]);
  // ...
}

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

function Parent() {
  const data = useSomeAPI();
  // ...
  // ✅ Хорошо: передача данных вниз до дочернего компонента
  return <Child data={data} />;
}

function Child({ data }) {
  // ...
}

Это проще и позволяет сохранить предсказуемость потока данных: данные передаются сверху вниз от родительского компонента к дочернему.

Подписка на внешний хранилище

Иногда вашим компонентам может потребоваться подписка на некоторые данные, находящиеся за пределами состояния React. Эти данные могут поступать из сторонней библиотеки или встроенного API браузера. Поскольку эти данные могут изменяться без ведома React, вам нужно вручную подписать на них ваши компоненты. Часто это делается с помощью Effect, например:

function useOnlineStatus() {
  // Неидеально: ручная подписка на хранилище в Effect
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener('online', updateState);
    window.addEventListener('offline', updateState);
    return () => {
      window.removeEventListener('online', updateState);
      window.removeEventListener('offline', updateState);
    };
  }, []);
  return isOnline;
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

Здесь компонент подписывается на внешнее хранилище данных (в данном случае, API браузера navigator.onLine). Поскольку этот API не существует на сервере (поэтому его нельзя использовать для исходного HTML), изначально состояние устанавливается в true. Всякий раз, когда значение этого хранилища данных изменяется в браузере, компонент обновляет своё состояние.

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

function subscribe(callback) {
  window.addEventListener('online', callback);
  
window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function useOnlineStatus() {
  // ✅ Правильно: подписка на внешний хранилище с помощью встроенного хука
  return useSyncExternalStore(
    subscribe, // React не будет повторно подписываться, пока вы передаете ту же функцию
    () => navigator.onLine, // Как получить значение на клиенте
    () => true //Как получить значение на сервере
  );
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

Этот подход менее подвержен ошибкам, чем ручная синхронизация изменяемых данных с состоянием React с помощью эффекта. Обычно вы пишете пользовательский хук, подобный приведенному выше useOnlineStatus(), чтобы не повторять этот код в отдельных компонентах. Узнайте больше о подписке на внешние хранилища из компонентов React.

Загрузка данных

Многие приложения используют эффекты для запуска загрузки данных. Довольно часто пишут эффект загрузки данных примерно так:

function SearchResults({ query }) {
  
const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);

  useEffect(() => {
    // 🔴 Не рекомендуется: Загрузка без логики очистки
    fetchResults(query, page).then(json => {
      setResults(json);
    });
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

Вам не нужно переносить этот запрос в обработчик событий.

Это может показаться противоречием с предыдущими примерами, где вам нужно было помещать логику в обработчики событий! Однако учтите, что само событие ввода не является основной причиной для запроса. Поля ввода для поиска часто заполняются из URL-адреса, и пользователь может переходить назад и вперед, не затрагивая поле ввода.

Неважно, откуда берутся page и query. Пока этот компонент виден, вам нужно поддерживать results синхронизированными с данными из сети для текущих page и query. Вот почему это Effect.

Однако в приведенном выше коде есть ошибка. Представьте, что вы быстро набираете «hello». Тогда query изменится с «h» на «he», «hel», 'hell' и «hello». Это запустит отдельные запросы, но нет гарантии, в каком порядке поступят ответы. Например, ответ «hell» может поступить после ответа «hello». Поскольку setResults() будет вызван последним, вы отобразите неверные результаты поиска. Это называется «условием гонки»: два разных запроса «побежали» друг против друга и пришли в другом порядке, чем вы ожидали.

Чтобы исправить условие гонки, вам нужно добавить функцию очистки, чтобы игнорировать устаревшие ответы:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);
  useEffect(() => {
    let ignore = false;
    fetchResults(query, page).then(json => {
      if (!ignore) {
        setResults(json);
      }
    });
    return () => {
      ignore = true;
    };
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

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

Обработка условий гонки — не единственная сложность при реализации извлечения данных. Возможно, вам также стоит подумать о кэшировании ответов (чтобы пользователь мог нажать «Назад» и мгновенно увидеть предыдущий экран), о том, как извлекать данные на сервере (чтобы исходный HTML, отрендеренный сервером, содержал извлеченный контент вместо индикатора загрузки), и о том, как избежать сетевых (чтобы дочерний элемент мог загружать данные, не дожидаясь каждого родительского).

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

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

function SearchResults ({ query }) {
  const [page, setPage] = useState(1);
  const params = new URLSearchParams({ query, page });
  const results = useData(`/api/search?${params}`);
  
function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

function useData(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    let ignore = false;
    fetch(url)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setData(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [url]);
  return data;
}

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

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

Вывод

  • Если вы можете что-то вычислить во время рендеринга, вам не нужен эффект.
  • Чтобы кэшировать ресурсоемкие вычисления, добавьте useMemo вместо useEffect.
  • Чтобы сбросить состояние всего дерева компонентов, передайте ему другой key.
  • Чтобы сбросить определенную часть состояния в ответ на изменение проп, установите ее во время рендеринга.
  • Код, который запускается из-за того, что компонент был отображен, должен находиться в эффектах, остальное — в событиях.
  • Если вам нужно обновить состояние нескольких компонентов, лучше сделать это во время одного события.
  • Всякий раз, когда вы пытаетесь синхронизировать переменные состояния в разных компонентах, подумайте о переносе состояния на более высокий уровень.
  • Вы можете получать данные с помощью Effects, но вам нужно реализовать очистку, чтобы избежать условий гонки.

Синхронизация с помощью эффектов

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

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

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

On this page