Возможно, вам не нужен эффект
Эффекты — это аварийный выход из парадигмы React. Они позволяют вам «выйти за пределы» React и синхронизировать свои компоненты с какой-либо внешней системой, такой как виджет, не являющийся React, сеть или DOM браузера. Если внешней системы нет (например, если вы хотите обновить состояние компонента при изменении свойств или состояния), эффект вам не нужен. Удаление ненужных эффектов сделает ваш код более понятным, быстрым и менее подверженным ошибкам.
Вы узнаете
- Почему и как удалить ненужные эффекты из своих компонентов
- Как кешировать дорогостоящие вычисления без эффектов
- Как сбросить и настроить состояние компонента без эффектов
- Как разделить логику между обработчиками событий
- Какую логику следует перенести в обработчики событий
- Как уведомить родительские компоненты об изменениях
Как удалить ненужные эффекты
Есть два распространённых случая, когда вам не нужны эффекты:
- Вам не нужны эффекты для преобразования данных для отрисовки. Например, предположим, что вы хотите отфильтровать список перед его отображением. Вы, возможно, захотите написать эффект, который обновляет переменную состояния при изменении списка. Однако это неэффективно. Когда вы обновляете состояние, React сначала вызовет ваши функции компонента, чтобы рассчитать, что должно быть на экране. Затем React «скоммитит» эти изменения в DOM, обновляя экран. Затем React выполнит ваши эффекты. Если ваш эффект также немедленно обновляет состояние, это перезапускает весь процесс с начала! Чтобы избежать ненужных проходов рендеринга, преобразуйте все данные на верхнем уровне своих компонентов. Этот код автоматически будет повторно выполняться всякий раз, когда меняются ваши свойства или состояние.
-
Вам не нужны эффекты для обработки событий пользователя. Например, предположим, что вы хотите отправить
/api/buyPOST-запрос и показать уведомление, когда пользователь покупает продукт. В обработчике события нажатия кнопки «Купить» вы точно знаете, что произошло. К тому времени, когда выполнится эффект, вы не знаете, что сделал пользователь (например, какая кнопка была нажата). Вот почему вы обычно обрабатываете события пользователей в соответствующих обработчиках событий.
Вам нужны эффекты для синхронизации с внешними системами. Например, вы можете написать эффект, который синхронизирует виджет jQuery с состоянием React. Вы также можете получать данные с помощью эффектов: например, вы можете синхронизировать результаты поиска с текущим запросом поиска. Имейте в виду, что современные фреймворки предоставляют более эффективные встроенные механизмы получения данных, чем написание эффектов непосредственно в своих компонентах.
Чтобы помочь вам правильно понять, давайте рассмотрим несколько общих конкретных примеров!
Обновление состояния на основе свойств или состояния
Предположим, у вас есть компонент с двумя переменными состояния: firstName и lastName. Вы хотите вычислить fullName из них, конкатенировав их. Более того, вы хотите, чтобы fullName обновлялось всякий раз, когда firstName или lastName меняются. Ваша первая реакция, возможно, состоит в том, чтобы добавить переменную fullName состояния и обновить её в эффекте:
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
} Это сложнее, чем необходимо. Это также неэффективно: это делает весь проход рендеринга со значениями по умолчанию для fullName, а затем немедленно повторно рендерит с обновлённым значением. Удалите переменную состояния и эффект:
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
// ...
} Когда что-то можно вычислить из существующих свойств или состояния, не помещайте это в состояние. Вместо этого вычисляйте его во время рендеринга. Это делает ваш код быстрее (вы избегаете дополнительных «каскадных» обновлений), проще (вы удаляете некоторый код) и менее подвержен ошибкам (вы избегаете ошибок, вызванных тем, что разные переменные состояния расходятся друг с другом). Если этот подход вам нов, Thinking in React объясняет, что должно храниться в состоянии.
Кеширование дорогостоящих вычислений
Этот компонент вычисляет visibleTodos, взяв todos, который он получает через свойства, и отфильтровав их в соответствии со свойством filter. Вы, возможно, захотите сохранить результат в состоянии и обновить его из эффекта:
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// 🔴 Avoid: redundant state and unnecessary Effect
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// ...
} Как и в предыдущем примере, это и не нужно, и неэффективно. Во-первых, удалите состояние и эффект:
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ This is fine if getFilteredTodos() is not slow.
const visibleTodos = getFilteredTodos(todos, filter);
// ...
} Обычно этот код приемлем! Но, возможно, getFilteredTodos() медленный или у вас много todos. В этом случае вы не хотите перевычислять getFilteredTodos(), если изменилась какая-то несвязанная переменная состояния, например %%%CODE_BLOCK_23%%.
Вы можете кэшировать (или «мемоизировать») дорогостоящее вычисление, обернув его в useMemo хук:
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ Does not re-run unless todos or filter change
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
} Или, записанное в одну строку:
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ Does not re-run getFilteredTodos() unless todos or filter change
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); // Skipped if todos and filter haven't changed
}, [todos, filter]);
console.timeEnd('filter array');
useMemo не ускорит первый рендеринг. Это лишь помогает пропустить ненужную работу при обновлениях.
Имейте в виду, что ваша машина, вероятно, быстрее, чем у ваших пользователей, поэтому неплохо протестировать производительность с искусственным замедлением. Например, Chrome предлагает опцию «Уменьшение производительности ЦП» для этого.
Обратите также внимание, что измерение производительности в режиме разработки не даст вам самых точных результатов. (Например, при включённом режиме Strict Mode каждый компонент будет рендериться дважды вместо одного.) Для получения наиболее точных оценок времени постройте своё приложение для производства и протестируйте его на устройстве, подобном тому, которое используют ваши пользователи.
Сброс всего состояния при изменении свойства
Этот ProfilePage компонент получает свойство userId. На странице есть поле для комментариев, и вы используете переменную состояния comment для хранения его значения. Однажды вы заметили проблему: при переходе от одного профиля к другому состояние comment не сбрасывается. В результате легко случайно опубликовать комментарий в профиле неправильного пользователя. Чтобы исправить проблему, вы хотите очистить переменную состояния comment всякий раз, когда меняется userId:
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 🔴 Avoid: Resetting state on prop change in an Effect
useEffect(() => {
setComment('');
}, [userId]);
// ...
} Это неэффективно, потому что ProfilePage и его потомки сначала отобразятся со старым значением, а затем снова отобразятся. Это также сложно, потому что вам нужно сделать это в каждом компоненте, который имеет состояние внутри ProfilePage. Например, если пользовательский интерфейс комментариев вложен, вам нужно будет очистить также вложенное состояние комментариев.
Вместо этого вы можете сказать React, что каждый профиль пользователя концептуально является разным профилем, присвоив ему явный ключ. Разделите свой компонент на два и передайте атрибут key от внешнего компонента во внутренний:
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// ✅ This and any other state below will reset on key change automatically
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);
// 🔴 Avoid: Adjusting state on prop change in an Effect
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);
// Better: Adjust the state while rendering
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 или установка таймаутов) должны оставаться в обработчиках событий или эффектах, чтобы сохранять компоненты чистыми.
Хотя этот подход более эффективен, чем эффект, большинству компонентов он тоже не нужен. В любом случае, настройка состояния на основе свойств или другого состояния затрудняет понимание и отладку потока данных. Всегда проверяйте, можете ли вы сбросить всё состояние с помощью ключа или вычислить всё во время рендеринга вместо этого. Например, вместо хранения (и сброса) выбранного элемента, вы можете хранить выбранный идентификатор элемента:
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ Best: Calculate everything during rendering
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
} Теперь нет необходимости «настраивать» состояние вообще. Если элемент с выбранным идентификатором есть в списке, он остаётся выбранным. Если его нет, значение selection, вычисленное во время рендеринга, будет null, поскольку не было найдено соответствующего элемента. Это поведение отличается, но, вероятно, лучше, поскольку большинство изменений в items сохраняют выбор.
Разделяемая логика между обработчиками событий
Допустим, у вас есть страница продукта с двумя кнопками (Купить и Оформить заказ), которые обе позволяют купить этот продукт. Вы хотите отобразить уведомление всякий раз, когда пользователь добавляет продукт в корзину. Вызов showNotification() в обработчиках нажатия обеих кнопок кажется повторяющимся, поэтому вы можете быть искушены поместить эту логику в эффект:
function ProductPage({ product, addToCart }) {
// 🔴 Avoid: Event-specific logic inside an 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 }) {
// ✅ Good: Event-specific logic is called from event handlers
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to the shopping cart!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
} Это устраняет нежелательный эффект и исправляет ошибку.
Отправка запроса POST
Этот Form компонент отправляет два типа запросов POST. Он отправляет событие аналитики при монтировании. При заполнении формы и нажатии кнопки Отправить он отправит запрос POST на /api/register конечную точку:
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ Good: This logic should run because the component was displayed
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
// 🔴 Avoid: Event-specific logic inside an Effect
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 не вызван отображением формы. Вы хотите отправить запрос только в определённый момент времени: когда пользователь нажимает кнопку. Это должно происходить только в ходе этого конкретного взаимодействия. Удалите второй эффект и переместите этот запрос POST в обработчик событий:
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ Good: This logic runs because the component was displayed
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
function handleSubmit(e) {
e.preventDefault();
// ✅ Good: Event-specific logic is in the event handler
post('/api/register', { firstName, lastName });
}
// ...
} При выборе, помещать ли некоторую логику в обработчик событий или эффект, основным вопросом, на который нужно ответить, является какая логика это с точки зрения пользователя. Если эта логика вызвана конкретным взаимодействием, сохраните её в обработчике событий. Если она вызвана отображением компонента пользователю на экране, сохраните её в эффекте.
Цепочки вычислений
Иногда вы можете быть искушены связать эффекты, каждый из которых настраивает часть состояния на основе другого состояния:
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
// 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
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('Good game!');
}, [isGameOver]);
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
} else {
setCard(nextCard);
}
}
// ... В этом коде есть две проблемы.
Первая проблема заключается в том, что он очень неэффективен: компонент (и его дочерние элементы) должны повторно отрисовываться между каждым вызовом set в цепочке. В приведённом выше примере в худшем случае (setCard → рендеринг → setGoldCardCount → рендеринг → setRound → рендеринг → setIsGameOver → рендеринг) происходит три ненужных повторных рендеринга дерева ниже.
Вторая проблема заключается в том, что даже если бы это не было медленным, по мере развития кода вы столкнётесь со случаями, когда написанная вами «цепочка» не соответствует новым требованиям. Представьте, что вы добавляете возможность просматривать историю ходов игры. Вы бы сделали это, обновив каждую переменную состояния до значения из прошлого. Однако установка состояния card в значение из прошлого снова запустит цепочку эффектов и изменит отображаемые данные. Такой код часто жёсткий и хрупкий.
В этом случае лучше вычислять то, что можно во время рендеринга, и настраивать состояние в обработчике событий:
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
// ✅ Calculate what you can during rendering
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
}
// ✅ Calculate all the next state in the event handler
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}
// ... Это намного эффективнее. Кроме того, если вы реализуете способ просмотра истории игры, теперь вы сможете установить каждую переменную состояния в ход из прошлого, не запускаю цепочку эффектов, которая настраивает все остальные значения. Если вам нужно повторно использовать логику между несколькими обработчиками событий, вы можете извлечь функцию и вызвать её из этих обработчиков.
Помните, что внутри обработчиков событий состояние ведёт себя как моментальный снимок. Например, даже после вызова setRound(round + 1), переменная round будет отражать значение в момент нажатия пользователем кнопки. Если вам нужно использовать следующее значение для вычислений, определите его вручную, как const nextRound = round + 1.
В некоторых случаях вы не можете вычислить следующее состояние непосредственно в обработчике событий. Например, представьте форму с несколькими раскрывающимися списками, где варианты следующего раскрывающегося списка зависят от выбранного значения предыдущего раскрывающегося списка. Тогда цепочка эффектов уместна, поскольку вы синхронизируетесь с сетью.
Инициализация приложения
Некоторая логика должна выполняться только один раз при загрузке приложения.
Вы можете быть искушены поместить её в эффект в компонент верхнего уровня:
function App() {
// 🔴 Avoid: Effects with logic that should only ever run once
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
// ...
} Однако вы быстро обнаружите, что она выполняется дважды в разработке. Это может вызвать проблемы — например, возможно, она делает недействительным токен аутентификации, потому что функция не была разработана для вызова дважды. В целом, ваши компоненты должны быть устойчивы к повторному монтированию. Это относится и к вашему компоненту верхнего уровня App.
Хотя в производстве он, возможно, никогда не будет повторно монтироваться, соблюдение одних и тех же ограничений во всех компонентах упрощает перемещение и повторное использование кода. Если какая-то логика должна выполняться один раз на загрузку приложения, а не один раз на монтирование компонента, добавьте переменную верхнего уровня, чтобы отслеживать, уже ли она выполнилась:
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// ✅ Only runs once per app load
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
} Вы также можете запустить её во время инициализации модуля и до рендеринга приложения:
if (typeof window !== 'undefined') { // Check if we're running in the browser.
// ✅ Only runs once per app load
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
} Код на верхнем уровне выполняется один раз при импорте компонента — даже если он не отображается. Чтобы избежать замедления или неожиданного поведения при импорте произвольных компонентов, не злоупотребляйте этим шаблоном. Сохраняйте логику инициализации приложения в модулях корневого компонента, таких как App.js, или в точке входа вашего приложения.
Уведомление родительских компонентов об изменениях состояния
Предположим, вы пишете компонент Toggle с внутренним состоянием isOn, которое может быть либо true, либо false. Есть несколько способов переключать его (нажимая или перетаскивая). Вы хотите уведомлять родительский компонент всякий раз, когда изменяется внутреннее состояние Toggle, поэтому вы экспонируете событие onChange и вызываете его из эффекта:
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
// 🔴 Avoid: The onChange handler runs too late
useEffect(() => {
onChange(isOn);
}, [isOn, onChange])
function handleClick() {
setIsOn(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
setIsOn(true);
} else {
setIsOn(false);
}
}
// ...
} Как и раньше, это не оптимально. Компонент Toggle сначала обновляет своё состояние, а React обновляет экран. Затем React выполняет эффект, который вызывает функцию onChange, переданную родительским компонентом. Теперь родительский компонент обновит своё собственное состояние, начав новый проход рендеринга. Было бы лучше сделать всё в одном проходе.
Удалите эффект и вместо этого обновите состояние обоих компонентов в одном обработчике событий:
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
function updateToggle(nextIsOn) {
// ✅ Good: Perform all updates during the event that caused them
setIsOn(nextIsOn);
onChange(nextIsOn);
}
function handleClick() {
updateToggle(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
updateToggle(true);
} else {
updateToggle(false);
}
}
// ...
} С этим подходом оба компонента, Toggle и его родительский компонент, обновляют своё состояние во время события. React группирует обновления из разных компонентов вместе, поэтому будет только один проход рендеринга.
Вы также можете вообще убрать состояние и вместо этого получить isOn от родительского компонента:
// ✅ Also good: the component is fully controlled by its parent
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();
// 🔴 Avoid: Passing data to the parent in an Effect
useEffect(() => {
if (data) {
onFetched(data);
}
}, [onFetched, data]);
// ...
} В React данные передаются от родительских компонентов к дочерним. Когда вы видите что-то неправильное на экране, вы можете проследить, откуда исходит информация, поднимаясь по цепочке компонентов, пока не найдёте компонент, который передаёт неправильное свойство или имеет неправильное состояние. Когда дочерние компоненты обновляют состояние своих родительских компонентов в эффектах, поток данных становится очень сложным для отслеживания. Поскольку и дочерний, и родительский компоненты нуждаются в одних и тех же данных, пусть родительский компонент извлечёт эти данные и передаст их дочернему компоненту вместо этого:
function Parent() {
const data = useSomeAPI();
// ...
// ✅ Good: Passing data down to the child
return <Child data={data} />;
}
function Child({ data }) {
// ...
} Это проще и сохраняет предсказуемость потока данных: данные передаются сверху вниз от родителя к дочернему компоненту.
Подписка на внешний хранилище
Иногда вашим компонентам может потребоваться подписаться на какие-либо данные вне состояния React. Эти данные могут поступать из сторонней библиотеки или встроенного API браузера. Поскольку эти данные могут изменяться без ведома React, вам необходимо вручную подписывать свои компоненты на них. Это часто делается с помощью эффекта, например:
function useOnlineStatus() {
// Not ideal: Manual store subscription in an 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() {
// ✅ Good: Subscribing to an external store with a built-in Hook
return useSyncExternalStore(
subscribe, // React won't resubscribe for as long as you pass the same function
() => navigator.onLine, // How to get the value on the client
() => true // How to get the value on the server
);
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
} Этот подход менее подвержен ошибкам, чем ручная синхронизация изменяемых данных с состоянием React с помощью эффекта. Обычно вы напишете пользовательский хук, такой как useOnlineStatus() выше, чтобы не повторять этот код в отдельных компонентах. Подробнее о подписке на внешние магазины из компонентов React.
Загрузка данных
Многие приложения используют эффекты для запуска загрузки данных. Довольно распространённо писать эффект загрузки данных так:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
// 🔴 Avoid: Fetching without cleanup logic
fetchResults(query, page).then(json => {
setResults(json);
});
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
} Вам не нужно перемещать этот запрос в обработчик событий.
Это может показаться противоречием с предыдущими примерами, где вам нужно было поместить логику в обработчики событий! Однако учтите, что это не событие ввода текста является основной причиной загрузки. Поисковые строки часто заполняются из URL, и пользователь может нажимать Назад и Вперёд, не касаясь поля ввода.
Неважно, откуда page и query. Пока этот компонент виден, вы хотите сохранить results синхронизированным с данными из сети для текущего page и query. Именно поэтому это эффект.
Однако в приведенном выше коде есть ошибка. Представьте, что вы вводите "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. Решение этих проблем нетривиально, поэтому современные фреймворки предоставляют более эффективные встроенные механизмы загрузки данных, чем загрузка данных в эффектах.
Если вы не используете фреймворк (и не хотите создавать свой собственный), но хотите сделать загрузку данных из эффектов более удобной, рассмотрите возможность извлечения логики загрузки в пользовательский хук, как в этом примере:
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. - Для сброса определенной части состояния в ответ на изменение свойства установите ее во время рендеринга.
- Код, выполняющийся, потому что компонент был отображен, должен находиться в эффектах, все остальное — в событиях.
- Если вам нужно обновить состояние нескольких компонентов, лучше сделать это во время одного события.
- Всякий раз, когда вы пытаетесь синхронизировать переменные состояния в разных компонентах, подумайте о подъёме состояния вверх.
- Вы можете загружать данные с помощью эффектов, но вам нужно реализовать очистку, чтобы избежать гонок.
Попробуйте некоторые задачи
Задача 1 из 4:
Преобразование данных без эффектов
TodoList ниже отображает список задач. При нажатии на флажок «Показать только активные задачи» завершенные задачи не отображаются в списке. Независимо от того, какие задачи видны, в подвале отображается количество задач, которые еще не выполнены.
Упростите этот компонент, удалив все ненужные состояния и эффекты.
import { useState, useEffect } from 'react'; import { initialTodos, createTodo } from './todos.js'; export default function TodoList() { const [todos, setTodos] = useState(initialTodos); const [showActive, setShowActive] = useState(false); const [activeTodos, setActiveTodos] = useState([]); const [visibleTodos, setVisibleTodos] = useState([]); const [footer, setFooter] = useState(null); useEffect(() => { setActiveTodos(todos.filter(todo => !todo.completed)); }, [todos]); useEffect(() => { setVisibleTodos(showActive ? activeTodos : todos); }, [showActive, todos, activeTodos]); useEffect(() => { setFooter( <footer> {activeTodos.length} todos left </footer> ); }, [activeTodos]); return ( <> <label> <input type="checkbox" checked={showActive} onChange={e => setShowActive(e.target.checked)} /> Show only active todos </label> <NewTodo onAdd={newTodo => setTodos([...todos, newTodo])} /> <ul> {visibleTodos.map(todo => ( <li key={todo.id}> {todo.completed ? <s>{todo.text}</s> : todo.text} </li> ))} </ul> {footer} </> ); } function NewTodo({ onAdd }) { const [text, setText] = useState(''); function handleAddClick() { setText(''); onAdd(createTodo(text)); } return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={handleAddClick}> Add </button> </> ); }
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/learn/you-might-not-need-an-effect