Обходные пути
Некоторым вашим компонентам может потребоваться управлять системами вне React и синхронизироваться с ними. Например, вам может понадобиться установить фокус на поле ввода с помощью API браузера, запустить и приостановить видеоплеер, реализованный без React, или подключиться к удалённому серверу и прослушивать его сообщения. В этой главе вы узнаете об обходных путях, которые позволяют «выйти за пределы» React и подключиться к внешним системам. Большая часть логики приложения и потока данных не должна зависеть от этих возможностей.
В этой главе
- Как «запоминать» информацию без повторного рендеринга
- Как получать доступ к DOM-элементам, которыми управляет React
- Как синхронизировать компоненты с внешними системами
- Как убрать из компонентов ненужные эффекты
- Чем жизненный цикл эффекта отличается от жизненного цикла компонента
- Как не допустить повторного запуска эффектов при изменении некоторых значений
- Как запускать эффект повторно реже
- Как повторно использовать логику в разных компонентах
Ссылки на значения с помощью рефов
Если вы хотите, чтобы компонент «запоминал» какую-либо информацию, но не хотите, чтобы эта информация вызывала повторный рендеринг, можно использовать реф:
const ref = useRef(0); Как и состояние, рефы сохраняются React между повторными рендерингами. Однако обновление состояния вызывает повторный рендеринг компонента. Изменение рефа — нет! Получить текущее значение рефа можно через свойство ref.current.
import { useRef } from 'react'; export default function Counter() { let ref = useRef(0); function handleClick() { ref.current = ref.current + 1; alert('You clicked ' + ref.current + ' times!'); } return ( <button onClick={handleClick}> Click me! </button> ); }
Реф — это как секретный карман компонента, за которым React не следит. Например, с помощью рефов можно хранить идентификаторы таймеров, DOM-элементы и другие объекты, которые не влияют на результат рендеринга компонента.
Хотите узнать об этом подробнее?
Прочитайте статью Ссылки на значения с помощью рефов, чтобы узнать, как использовать рефы для хранения информации.
ПодробнееИзменение DOM с помощью рефов
React автоматически обновляет DOM в соответствии с результатами рендеринга, поэтому компонентам обычно не нужно изменять его вручную. Однако иногда может понадобиться доступ к DOM-элементам, которыми управляет React, — например, чтобы установить фокус на узел, прокрутить страницу до него или измерить его размер и положение. В React нет встроенного способа делать это, поэтому вам понадобится реф на DOM-узел. Например, при нажатии кнопки фокус будет установлен на поле ввода с помощью рефа:
import { useRef } from 'react'; export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
Хотите узнать об этом подробнее?
Прочитайте статью Изменение DOM с помощью рефов, чтобы узнать, как получать доступ к DOM-элементам, которыми управляет React.
ПодробнееСинхронизация с помощью эффектов
Некоторым компонентам требуется синхронизироваться с внешними системами. Например, вы можете захотеть управлять компонентом, не относящимся к React, на основе состояния React, настроить подключение к серверу или отправлять запись в аналитику, когда компонент появляется на экране. В отличие от обработчиков событий, которые позволяют обрабатывать определённые события, эффекты позволяют выполнять код после рендеринга. Используйте их, чтобы синхронизировать компонент с системой вне React.
Несколько раз нажмите «Воспроизвести/Приостановить» и посмотрите, как видеоплеер синхронизируется со значением свойства isPlaying:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } }, [isPlaying]); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
Многие эффекты также выполняют «очистку» после себя. Например, эффект, который устанавливает соединение с сервером чата, должен возвращать функцию очистки, которая сообщает React, как отключить компонент от этого сервера:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the chat!</h1>; }
В режиме разработки React сразу же один дополнительный раз запустит и очистит ваш эффект. Поэтому вы видите, что "✅ Connecting..." выводится дважды. Это помогает убедиться, что вы не забыли реализовать функцию очистки.
Хотите узнать об этом подробнее?
Прочитайте статью Синхронизация с помощью эффектов, чтобы узнать, как синхронизировать компоненты с внешними системами.
ПодробнееВозможно, эффект вам не нужен
Эффекты — это обходной путь за пределы парадигмы React. Они позволяют «выйти за пределы» React и синхронизировать компоненты с внешними системами. Если внешняя система не задействована (например, если вы хотите обновить состояние компонента при изменении свойств или состояния), эффект, скорее всего, не нужен. Удаление ненужных эффектов упростит чтение кода, ускорит его выполнение и снизит вероятность ошибок.
Есть два распространённых случая, когда эффекты не нужны:
- Эффекты не нужны для преобразования данных при рендеринге.
- Эффекты не нужны для обработки пользовательских событий.
Например, эффект не нужен, чтобы обновить одно состояние на основе другого:
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]);
// ...
} Вместо этого вычисляйте всё возможное во время рендеринга:
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
// ...
} Однако эффекты нужны для синхронизации с внешними системами.
Хотите узнать об этом подробнее?
Прочитайте статью Возможно, эффект вам не нужен, чтобы узнать, как убрать ненужные эффекты.
ПодробнееЖизненный цикл реактивных эффектов
У эффектов другой жизненный цикл, не такой, как у компонентов. Компоненты могут монтироваться, обновляться или размонтироваться. Эффект может только начать синхронизацию и позднее прекратить её. Этот цикл может повторяться несколько раз, если эффект зависит от свойств и состояния, которые со временем меняются.
Этот эффект зависит от значения свойства roomId. Свойства — это реактивные значения, то есть они могут измениться при повторном рендеринге. Обратите внимание: эффект повторно синхронизируется (и подключается к серверу заново), если изменяется roomId:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
React предоставляет правило линтера, которое проверяет, правильно ли указаны зависимости эффекта. Если в приведённом выше примере вы забудете добавить roomId в список зависимостей, линтер автоматически обнаружит эту ошибку.
Хотите узнать об этом подробнее?
Прочитайте статью Жизненный цикл реактивных событий, чтобы узнать, чем жизненный цикл эффекта отличается от жизненного цикла компонента.
ПодробнееОтделение событий от эффектов
Обработчики событий запускаются повторно только тогда, когда вы снова выполняете то же взаимодействие. В отличие от них, эффекты повторно синхронизируются, если какие-либо используемые ими значения, например свойства или состояние, отличаются от значений при предыдущем рендеринге. Иногда нужны оба варианта поведения: эффект, который повторно запускается при изменении одних значений, но не других.
Весь код внутри эффектов реактивен. Он запустится снова, если какое-либо используемое им реактивное значение изменится при повторном рендеринге. Например, этот эффект повторно подключится к чату, если изменится roomId или theme:
import { useState, useEffect } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId, theme }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { showNotification('Connected!', theme); }); connection.connect(); return () => connection.disconnect(); }, [roomId, theme]); return <h1>Welcome to the {roomId} room!</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = 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> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
Это не лучший вариант. Вы хотите повторно подключаться к чату только при изменении roomId. Переключение theme не должно вызывать повторное подключение к чату! Перенесите код, который читает theme, из эффекта в событие эффекта:
import { useState, useEffect } from 'react'; import { useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); }); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = 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> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
Код внутри событий эффекта не является реактивным, поэтому изменение theme больше не приводит к повторному подключению эффекта.
Хотите узнать об этом подробнее?
Прочитайте статью Отделение событий от эффектов, чтобы узнать, как не допустить повторного запуска эффектов при изменении некоторых значений.
ПодробнееУдаление зависимостей эффектов
Когда вы пишете эффект, линтер проверяет, включили ли вы в список зависимостей все реактивные значения (например, свойства и состояние), которые читает эффект. Это гарантирует, что эффект остаётся синхронизированным с последними свойствами и состоянием компонента. Ненужные зависимости могут привести к слишком частому запуску эффекта или даже к бесконечному циклу. Способ их удаления зависит от конкретного случая.
Например, этот эффект зависит от объекта options, который создаётся заново при каждом изменении поля ввода:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); const options = { serverUrl: serverUrl, roomId: roomId }; useEffect(() => { const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [options]); return ( <> <h1>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
Вы не хотите, чтобы чат подключался заново каждый раз, когда вы начинаете набирать в нём сообщение. Чтобы решить эту проблему, создавайте объект options внутри эффекта, чтобы эффект зависел только от строки roomId:
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} /> </> ); }
Обратите внимание: вы не стали сразу изменять список зависимостей и удалять из него зависимость options. Это было бы неправильно. Вместо этого вы изменили окружающий код, чтобы зависимость стала ненужной. Считайте список зависимостей перечнем всех реактивных значений, используемых в коде эффекта. Вы не выбираете намеренно, что включать в этот список. Список отражает ваш код. Чтобы изменить список зависимостей, измените код.
Хотите узнать об этом подробнее?
Прочитайте статью Удаление зависимостей эффектов, чтобы узнать, как запускать эффект повторно реже.
ПодробнееПовторное использование логики с помощью пользовательских хуков
В React есть встроенные хуки, такие как useState, useContext и useEffect. Иногда вам может понадобиться хук для более конкретной задачи: например, для загрузки данных, отслеживания того, находится ли пользователь в сети, или подключения к комнате чата. Для этого можно создавать собственные хуки, подходящие для задач вашего приложения.
В этом примере пользовательский хук usePointerPosition отслеживает положение курсора, а пользовательский хук useDelayedValue возвращает значение, которое «отстаёт» от переданного вами на заданное количество миллисекунд. Перемещайте курсор по области предпросмотра песочницы, чтобы увидеть, как за ним движется след из точек:
import { usePointerPosition } from './usePointerPosition.js'; import { useDelayedValue } from './useDelayedValue.js'; export default function Canvas() { const pos1 = usePointerPosition(); const pos2 = useDelayedValue(pos1, 100); const pos3 = useDelayedValue(pos2, 200); const pos4 = useDelayedValue(pos3, 100); const pos5 = useDelayedValue(pos4, 50); return ( <> <Dot position={pos1} opacity={1} /> <Dot position={pos2} opacity={0.8} /> <Dot position={pos3} opacity={0.6} /> <Dot position={pos4} opacity={0.4} /> <Dot position={pos5} opacity={0.2} /> </> ); } function Dot({ position, opacity }) { return ( <div style={{ position: 'absolute', backgroundColor: 'pink', borderRadius: '50%', opacity, transform: `translate(${position.x}px, ${position.y}px)`, pointerEvents: 'none', left: -20, top: -20, width: 40, height: 40, }} /> ); }
Вы можете создавать пользовательские хуки, объединять их, передавать между ними данные и повторно использовать в разных компонентах. По мере роста приложения вы будете писать меньше эффектов вручную, поскольку сможете повторно использовать уже созданные пользовательские хуки. Сообщество React также поддерживает множество отличных пользовательских хуков.
Хотите узнать об этом подробнее?
Прочитайте статью Повторное использование логики с помощью пользовательских хуков, чтобы узнать, как делиться логикой между компонентами.
ПодробнееЧто дальше?
Перейдите к статье Ссылки на значения с помощью рефов, чтобы начать читать эту главу страницу за страницей!
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/learn/escape-hatches