useEffect
useEffect — это React Hook, который позволяет синхронизировать компонент с внешней системой.
useEffect(setup, dependencies?)Справочник
useEffect(setup, dependencies?)
Вызовите useEffect на верхнем уровне вашего компонента, чтобы объявить Effect:
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}Параметры
-
setup: Функция с логикой вашего Effect. Функция setup также может при желании возвращать функцию cleanup. Когда ваш компонент фиксируется (commits), React выполнит функцию setup. После каждого commit с изменившимися зависимостями React сначала выполнит функцию cleanup (если вы её указали) со старыми значениями, а затем выполнит функцию setup с новыми значениями. После того как ваш компонент будет удалён из DOM, React выполнит функцию cleanup. -
optional
dependencies: Список всех реактивных значений, на которые ссылается кодsetup. К реактивным значениям относятся props, state, а также все переменные и функции, объявленные непосредственно внутри тела вашего компонента. Если ваш линтер настроен для React, он проверит, что каждое реактивное значение корректно указано как зависимость. Список зависимостей должен содержать постоянное число элементов и быть записан прямо в коде, например[dep1, dep2, dep3]. React будет сравнивать каждую зависимость с её предыдущим значением, используя сравнениеObject.is. Если вы опустите этот аргумент, ваш Effect будет запускаться заново после каждого commit компонента. См. разницу между передачей массива зависимостей, пустого массива и полным отсутствием зависимостей.
Возвращаемое значение
useEffect возвращает undefined.
Предостережения
-
useEffect— это Hook, поэтому вы можете вызывать его только на верхнем уровне вашего компонента или ваших собственных Hooks. Нельзя вызывать его внутри циклов или условий. Если это нужно, извлеките новый компонент и перенесите state в него. -
Если вы не пытаетесь синхронизироваться с какой-либо внешней системой, Effect, скорее всего, вам не нужен.
-
Когда включён Strict Mode, React выполнит один дополнительный цикл setup+cleanup только для разработки перед первым реальным setup. Это стресс-тест, который проверяет, что логика cleanup "зеркалит" логику setup и останавливает либо отменяет всё, что делает setup. Если это вызывает проблему, реализуйте функцию cleanup.
-
Если некоторые из ваших зависимостей — это объекты или функции, определённые внутри компонента, есть риск, что они будут заставлять Effect запускаться чаще, чем нужно. Чтобы исправить это, удалите ненужные зависимости- object и function. Также можно вынести обновления state и нереактивную логику за пределы Effect.
-
Если ваш Effect был вызван не взаимодействием (например, кликом), React обычно позволяет браузеру сначала отрисовать обновлённый экран, а уже потом запускать ваш Effect. Если ваш Effect делает что-то визуальное (например, позиционирует tooltip), и задержка заметна (например, он мерцает), замените
useEffectнаuseLayoutEffect. -
Если ваш Effect вызван взаимодействием (например, кликом), React может запустить ваш Effect до того, как браузер отрисует обновлённый экран. Это гарантирует, что результат Effect может быть замечен системой событий. Обычно это работает как ожидается. Однако если вам нужно отложить работу до после отрисовки, например
alert(), можно использоватьsetTimeout. -
Даже если ваш Effect был вызван взаимодействием (например, кликом), React может позволить браузеру перерисовать экран до обработки обновлений state внутри вашего Effect. Обычно это работает как ожидается. Однако если вам нужно заблокировать перерисовку экрана браузером, необходимо заменить
useEffectнаuseLayoutEffect. -
Effects работают только на клиенте. Они не выполняются во время server rendering.
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}Использование
Подключение к внешней системе
Некоторым компонентам нужно оставаться подключёнными к сети, какому-то browser API или сторонней библиотеке, пока они отображаются на странице. Эти системы не управляются React, поэтому их называют external.
Чтобы подключить ваш компонент к внешней системе, вызовите useEffect на верхнем уровне вашего компонента:
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]);
return (
<>
<label>
Server URL:{' '}
<input
value={serverUrl}
onChange={e => setServerUrl(e.target.value)}
/>
</label>
<h1>Welcome to the {roomId} room!</h1>
</>
);
}
export default function App() {
const [roomId, setRoomId] = useState('general');
const [show, setShow] = useState(false);
return (
<>
<label>
Choose the chat room:{' '}
<select
value={roomId}
onChange={e => setRoomId(e.target.value)}
>
<option value="general">general</option>
<option value="travel">travel</option>
<option value="music">music</option>
</select>
</label>
<button onClick={() => setShow(!show)}>
{show ? 'Close chat' : 'Open chat'}
</button>
{show && <hr />}
{show && <ChatRoom roomId={roomId} />}
</>
);
}Прослушивание глобального браузерного события
В этом примере внешней системой является сам browser DOM. Обычно слушатели событий задаются через JSX, но таким способом нельзя подписаться на глобальный объект window. Effect позволяет подключиться к объекту window и слушать его события. Прослушивание события pointermove позволяет отслеживать положение курсора (или пальца) и перемещать за ним красную точку.
import { useState, useEffect } from 'react';
export default function App() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
function handleMove(e) {
setPosition({ x: e.clientX, y: e.clientY });
}
window.addEventListener('pointermove', handleMove);
return () => {
window.removeEventListener('pointermove', handleMove);
};
}, []);
return (
<div style={{
position: 'absolute',
backgroundColor: 'pink',
borderRadius: '50%',
opacity: 0.6,
transform: `translate(${position.x}px, ${position.y}px)`,
pointerEvents: 'none',
left: -20,
top: -20,
width: 40,
height: 40,
}} />
);
}Запуск анимации
В этом примере внешней системой является библиотека анимации в animation.js. Она предоставляет JavaScript-класс FadeInAnimation, который принимает DOM-узел в качестве аргумента и открывает методы start() и stop() для управления анимацией. Этот компонент использует ref, чтобы получить доступ к базовому DOM-узлу. Effect читает DOM-узел из ref и автоматически запускает анимацию для этого узла, когда компонент появляется.
import { useState, useEffect, useRef } from 'react';
import { FadeInAnimation } from './animation.js';
function Welcome() {
const ref = useRef(null);
useEffect(() => {
const animation = new FadeInAnimation(ref.current);
animation.start(1000);
return () => {
animation.stop();
};
}, []);
return (
<h1
ref={ref}
style={{
opacity: 0,
color: 'white',
padding: 50,
textAlign: 'center',
fontSize: 50,
backgroundImage: 'radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%)'
}}
>
Welcome
</h1>
);
}
export default function App() {
const [show, setShow] = useState(false);
return (
<>
<button onClick={() => setShow(!show)}>
{show ? 'Remove' : 'Show'}
</button>
<hr />
{show && <Welcome />}
</>
);
}Управление модальным диалогом
В этом примере внешней системой является browser DOM. Компонент ModalDialog рендерит элемент <dialog>. Он использует Effect, чтобы синхронизировать prop isOpen с вызовами методов showModal() и close().
import { useState } from 'react';
import ModalDialog from './ModalDialog.js';
export default function App() {
const [show, setShow] = useState(false);
return (
<>
<button onClick={() => setShow(true)}>
Open dialog
</button>
<ModalDialog isOpen={show}>
Hello there!
<br />
<button onClick={() => {
setShow(false);
}}>Close</button>
</ModalDialog>
</>
);
}Отслеживание видимости элемента
В этом примере внешней системой снова является browser DOM. Компонент App отображает длинный список, затем компонент Box, а затем ещё один длинный список. Прокрутите список вниз. Обратите внимание: когда весь компонент Box полностью виден во viewport, цвет фона меняется на чёрный. Чтобы реализовать это, компонент Box использует Effect для управления IntersectionObserver. Этот browser API сообщает вам, когда DOM-элемент виден во viewport.
import Box from './Box.js';
export default function App() {
return (
<>
<LongSection />
<Box />
<LongSection />
<Box />
<LongSection />
</>
);
}
function LongSection() {
const items = [];
for (let i = 0; i < 50; i++) {
items.push(<li key={i}>Item #{i} (keep scrolling)</li>);
}
return <ul>{items}</ul>
}Обёртывание Effects в custom Hooks
Effects — это "escape hatch":, который используют, когда нужно "выйти за пределы React" и когда для вашего случая нет лучшего встроенного решения. Если вы часто ловите себя на том, что вручную пишете Effects, это обычно знак того, что стоит вынести общие поведения, на которые опираются ваши компоненты, в custom Hooks.
Например, этот custom Hook useChatRoom "прячет" логику вашего Effect за более декларативным API:
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}Затем вы можете использовать его из любого компонента вот так:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...Во всей экосистеме React есть множество отличных custom Hooks на любой случай.
Подробнее об обёртывании Effects в custom Hooks.
Пользовательский Hook useChatRoom
Этот пример полностью идентичен одному из предыдущих примеров, но логика вынесена в custom Hook.
import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
return (
<>
<label>
Server URL:{' '}
<input
value={serverUrl}
onChange={e => setServerUrl(e.target.value)}
/>
</label>
<h1>Welcome to the {roomId} room!</h1>
</>
);
}
export default function App() {
const [roomId, setRoomId] = useState('general');
const [show, setShow] = useState(false);
return (
<>
<label>
Choose the chat room:{' '}
<select
value={roomId}
onChange={e => setRoomId(e.target.value)}
>
<option value="general">general</option>
<option value="travel">travel</option>
<option value="music">music</option>
</select>
</label>
<button onClick={() => setShow(!show)}>
{show ? 'Close chat' : 'Open chat'}
</button>
{show && <hr />}
{show && <ChatRoom roomId={roomId} />}
</>
);
}Пользовательский Hook useWindowListener
Этот пример полностью идентичен одному из предыдущих примеров, но логика вынесена в custom Hook.
import { useState } from 'react';
import { useWindowListener } from './useWindowListener.js';
export default function App() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useWindowListener('pointermove', (e) => {
setPosition({ x: e.clientX, y: e.clientY });
});
return (
<div style={{
position: 'absolute',
backgroundColor: 'pink',
borderRadius: '50%',
opacity: 0.6,
transform: `translate(${position.x}px, ${position.y}px)`,
pointerEvents: 'none',
left: -20,
top: -20,
width: 40,
height: 40,
}} />
);
}Пользовательский Hook useIntersectionObserver
Этот пример полностью идентичен одному из предыдущих примеров,, но логика частично вынесена в custom Hook.
import Box from './Box.js';
export default function App() {
return (
<>
<LongSection />
<Box />
<LongSection />
<Box />
<LongSection />
</>
);
}
function LongSection() {
const items = [];
for (let i = 0; i < 50; i++) {
items.push(<li key={i}>Item #{i} (keep scrolling)</li>);
}
return <ul>{items}</ul>
}Управление не-React виджетом
Иногда вам нужно держать внешнюю систему синхронизированной с каким-то prop или state вашего компонента.
Например, если у вас есть сторонний map widget или компонент видеоплеера, написанный без React, вы можете использовать Effect, чтобы вызывать у него методы, которые заставляют его state соответствовать текущему state вашего React-компонента. Этот Effect создаёт экземпляр класса MapWidget, определённого в map-widget.js. Когда вы меняете prop zoomLevel у компонента Map, Effect вызывает на экземпляре класса метод setZoom(), чтобы поддерживать синхронизацию.
//json package.json hidden
{
"dependencies": {
"leaflet": "1.9.1",
"react": "latest",
"react-dom": "latest",
"react-scripts": "latest",
"remarkable": "2.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}В этом примере функция cleanup не нужна, потому что класс MapWidget управляет только тем DOM-узлом, который ему передали. После того как React-компонент Map удаляется из дерева, и DOM-узел, и экземпляр класса MapWidget будут автоматически собраны сборщиком мусора движка JavaScript браузера.
Получение данных с помощью Effects
Вы можете использовать Effect, чтобы загружать данные для вашего компонента. Обратите внимание: если вы используете framework, использование встроенного механизма загрузки данных этого framework будет гораздо эффективнее, чем ручное написание Effects.
Если вы хотите вручную загружать данные из Effect, ваш код может выглядеть так:
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);
// ...Обратите внимание на переменную ignore, которая инициализируется как false и устанавливается в true во время cleanup. Это гарантирует, что ваш код не столкнётся с "race conditions": сетевые ответы могут приходить в другом порядке, чем вы их отправили.
{/* TODO(@poteto) - investigate potential false positives in react compiler validation */}
//App.js
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
}
}, [person]);
return (
<>
<select value={person} onChange={e => {
setPerson(e.target.value);
}}>
<option value="Alice">Alice</option>
<option value="Bob">Bob</option>
<option value="Taylor">Taylor</option>
</select>
<hr />
<p><i>{bio ?? 'Loading...'}</i></p>
</>
);
}Вы также можете переписать это с помощью синтаксиса async / await, но вам всё равно понадобится функция cleanup:
//App.js
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
async function startFetching() {
setBio(null);
const result = await fetchBio(person);
if (!ignore) {
setBio(result);
}
}
let ignore = false;
startFetching();
return () => {
ignore = true;
}
}, [person]);
return (
<>
<select value={person} onChange={e => {
setPerson(e.target.value);
}}>
<option value="Alice">Alice</option>
<option value="Bob">Bob</option>
<option value="Taylor">Taylor</option>
</select>
<hr />
<p><i>{bio ?? 'Loading...'}</i></p>
</>
);
}Написание загрузки данных напрямую в Effects становится однообразным и усложняет добавление оптимизаций вроде кэширования и server rendering в дальнейшем. Проще использовать custom Hook — свой собственный или поддерживаемый сообществом.
Какие есть хорошие альтернативы загрузке данных в Effects?
Использование fetch внутри Effects — популярный способ загрузки данных, особенно в полностью client-side приложениях. Однако это очень ручной подход, у которого есть серьёзные недостатки:
- Effects не выполняются на сервере. Это означает, что начальный HTML, отрендеренный на сервере, будет содержать только состояние загрузки без данных. Клиентскому компьютеру придётся скачать весь JavaScript и отрендерить приложение только для того, чтобы потом обнаружить, что теперь нужно ещё и загрузить данные. Это не очень эффективно.
- Загрузка данных напрямую в Effects легко приводит к "network waterfalls". Сначала рендерится родительский компонент, он загружает какие-то данные, затем рендерит дочерние компоненты, и уже они начинают загружать свои данные. Если сеть не очень быстрая, это значительно медленнее, чем загрузка всех данных параллельно.
- Загрузка данных напрямую в Effects обычно означает, что вы не предзагружаете и не кэшируете данные. Например, если компонент размонтируется, а затем снова смонтируется, ему придётся загружать данные заново.
- Это не очень удобно. При написании
fetchвызовов так, чтобы не появлялись баги вроде race conditions, приходится писать довольно много шаблонного кода.
Этот список недостатков не специфичен для React. Он относится к загрузке данных при монтировании в любой библиотеке. Как и с routing, загрузка данных — это не то, что легко сделать хорошо, поэтому мы рекомендуем следующие подходы:
- Если вы используете framework, используйте встроенный механизм загрузки данных. Современные React framework имеют встроенные механизмы загрузки данных, которые эффективны и не страдают от описанных выше проблем.
- В противном случае рассмотрите возможность использовать или создать client-side cache. Популярные open source решения включают TanStack Query, useSWR и React Router 6.4+. Вы также можете построить своё решение: в этом случае под капотом вы всё равно будете использовать Effects, но дополнительно добавите логику для дедупликации запросов, кэширования ответов и предотвращения network waterfalls (например, за счёт предварительной загрузки данных или поднятия требований к данным на уровень роутов).
Вы можете продолжать загружать данные напрямую в Effects, если ни один из этих подходов вам не подходит.
Указание реактивных зависимостей
Обратите внимание: вы не можете "выбирать" зависимости вашего Effect. Каждое reactive value, используемое кодом Effect, должно быть указано как зависимость. Список зависимостей вашего Effect определяется окружающим кодом:
function ChatRoom({ roomId }) { // This is a reactive value
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // This is a reactive value too
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect
// ...
}Если либо serverUrl, либо roomId изменятся, ваш Effect переподключится к чату, используя новые значения.
Реактивные значения включают props и все переменные и функции, объявленные непосредственно внутри вашего компонента. Поскольку roomId и serverUrl — это реактивные значения, вы не можете убрать их из зависимостей. Если вы попытаетесь опустить их и ваш линтер корректно настроен для React, линтер пометит это как ошибку, которую нужно исправить:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
// ...
}Чтобы убрать зависимость, нужно "доказать" линтеру, что она не обязана быть зависимостью. Например, можно вынести serverUrl из компонента, чтобы показать, что он не является реактивным и не будет меняться при повторных рендерах:
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}Теперь, когда serverUrl не является реактивным значением (и не может измениться при повторном рендере), он не должен быть зависимостью. Если код вашего Effect не использует никаких реактивных значений, список зависимостей должен быть пустым ([]):
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
const roomId = 'music'; // Not a reactive value anymore
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
}Effect с пустыми зависимостями не запускается заново при изменении любых props или state вашего компонента.
Если у вас уже есть существующая кодовая база, у вас могут быть Effects, где линтер подавлен вот так:
useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);Когда зависимости не соответствуют коду, очень высок риск внести баги. Подавляя линтер, вы "врёте" React о тех значениях, от которых зависит ваш Effect. Вместо этого докажите, что они не нужны.
Передача массива зависимостей
Если вы укажете зависимости, ваш Effect запускается после первоначального commit и после commit с изменившимися зависимостями.
useEffect(() => {
// ...
}, [a, b]); // Runs again if a or b are differentВ примере ниже serverUrl и roomId — реактивные значения, поэтому оба должны быть указаны как зависимости. В результате выбор другой комнаты в выпадающем списке или изменение значения в поле server URL вызывает переподключение чата. Однако поскольку message не используется в Effect (и, следовательно, не является зависимостью), редактирование сообщения не вызывает переподключения к чату.
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
return (
<>
<label>
Server URL:{' '}
<input
value={serverUrl}
onChange={e => setServerUrl(e.target.value)}
/>
</label>
<h1>Welcome to the {roomId} room!</h1>
<label>
Your message:{' '}
<input value={message} onChange={e => setMessage(e.target.value)} />
</label>
</>
);
}
export default function App() {
const [show, setShow] = useState(false);
const [roomId, setRoomId] = useState('general');
return (
<>
<label>
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>
<button onClick={() => setShow(!show)}>
{show ? 'Close chat' : 'Open chat'}
</button>
</label>
{show && <hr />}
{show && <ChatRoom roomId={roomId}/>}
</>
);
}Передача пустого массива зависимостей
Если ваш Effect действительно не использует никаких реактивных значений, он будет запускаться только после первоначального commit.
useEffect(() => {
// ...
}, []); // Does not run again (except once in development)Даже при пустых зависимостях setup и cleanup запускаются один дополнительный раз в разработке, чтобы помочь вам найти баги.
В этом примере serverUrl и roomId жёстко заданны. Поскольку они объявлены вне компонента, они не являются реактивными значениями и, следовательно, не являются зависимостями. Список зависимостей пуст, поэтому Effect не запускается заново при повторных рендерах.
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
const serverUrl = 'https://localhost:1234';
const roomId = 'music';
function ChatRoom() {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []);
return (
<>
<h1>Welcome to the {roomId} room!</h1>
<label>
Your message:{' '}
<input value={message} onChange={e => setMessage(e.target.value)} />
</label>
</>
);
}
export default function App() {
const [show, setShow] = useState(false);
return (
<>
<button onClick={() => setShow(!show)}>
{show ? 'Close chat' : 'Open chat'}
</button>
{show && <hr />}
{show && <ChatRoom />}
</>
);
}Передача вообще без массива зависимостей
Если вы не передаёте массив зависимостей, ваш Effect запускается после каждого commit вашего компонента.
useEffect(() => {
// ...
}); // Always runs againВ этом примере Effect запускается повторно, когда вы меняете serverUrl и roomId, и это разумно. Однако он также запускается повторно, когда вы меняете message, что, вероятно, нежелательно. Поэтому обычно и указывают массив зависимостей.
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}); // No dependency array at all
return (
<>
<label>
Server URL:{' '}
<input
value={serverUrl}
onChange={e => setServerUrl(e.target.value)}
/>
</label>
<h1>Welcome to the {roomId} room!</h1>
<label>
Your message:{' '}
<input value={message} onChange={e => setMessage(e.target.value)} />
</label>
</>
);
}
export default function App() {
const [show, setShow] = useState(false);
const [roomId, setRoomId] = useState('general');
return (
<>
<label>
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>
<button onClick={() => setShow(!show)}>
{show ? 'Close chat' : 'Open chat'}
</button>
</label>
{show && <hr />}
{show && <ChatRoom roomId={roomId}/>}
</>
);
}Обновление state на основе предыдущего state из Effect
Когда вы хотите обновить state на основе предыдущего state внутри Effect, можно столкнуться с проблемой:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // You want to increment the counter every second...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
// ...
}Поскольку count — реактивное значение, его нужно указать в списке зависимостей. Однако из-за этого Effect будет делать cleanup и setup снова каждый раз, когда меняется count. Это не идеально.
Чтобы исправить это, передайте state updater c => c + 1 в setCount:
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(c => c + 1); // ✅ Pass a state updater
}, 1000);
return () => clearInterval(intervalId);
}, []); // ✅ Now count is not a dependency
return <h1>{count}</h1>;
}Теперь, когда вы передаёте c => c + 1 вместо count + 1, вашему Effect больше не нужно зависеть от count. Благодаря этому исправлению ему не нужно будет снова делать cleanup и setup интервала каждый раз при изменении count.
Если ваш Effect зависит от объекта или функции, созданных во время рендера, он может запускаться слишком часто. Например, этот Effect переподключается после каждого commit, потому что объект options отличается при каждом рендере:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = { // 🚩 This object is created from scratch on every re-render
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // It's used inside the Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 As a result, these dependencies are always different on a commit
// ...Не используйте объект, созданный во время рендера, в качестве зависимости. Вместо этого создайте объект внутри Effect:
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} />
</>
);
}Удаление ненужных зависимостей-объектов
Если ваш Effect зависит от объекта или функции, созданных во время рендера, он может запускаться слишком часто. Например, этот Effect переподключается после каждого commit, потому что объект options отличается при каждом рендере:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() { // 🚩 This function is created from scratch on every re-render
return {
serverUrl: serverUrl,
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // It's used inside the Effect
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 As a result, these dependencies are always different on a commit
// ...Само по себе создание функции с нуля на каждом повторном рендере не является проблемой. Оптимизировать это не нужно. Однако если вы используете такую функцию как зависимость Effect, это заставит ваш Effect запускаться заново после каждого commit.
Не используйте функцию, созданную во время рендера, в качестве зависимости. Вместо этого объявите её внутри Effect:
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() {
return {
serverUrl: serverUrl,
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return (
<>
<h1>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} />
</>
);
}Теперь, когда вы определяете функцию createOptions внутри Effect, сам Effect зависит только от строки roomId. С этим исправлением ввод текста в поле не вызывает переподключение чата. В отличие от функции, которая создаётся заново, строка вроде roomId не меняется, пока вы не присвоите ей другое значение. Подробнее об удалении зависимостей.
Чтение последних props и state из Effect
По умолчанию, когда вы читаете реактивное значение внутри Effect, его нужно добавить в зависимости. Это гарантирует, что ваш Effect "реагирует" на каждое изменение этого значения. Для большинства зависимостей это именно то поведение, которое вам нужно.
Однако иногда вам нужно читать последние props и state из Effect, не "реагируя" на них. Например, представьте, что вы хотите логировать число товаров в корзине для каждого визита на страницу:
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
}Что, если вы хотите логировать новый визит на страницу после каждого изменения url, но не тогда, когда меняется только shoppingCart? Вы не можете исключить shoppingCart из зависимостей, не нарушив правила реактивности. Однако можно выразить, что вы не хотите, чтобы какой-то кусок кода "реагировал" на изменения, даже если он вызывается изнутри Effect. Объявите Effect Event с помощью Hook useEffectEvent и перенесите код, читающий shoppingCart, внутрь него:
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}Effect Events не являются реактивными и всегда должны быть исключены из зависимостей вашего Effect. Именно это позволяет помещать внутрь них нереактивный код (где можно читать последнее значение некоторых props и state). Считывая shoppingCart внутри onVisit, вы гарантируете, что shoppingCart не будет заново запускать ваш Effect.
Подробнее о том, как Effect Events позволяют разделять реактивный и нереактивный код.
Отображение разного содержимого на сервере и клиенте
Если ваше приложение использует server rendering (либо напрямую, либо через framework), ваш компонент будет рендериться в двух разных окружениях. На сервере он будет рендериться для формирования начального HTML. На клиенте React снова выполнит код рендера, чтобы прикрепить ваши обработчики событий к этому HTML. Поэтому, чтобы работал hydration, результат начального рендера на клиенте и на сервере должен быть идентичным.
В редких случаях вам может понадобиться отображать разный контент на клиенте. Например, если ваше приложение читает какие-то данные из localStorage, на сервере оно физически не сможет этого сделать. Вот как можно реализовать это:
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... return client-only JSX ...
} else {
// ... return initial JSX ...
}
}Пока приложение загружается, пользователь увидит результат начального рендера. Затем, когда всё загрузится и hydrate'ится, ваш Effect выполнится и установит didMount в true, вызвав повторный рендер. В результате отобразится клиентский вариант рендера. Effects не выполняются на сервере, поэтому didMount был false во время начального серверного рендера.
Используйте этот паттерн экономно. Помните, что пользователи с медленным соединением будут довольно долго видеть начальный контент — возможно, много секунд, — поэтому не стоит делать резкие изменения во внешнем виде компонента. Во многих случаях этого можно избежать, условно показывая разный контент с помощью CSS.
Поиск и устранение неполадок
Мой Effect запускается дважды при монтировании компонента
Когда включён Strict Mode, в разработке React один дополнительный раз выполняет setup и cleanup перед фактическим setup.
Это стресс-тест, который проверяет, что логика вашего Effect реализована правильно. Если это вызывает видимые проблемы, значит, в вашей функции cleanup не хватает какой-то логики. Функция cleanup должна останавливать или отменять всё, что делал setup. Практическое правило такое: пользователь не должен различать ситуацию, когда setup вызван один раз (как в production), и последовательность setup → cleanup → setup (как в development).
Подробнее о том, как это помогает находить баги и как исправить логику.
Мой Effect запускается после каждого повторного рендера
Сначала проверьте, не забыли ли вы указать массив зависимостей:
useEffect(() => {
// ...
}); // 🚩 No dependency array: re-runs after every commit!Сначала проверьте, не забыли ли вы указать массив зависимостей:
useEffect(() => {
// ..
}, [serverUrl, roomId]);
console.log([serverUrl, roomId]);Затем можно щёлкнуть правой кнопкой мыши по массивам из разных повторных рендеров в консоли и выбрать для обоих пункт "Store as a global variable". Предположим, первый был сохранён как temp1, а второй — как temp2. Тогда можно использовать консоль браузера, чтобы проверить, одинаковая ли каждая зависимость в обоих массивах:
Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...Когда вы находите зависимость, которая отличается при каждом повторном рендере, обычно это можно исправить одним из следующих способов:
- Обновление state на основе предыдущего state из Effect
- Удаление ненужных object dependencies
- Удаление ненужных function dependencies
- Чтение последних props и state из Effect
В крайнем случае (если эти способы не помогли) оберните её создание в useMemo или useCallback (для функций).
Мой Effect бесконечно запускается снова и снова
Если ваш Effect запускается в бесконечном цикле, должны быть верны два условия:
- Ваш Effect обновляет какой-то state.
- Этот state приводит к повторному рендеру, который вызывает изменение зависимостей Effect.
Прежде чем исправлять проблему, спросите себя: подключается ли ваш Effect к какому-то внешнему system (например, DOM, сети, стороннему widget и так далее)? Зачем вашему Effect нужно устанавливать state? Он синхронизируется с этой внешней системой? Или вы пытаетесь управлять потоком данных приложения с его помощью?
Если внешней системы нет, подумайте, не упростит ли вашу логику полное удаление Effect.
Если вы действительно синхронизируетесь с внешней системой, подумайте, почему и при каких условиях ваш Effect должен обновлять state. Изменилось ли что-то, что влияет на визуальный результат вашего компонента? Если вам нужно отслеживать какие-то данные, которые не используются при рендеринге, более подходящим может быть ref (он не вызывает повторные рендеры). Убедитесь, что ваш Effect обновляет state (и вызывает повторные рендеры) не чаще, чем нужно.
Наконец, если ваш Effect обновляет state в правильный момент, но цикл всё равно остаётся, значит, это обновление state приводит к изменению одной из зависимостей Effect. Прочитайте, как отлаживать изменения зависимостей.
Моя логика cleanup выполняется, хотя компонент не размонтировался
Функция cleanup выполняется не только при unmount, но и перед каждым повторным рендером с изменившимися зависимостями. Кроме того, в разработке React выполняет setup+cleanup ещё один раз сразу после монтирования компонента.
Если у вас есть код cleanup без соответствующего кода setup, это обычно плохой признак:
useEffect(() => {
// 🔴 Avoid: Cleanup logic without corresponding setup logic
return () => {
doSomething();
};
}, []);Логика cleanup должна быть "симметричной" логике setup и должна останавливать или отменять всё, что сделал setup:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);Узнайте, чем жизненный цикл Effect отличается от жизненного цикла компонента.
Мой Effect делает что-то визуальное, и я вижу мерцание до его запуска
Если вашему Effect нужно блокировать браузер от отрисовки экрана, замените useEffect на useLayoutEffect. Обратите внимание: это не должно требоваться для подавляющего большинства Effects. Это нужно только в тех случаях, когда критически важно запустить Effect до отрисовки браузером: например, чтобы измерить и позиционировать tooltip до того, как пользователь его увидит.