Повторное использование логики с помощью пользовательских хуков
React предоставляет несколько встроенных хуков, таких как `useState`, `useContext` и `useEffect`. Иногда вам захочется, чтобы существовал хук для какой-то более конкретной цели- например, для извлечения данных, отслеживания того, находится ли пользователь в сети, или для подключения к чату. Возможно, вы не найдете таких хуков в React, но можете создать собственные хуки для нужд вашего приложения.
Вы узнаете
- Что такое пользовательские хуки и как писать свои собственные
- Как повторно использовать логику между компонентами
- Как называть и структурировать свои пользовательские хуки
- Когда и зачем извлекать пользовательские хуки
Пользовательские хуки: совместное использование логики между компонентами
Представьте, что вы разрабатываете приложение, которое в значительной степени зависит от сети (как и большинство приложений). Вы хотите предупредить пользователя, если его сетевое соединение случайно прервалось во время использования вашего приложения. Как бы вы это реализовали? Похоже, вам понадобятся две вещи в вашем компоненте:
- Состояние, которое отслеживает, подключено ли устройство к сети.
- Эффект, который подписывается на глобальное
onlineиofflineи обновляет этот state.
Это позволит вашему компоненту синхронизироваться с состоянием сети. Вы можете начать с чего-то вроде этого:
import { useState, useEffect } from 'react';
export default function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener ('offline', handleOffline);
};
}, []);
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}Попробуйте включить и выключить сеть и обратите внимание, как этот StatusBar обновляется в ответ на ваши действия.
Теперь представьте, что вы также хотите использовать ту же логику в другом компоненте. Вы хотите реализовать кнопку «Сохранить», которая будет отключена и показывать «Повторное подключение...» вместо «Сохранить», пока сеть отключена.
Для начала вы можете скопировать и вставить состояние isOnline и эффект в SaveButton:
import { useState, useEffect } from 'react';
export default function SaveButton() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true) ;
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
function handleSaveClick() {
console.log('✅ Прогресс сохранен');
}
return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Сохранить прогресс' : 'Повторное подключение...'}
</button>
);
}Убедитесь, что при отключении сети кнопка изменит свой вид.
Эти два компонента работают нормально, но дублирование логики между ними нежелательно. Похоже, что, несмотря на различия в визуальном оформлении, вы хотите повторно использовать логику между ними.
Извлечение собственного пользовательского хука из компонента
Представьте на мгновение, что, подобно useState и useEffect, существовал встроенный хук useOnlineStatus. Тогда оба этих компонента можно было бы упростить, и вы могли бы устранить дублирование между ними:
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
function handleSaveClick() {
console.log('✅ Прогресс сохранен');
}
return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Сохранить прогресс' : 'Повторное подключение...'}
</button>
);
}Хотя такого встроенного хука нет, вы можете написать его самостоятельно. Объявите функцию с именем useOnlineStatus и перенесите в нее весь дублирующийся код из компонентов, которые вы написали ранее:
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}В конце функции верните isOnline. Это позволит вашим компонентам считывать это значение:
import { useOnlineStatus } from './useOnlineStatus.js';
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
function handleSaveClick() {
console.log(' ✅ Прогресс сохранен');
}
return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Сохранить прогресс' : 'Повторное подключение...'}
</button>
);
}
export default function App() {
return (
<>
<SaveButton />
<StatusBar />
</>
);
}Убедитесь, что включение и отключение сети обновляет оба компонента.
Теперь в ваших компонентах стало меньше повторяющейся логики. *Что еще важнее, код внутри них описывает что они хотят сделать (использовать статус онлайн!), а не как это сделать (подписываясь на события браузера). *
Когда вы выносите логику в пользовательские хуки, вы можете скрыть сложные детали взаимодействия с какой-либо внешней системой или API браузера. Код ваших компонентов выражает ваше намерение, а не реализацию.
Имена хуков всегда начинаются с use
Приложения React строятся из компонентов. Компоненты строятся из хуков, будь то встроенные или пользовательские. Вы, вероятно, часто будете использовать пользовательские хуки, созданные другими, но иногда вы можете написать их самостоятельно!
Вы должны следовать следующим соглашениям об именовании:
- Имена компонентов React должны начинаться с заглавной буквы, например
StatusBarиSaveButton. Компоненты React также должны возвращать что-то, что React умеет отображать, например, фрагмент JSX. - Имена хуков должны начинаться с
use, за которым следует заглавная буква, какuseState(встроенный) илиuseOnlineStatus(пользовательский, как показано выше на этой странице). Хуки могут возвращать произвольные значения.
Это соглашение гарантирует, что вы всегда можете взглянуть на компонент и понять, где его состояние, эффекты и другие функции React могут « скрываются». Например, если вы видите вызов функции getColor() внутри вашего компонента, вы можете быть уверены, что она не может содержать состояние React, поскольку её имя не начинается с use. Однако вызов функции, подобный useOnlineStatus(), скорее всего, будет содержать вызовы других хуков!
Если ваш линтер настроен для React, он будет обеспечивать соблюдение этого соглашения об именовании. Прокрутите вверх к песочнице выше и переименуйте useOnlineStatus в getOnlineStatus. Обратите внимание, что линтер больше не позволит вам вызывать useState или useEffect внутри него. Только хуки и компоненты могут вызывать другие хуки!
Должны ли все функции, вызываемые во время рендеринга, начинаться с префикса use?
Нет. Функции, которые не вызывают хуки, не обязательно должны быть хуками.
Если ваша функция не вызывает никаких хуков, избегайте префикса use. Вместо этого напишите её как обычную функцию без префикса use. Например, useSorted ниже не вызывает хуки, поэтому назовите её getSorted:
// 🔴 Не рекомендуется: хук, который не использует хуки
function useSorted(items) {
return items.slice().sort();
}
// ✅ Рекомендуется: обычная функция, которая не использует хуки
function getSorted(items) {
return items.slice().sort();
}Это гарантирует, что ваш код сможет вызывать эту обычную функцию где угодно, включая условия:
function List({ items, shouldSort }) {
let displayedItems = items;
if (shouldSort) {
// ✅ Можно вызывать getSorted() условно, потому что это не хук
displayedItems = getSorted(items);
}
// ...
}Вы должны добавить префикс use к функции (и, таким образом, сделать ее хуком), если она использует хотя бы один хук внутри себя:
// ✅ Хорошо: хук, использующий другие хуки
function useAuth() {
return useContext(Auth);
}Технически React это не требует. В принципе, вы можете создать Hook, который не вызывает другие Hooks. Это часто сбивает с толку и ограничивает возможности, поэтому лучше избегать такого шаблона. Однако могут быть редкие случаи, когда это будет полезно. Например, возможно, ваша функция сейчас не использует никаких хуков, но вы планируете добавить в нее вызовы хуков в будущем. Тогда имеет смысл назвать ее с префиксом use:
// ✅ Правильно: хук, который, вероятно, будет использовать другие хуки позже
function useAuth() {
// TODO: Заменить этой строкой, когда будет реализована аутентификация:
// return useContext(Auth);
return TEST_USER;
}Тогда компоненты не смогут вызывать его условно. Это станет важным, когда вы действительно добавите вызовы хуков внутрь. Если вы не планируете использовать хуки внутри него (ни сейчас, ни позже), не делайте его хуком.
Пользовательские хуки позволяют делиться логикой, зависящей от состояния, а не самим состоянием
В предыдущем примере, когда вы включали и выключали сеть, оба компонента обновлялись одновременно. Однако неправильно думать, что между ними используется одна и та же переменная состояния isOnline. Посмотрите на этот код:
function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}
function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}Это работает так же, как и до того, как вы устранили дублирование:
function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}
function SaveButton() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}Это две совершенно независимые переменные состояния и эффекты! Они просто оказались с одинаковым значением в одно и то же время, потому что вы синхронизировали их с одним и тем же внешним значением (включена ли сеть).
Чтобы лучше проиллюстрировать это, нам понадобится другой пример. Рассмотрим этот компонент Form:
import { useState } from 'react';
export default function Form() {
const [firstName, setFirstName] = useState('Mary');
const [lastName, setLastName] = useState('Poppins');
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
return (
<>
<label>
Имя:
<input value={firstName} onChange={handleFirstNameChange} />
</label>
<label>
Фамилия:
<input value={lastName} onChange={handleLastNameChange} />
</label>
<p><b>Доброе утро, {firstName} {lastName}.</b></p>
</>
);
}Для каждого поля формы присутствует повторяющаяся логика:
- Есть часть состояния (
firstNameиlastName). - Есть обработчик изменений (
handleFirstNameChangeиhandleLastNameChange). - Есть фрагмент JSX, который задает атрибуты
valueиonChangeдля этого поля ввода.
Вы можете вынести повторяющуюся логику в этот пользовательский хук useFormInput:
import { useFormInput } from './useFormInput.js';
export default function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
return (
<>
<label>
Имя:
<input {...firstNameProps} />
</label>
<label>
Фамилия:
<input {...lastNameProps} />
</label>
<p><b>Доброе утро, {firstNameProps.value} {lastNameProps.value}.</b></p>
</>
);
}//useFormInput.js active
import { useState } from 'react';
export function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
const inputProps = {
value: value,
onChange: handleChange
};
return inputProps;
}Обратите внимание, что здесь объявляется только одна переменная состояния с именем value.
Однако компонент Form вызывает useFormInput два раза:
function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
// . ..Вот почему это работает так, как будто объявляются две отдельные переменные состояния!
Пользовательские хуки позволяют вам совместно использовать * логику состояния, но не само состояние. Каждый вызов хука полностью независим от всех остальных вызовов того же хука.* Именно поэтому две приведенные выше «песочницы» полностью эквивалентны. Если хотите, прокрутите страницу вверх и сравните их. Поведение до и после извлечения пользовательского хука идентично.
Когда вам нужно совместно использовать само состояние между несколькими компонентами, перенесите его на более высокий уровень и передайте вниз.
Передача реактивных значений между хуками
Код внутри ваших пользовательских хуков будет перезапускаться при каждом перерисовке вашего компонента. Вот почему, как и компоненты, пользовательские хуки должны быть чистыми. Думайте о коде пользовательских хуков как о части тела вашего компонента!
Поскольку пользовательские хуки перерисовываются вместе с вашим компонентом, они всегда получают самые последние пропсы и состояние. Чтобы понять, что это значит, рассмотрим этот пример чата. Измените URL-адрес сервера или чат:
import { useState } from 'react';
import ChatRoom from './ChatRoom.js';
export default function App() {
const [roomId, setRoomId] = useState('general');
return (
<>
<label>
Выберите чат-комнату:{' '}
<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}
/>
</>
);
}Когда вы изменяете serverUrl или roomId, эффект «реагирует» на ваши изменения и повторно синхронизируется.
По сообщениям в консоли можно определить, что чат переподключается каждый раз, когда вы изменяете зависимости вашего эффекта.
Теперь перенесите код эффекта в пользовательский хук:
export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('New message: ' + msg);
});
return () => connection.disconnect() ;
}, [roomId, serverUrl]);
}Это позволяет вашему компоненту ChatRoom вызывать ваш пользовательский хук, не беспокоясь о том, как он работает внутри:
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
return (
<>
<label>
URL сервера:
<input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
</label>
<h1>Добро пожаловать в комнату {roomId}!</h1>
</>
);
}Это выглядит гораздо проще! (Но делает то же самое.)
Обратите внимание, что логика по-прежнему реагирует на изменения проп и состояния. Попробуйте изменить URL сервера или выбранную комнату:
//App.js
import { useState } from 'react';
import ChatRoom from './ChatRoom.js';
export default function App() {
const [roomId, setRoomId] = useState('general');
return (
<>
<label>
Выберите чат-комнату: {' '}
<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}
/>
</>
);
}Обратите внимание, как вы берете возвращаемое значение одного хука:
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom( {
roomId: roomId,
serverUrl: serverUrl
});
// ...и передаете его в качестве входных данных другому хуку:
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...Каждый раз, когда ваш компонент ChatRoom перерисовывается, он передает последние значения roomId и serverUrl вашему хуку. Именно поэтому ваш эффект повторно подключается к чату всякий раз, когда их значения отличаются после перерисовки. (Если вы когда-либо работали с программным обеспечением для обработки аудио или видео, такая цепочка хуков может напомнить вам цепочку визуальных или звуковых эффектов. Это как будто выходные данные use State «подается» на вход useChatRoom.)
Передача обработчиков событий в пользовательские хуки
По мере того, как вы начнете использовать useChatRoom во все большем количестве компонентов, вы, возможно, захотите позволить компонентам настраивать его поведение. Например, в настоящее время логика действий при поступлении сообщения жестко запрограммирована внутри Hook:
export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('New message: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}Предположим, вы хотите перенести эту логику обратно в свой компонент:
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
showNotification('Новое сообщение: ' + msg);
}
});
// ...Чтобы это работало, измените свой пользовательский хук так, чтобы он принимал onReceiveMessage в качестве одного из именованных параметров:
export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onReceiveMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl, onReceiveMessage]); // ✅ Все зависимости объявлены
}Это будет работать, но есть еще одно улучшение, которое вы можете внести, если ваш пользовательский хук принимает обработчики событий.
Добавление зависимости от onReceiveMessage не является идеальным решением, поскольку это приведет к повторному подключению чата при каждом перерисовке компонента. Объедините этот обработчик событий в Effect Event, чтобы удалить его из зависимостей:
import { useEffect, useEffectEvent } from 'react';
// ...
export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ Все зависимости объявлены
}Теперь чат не будет переподключаться каждый раз, когда компонент ChatRoom перерисовывается. Вот полностью рабочая демонстрация передачи обработчика событий в пользовательский хук, с которой вы можете поэкспериментировать:
//App.js
import { useState } from 'react';
import ChatRoom from './ChatRoom.js';
export default function App
() {
const [roomId, setRoomId] = useState('general');
return (
<>
<label>
Выберите чат-комнату:{' '}
<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}
/>
</>
);
}Обратите внимание, что теперь вам не нужно знать, как работает useChatRoom, чтобы его использовать. Вы можете добавить его в любой другой компонент, передать любые другие опции, и он будет работать точно так же. В этом и заключается сила пользовательских хуков.
Когда использовать пользовательские хуки
Не нужно извлекать пользовательский хук для каждого небольшого дублирующегося фрагмента кода. Некоторое дублирование допустимо. Например, извлечение хука useFormInput для обертывания одного вызова useState, как ранее, вероятно, не нужно.
Однако всякий раз, когда вы пишете эффект, подумайте, не будет ли понятнее обернуть его в пользовательский хук. Вам не должны нуждаться в эффектах очень часто, поэтому, если вы пишете один, это означает, что вам нужно «выйти за пределы React», чтобы синхронизироваться с какой-то внешней системой или сделать что-то, для чего у React нет встроенного API. Обернув его в пользовательский хук, вы сможете точно передать свое намерение и то, как данные проходят через него.
Например, рассмотрим компонент ShippingForm, который отображает два выпадающих списка: один показывает список городов, а другой — список районов в выбранном городе. Вы можете начать с кода, который выглядит примерно так:
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// Этот эффект загружает города для страны
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
// Этот эффект загружает районы для выбранного города
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]);
// ...Хотя этот код довольно повторяется, [правильно держать эти эффекты отдельно друг от друга.](/learn/removing-effect-dependencies#is-your -effect-doing-several-unrelated-things) Они синхронизируют две разные вещи, поэтому не следует объединять их в один эффект. Вместо этого вы можете упростить компонент ShippingForm выше, извлекая общую логику между ними в свой собственный хук useData:
function useData(url) {
const [data, setData] = useState(null);
useEff ect(() => {
if (url) {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}
}, [url]);
return data;
}Теперь вы можете заменить оба эффекта в компонентах ShippingForm вызовами useData:
function ShippingForm({ country }) {
const cities = useData(`/api/cities?country=${country}`);
const
[city, setCity] = useState(null);
const areas = useData(city ? `/api/areas?city=${city}` : null);
// ...Извлечение пользовательского хука делает поток данных явным. Вы подаете url и получаете data. «Скрывая» свой эффект внутри useData, вы также предотвращаете добавление ненужных зависимостей к нему кем-либо, работающим над компонентом ShippingForm. Со временем большинство эффектов вашего приложения будут находиться в пользовательских хуках.
Сосредоточьте свои пользовательские хуки на конкретных высокоуровневых сценариях использования
Начните с выбора имени для вашего пользовательского хука. Если вам сложно подобрать понятное имя, это может означать, что ваш эффект слишком тесно связан с остальной логикой вашего компонента и еще не готов к извлечению.
В идеале имя вашего пользовательского хука должно быть достаточно понятным, чтобы даже человек, который не часто пишет код, мог с высокой степенью вероятности догадаться, что делает ваш пользовательский хук, что он принимает и что возвращает:
- ✅
useData(url) - ✅
useImpressionLog(eventName, extraData) - ✅
useChatRoom(options)
При синхронизации с внешней системой имя вашего пользовательского хука может быть более техническим и использовать жаргон, специфичный для этой системы. Это допустимо, если оно будет понятно человеку, знакомому с этой системой:
- ✅
useMediaQuery(query) - ✅
useSocket(url) - ✅
useIntersectionObserver(ref, options)
Ориентируйте пользовательские хуки на конкретные высокоуровневые сценарии использования. Избегайте создания и использования пользовательских хуков «жизненного цикла», которые действуют как альтернативы и удобные оболочки для самого API useEffect:
- 🔴
useMount(fn) - 🔴
useEffectOnce(fn) - 🔴
useUpdateEffect(fn)
Например, этот хук useMount пытается гарантировать, что некоторый код выполняется только «при монтировании»:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
// 🔴 Не рекомендуется: использование пользовательских хуков «жизненного цикла»
useMount(() => {
const connection = createConnection({ roomId, serverUrl });
connection.connect();
post('/analytics/event', { eventName: 'visit_chat' });
});
// ...
}
// 🔴 Избегайте: создания пользовательских хуков «жизненного цикла»
function useMount(fn) {
useEffect(() => {
fn();
}, []); // 🔴 В хуке React useEffect отсутствует зависимость: 'fn'
}Пользовательские хуки «жизненного цикла», такие как useMount, плохо вписываются в парадигму React. Например, в этом примере кода есть ошибка (он не «реагирует» на изменения roomId или serverUrl), , но линтер не предупредит вас об этом, поскольку он проверяет только прямые вызовы useEffect. Он не узнает о вашем хуке.
Если вы пишете эффект, начните с прямого использования API React:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https:// localhost:1234');
// ✅ Хорошо: два отдельных эффекта, разделенных по назначению
useEffect(() => {
const connection = createConnection({ serverUrl, roomId });
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
useEffect(() => {
post('/analytics/event', { eventName: 'visit_chat', roomId });
}, [roomId]);
// . ..
}Затем вы можете (но не обязаны) извлечь пользовательские хуки для различных сценариев использования высокого уровня:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
// ✅ Отлично: пользовательские хуки, названные в соответствии с их назначением
useChatRoom({ serverUrl, roomId });
useImpressionLog('visit_chat', { roomId });
// ...
}Хороший пользовательский хук делает вызывающий код более декларативным, ограничивая его функции. Например, useChatRoom(options) может только подключаться к чату, а useImpressionLog(eventName, extraData) — только отправлять журнал просмотров в аналитику. Если ваш пользовательский API хуков не ограничивает варианты использования и является очень абстрактным, в долгосрочной перспективе он, скорее всего, создаст больше проблем, чем решит.
Пользовательские хуки помогают перейти к более эффективным шаблонам
Эффекты — это «аварийный выход» : их используют, когда нужно «выйти за пределы React» и когда для вашего сценария использования нет лучшего встроенного решения. Со временем команда React планирует свести количество эффектов в вашем приложении к минимуму, предлагая более конкретные решения для более конкретных задач. Оборачивание ваших эффектов в пользовательские хуки упрощает обновление кода, когда такие решения станут доступны.
Вернёмся к этому примеру:
import { useOnlineStatus } from './useOnlineStatus.js';
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Отключено'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
function handleSaveClick() {
console.log('✅ Прогресс сохранен');
}
return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Сохранить прогресс' : 'Повторное подключение...'}
</button>
);
}
export default function App() {
return (
<>
<SaveButton />
<StatusBar />
</>
);
}В приведенном выше примере useOnlineStatus реализован с помощью пары useState и useEffect. Однако это не самое лучшее из возможных решений. Есть ряд крайних случаев, которые оно не учитывает. Например, оно предполагает, что при монтировании компонента isOnline уже равно true, но это может быть неверно, если сеть уже отключилась. Вы можете использовать API браузера navigator.onLine для проверить это, но его прямое использование не сработает на сервере при генерации исходного HTML. Одним словом, этот код можно улучшить.
React включает специальный API под названием useSyncExternalStore, который решает все эти проблемы за вас. Вот ваш хук useOnlineStatus, переписанный с учетом этого нового API:
import { useOnlineStatus } from './useOnlineStatus.js';
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
function handleSaveClick() {
console.log('✅ Progress saved');
}
return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Сохранить прогресс' : 'Повторное подключение...'}
</button>
);
}
export default function App() {
return (
<>
<SaveButton />
<StatusBar />
</>
);
}Обратите внимание, что вам не нужно было изменять ни один из компонентов, чтобы осуществить этот переход:
function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}
function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}Это еще одна причина, по которой обертывание эффектов в пользовательские хуки часто бывает полезно:
- Вы делаете поток данных в и из ваших эффектов очень явным.
- Вы позволяете вашим компонентам сосредоточиться на замысле, а не на конкретной реализации ваших эффектов.
- Когда React добавляет новые функции, вы можете удалить эти эффекты, не изменяя ни один из ваших компонентов.
Подобно системе дизайна,, вам может быть полезно начать извлекать общие идиомы из компонентов вашего приложения в пользовательские хуки. Это позволит коду ваших компонентов сосредоточиться на замысле и избежать частого написания «сырых» эффектов. Сообщество React поддерживает множество отличных пользовательских хуков.
Предоставит ли React какое-либо встроенное решение для извлечения данных?
Сегодня, с помощью API use, данные можно считывать при рендеринге, передавая [Promise](https://developer.mozilla.org/en-US/ docs/Web/JavaScript/Reference/Global_Objects/Promise) в use:
import { use, Suspense } from «react»;
function Message({ messagePromise }) {
const messageContent = use(messagePromise);
return <p>Вот сообщение: {messageContent}</p>;
}
export function MessageContainer({ messagePromise }) {
return (
<Suspense fallback={<p>⌛Загрузка сообщения...</p>}>
<Message messagePromise={messagePromise} />
</Suspense>
);
}Мы еще прорабатываем детали, но ожидаем, что в будущем вы будете писать код для извлечения данных примерно так:
import { use } from 'react';
function ShippingForm({ country }) {
const cities = use(fetch(`/api/cities? country=${country}`));
const [city, setCity] = useState(null);
const areas = city ? use(fetch(`/api/areas?city=${city}`)) : null;
// ...Если вы используете в приложении пользовательские хуки, такие как useData выше, для перехода на рекомендуемый в конечном итоге подход потребуется меньше изменений, чем если бы вы вручную писали необработанные эффекты в каждом компоненте. Однако старый подход по-прежнему будет работать нормально, поэтому, если вам нравится писать необработанные эффекты, вы можете продолжать это делать.
Есть более чем один способ сделать это
Допустим, вы хотите реализовать анимацию появления с нуля с помощью браузерного API requestAnimationFrame. Вы можете начать с эффекта, который настраивает цикл анимации. В течение каждого кадра анимации вы можете изменять прозрачность узла DOM, который вы храните в референции, пока она не достигнет 1.
Иногда вам даже не нужен хук!
Вывод
- Пользовательские хуки позволяют совместно использовать логику между компонентами.
- Имена пользовательских хуков должны начинаться с
use, за которым следует заглавная буква. - Пользовательские хуки передают только логику, связанную с состоянием, а не само состояние.
- Вы можете передавать реактивные значения из одного хука в другой, и они остаются актуальными.
- Все хуки запускаются заново каждый раз, когда ваш компонент перерисовывается.
- Код ваших пользовательских хуков должен быть чистым, как и код вашего компонента.
- Оборачивайте обработчики событий, полученные пользовательскими хуками, в Effect Events.
- Не создавайте пользовательские хуки типа
useMount. Пусть их назначение остается конкретным. - Как и где выбрать границы вашего кода — решать вам.