Переиспользование логики с помощью пользовательских хуков¶
React поставляется с несколькими встроенными хуками, такими как useState, useContext и useEffect. Иногда вам захочется иметь хук для какой-то более конкретной цели: например, для получения данных, отслеживания того, находится ли пользователь в сети, или для подключения к чату. Возможно, вы не найдете таких хуков в React, но вы можете создать свои собственные хуки для нужд вашего приложения.
Что такое пользовательские хуки и как написать свой собственный
Как повторно использовать логику между компонентами
Как назвать и структурировать пользовательские хуки
Когда и зачем извлекать пользовательские хуки
Пользовательские хуки: Совместное использование логики между компонентами {/custom-hooks-sharing-logic-between-components/}¶
Представьте, что вы разрабатываете приложение, которое сильно зависит от сети (как и большинство приложений). Вы хотите предупредить пользователя, если его сетевое соединение случайно прервалось во время работы с вашим приложением. Как вы собираетесь это сделать? Похоже, что вам понадобятся две вещи в вашем компоненте:
Элемент состояния, который отслеживает, находится ли сеть в сети.
Эффект, который подписывается на глобальные события online и offline и обновляет это состояние.
Это позволит вашему компоненту синхронизироваться со статусом сети. Вы можете начать с чего-то подобного:
Попробуйте включить и выключить сеть, и обратите внимание, как эта StatusBar обновляется в ответ на ваши действия.
Теперь представьте, что вы также хотите использовать ту же логику в другом компоненте. Вы хотите реализовать кнопку Save, которая будет отключена и показывать "Reconnecting..." вместо "Save", пока сеть выключена.
Для начала вы можете скопировать и вставить состояние isOnline и эффект в SaveButton:
Убедитесь, что при отключении сети кнопка изменит свой вид.
Эти два компонента работают нормально, но дублирование логики между ними вызывает сожаление. Похоже, что даже если они имеют разный визуальный вид, вы хотите повторно использовать логику между ними.
Извлечение собственного пользовательского хука из компонента {/extracting-your-own-custom-hook-from-a-component/}¶
Представьте на секунду, что, подобно useState и useEffect, существует встроенный хук useOnlineStatus. Тогда оба этих компонента можно было бы упростить и убрать дублирование между ними:
Хотя такого встроенного Hook не существует, вы можете написать его самостоятельно. Объявите функцию useOnlineStatus и перенесите в нее весь дублирующийся код из компонентов, которые вы написали ранее:
Убедитесь, что включение и выключение сети обновляет оба компонента.
Теперь в ваших компонентах не так много повторяющейся логики. Более того, код внутри них описывает что они хотят сделать (использовать сетевой статус!), а не как это сделать (подписываясь на события браузера).
Когда вы извлекаете логику в пользовательские Hooks, вы можете скрыть ужасные детали того, как вы работаете с какой-то внешней системой или API браузера. Код ваших компонентов выражает ваше намерение, а не реализацию.
Имена хуков всегда начинаются с use {/hook-names-always-start-with-use/}¶
Приложения React строятся из компонентов. Компоненты строятся из хуков, встроенных или пользовательских. Скорее всего, вы часто будете использовать пользовательские хуки, созданные другими, но иногда вы можете написать один самостоятельно!
Вы должны следовать этим соглашениям об именовании:
Имена компонентов React должны начинаться с заглавной буквы, например, StatusBar и SaveButton. Компоненты React также должны возвращать что-то, что React умеет отображать, например, кусок JSX.
Имена хуков должны начинаться с use, за которым следует заглавная буква, например useState (встроенный) или useOnlineStatus (пользовательский, как ранее на этой странице). Хуки могут возвращать произвольные значения.
Это соглашение гарантирует, что вы всегда сможете посмотреть на компонент и узнать, где может "прятаться" его состояние, Эффекты и другие возможности React. Например, если вы видите вызов функции getColor() внутри вашего компонента, вы можете быть уверены, что она не может содержать внутри себя состояние React, потому что ее имя не начинается с use. Однако вызов такой функции, как useOnlineStatus(), скорее всего, будет содержать вызовы других Hooks внутри!
Если ваш линтер настроен на React,, он будет применять это соглашение об именовании. Прокрутите вверх до песочницы выше и переименуйте useOnlineStatus в getOnlineStatus. Обратите внимание, что линтер больше не позволит вам вызывать useState или useEffect внутри него. Только Hooks и компоненты могут вызывать другие Hooks!
Должны ли все функции, вызываемые во время рендеринга, начинаться с префикса use? {/should-all-functions called-during-rendering-start-with-the-use-prefix/}¶
Нет. Функции, которые не вызывают Hooks, не обязаны быть Hooks.
Если ваша функция не вызывает никаких хуков, избегайте префикса use. Вместо этого напишите ее как обычную функцию без префикса use. Например, useSorted ниже не вызывает хуков, поэтому вместо этого назовите ее getSorted:
123456789
// 🔴 Avoid: A Hook that doesn't use HooksfunctionuseSorted(items){returnitems.slice().sort();}// ✅ Good: A regular function that doesn't use HooksfunctiongetSorted(items){returnitems.slice().sort();}
Это гарантирует, что ваш код сможет вызвать эту регулярную функцию в любом месте, включая условия:
12345678
functionList({items,shouldSort}){letdisplayedItems=items;if(shouldSort){// ✅ It's ok to call getSorted() conditionally because it's not a HookdisplayedItems=getSorted(items);}// ...}
Вы должны дать префикс use функции (и таким образом сделать ее хуком), если она использует хотя бы один хук внутри себя:
1234
// ✅ Good: A Hook that uses other HooksfunctionuseAuth(){returnuseContext(Auth);}
Технически, React этого не делает. В принципе, вы можете сделать хук, который не вызывает другие хуки. Это часто запутывает и ограничивает, поэтому лучше избегать такого шаблона. Однако в редких случаях это может быть полезно. Например, возможно, ваша функция сейчас не использует никаких хуков, но в будущем вы планируете добавить в нее несколько вызовов хуков. Тогда имеет смысл назвать ее с префиксом use:
123456
// ✅ Good: A Hook that will likely use some other Hooks laterfunctionuseAuth(){// TODO: Replace with this line when authentication is implemented:// return useContext(Auth);returnTEST_USER;}
Тогда компоненты не смогут вызывать его условно. Это станет важным, когда вы действительно добавите вызовы Hook внутри. Если вы не планируете использовать в нем хуки (сейчас или позже), не делайте его хуком.
Пользовательские хуки позволяют вам делиться логикой состояния, а не самим состоянием {/custom-hooks-let-you-share-stateful-logic-not-state-itself/}¶
В предыдущем примере, когда вы включали и выключали сеть, оба компонента обновлялись вместе. Однако неправильно думать, что одна переменная состояния isOnline разделяется между ними. Посмотрите на этот код:
Это две совершенно независимые переменные состояния и Effects! Они имеют одинаковое значение в одно и то же время, потому что вы синхронизировали их с одним и тем же внешним значением (включена ли сеть).
Чтобы лучше проиллюстрировать это, нам понадобится другой пример. Рассмотрим компонент Form:
Вот почему это работает как объявление двух отдельных переменных состояния!
Настроенные хуки позволяют вам делиться логикой состояния, но не самим состоянием.* Каждый вызов хука полностью независим от любого другого вызова того же хука. Вот почему две вышеприведенные песочницы полностью эквивалентны. Если хотите, прокрутите страницу назад и сравните их. Поведение до и после извлечения пользовательского хука идентично.
Если вам нужно разделить само состояние между несколькими компонентами, вместо этого lift it up and pass it down.
Передача реактивных значений между хуками {/passing-reactive-values-between-hooks/}¶
Код внутри ваших пользовательских хуков будет выполняться заново при каждом повторном рендеринге компонента. Вот почему, как и компоненты, пользовательские хуки должны быть чистыми. Думайте о коде пользовательских хуков как о части тела вашего компонента!
Поскольку пользовательские хуки перерисовываются вместе с вашим компонентом, они всегда получают последние реквизиты и состояние. Чтобы понять, что это значит, рассмотрим пример с чатом. Измените URL сервера или чата:
exportfunctioncreateConnection({serverUrl,roomId}){// A real implementation would actually connect to the serverif(typeofserverUrl!=='string'){throwError('Expected serverUrl to be a string. Received: '+serverUrl);}if(typeofroomId!=='string'){throwError('Expected roomId to be a string. Received: '+roomId);}letintervalId;letmessageCallback;return{connect(){console.log('✅ Connecting to "'+roomId+'" room at '+serverUrl+'...');clearInterval(intervalId);intervalId=setInterval(()=>{if(messageCallback){if(Math.random()>0.5){messageCallback('hey');}else{messageCallback('lol');}}},3000);},disconnect(){clearInterval(intervalId);messageCallback=null;console.log('❌ Disconnected from "'+roomId+'" room at '+serverUrl+'');},on(event,callback){if(messageCallback){throwError('Cannot add the handler twice.');}if(event!=='message'){throwError('Only "message" event is supported.');}messageCallback=callback;},};}
Когда вы изменяете serverUrl или roomId, Эффект ["реагирует" на ваши изменения] (/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) и пересинхронизируется. По сообщениям консоли вы можете определить, что чат переподключается каждый раз, когда вы изменяете зависимости вашего Эффекта.
Теперь переместите код эффекта в пользовательский хук:
exportfunctioncreateConnection({serverUrl,roomId}){// A real implementation would actually connect to the serverif(typeofserverUrl!=='string'){throwError('Expected serverUrl to be a string. Received: '+serverUrl);}if(typeofroomId!=='string'){throwError('Expected roomId to be a string. Received: '+roomId);}letintervalId;letmessageCallback;return{connect(){console.log('✅ Connecting to "'+roomId+'" room at '+serverUrl+'...');clearInterval(intervalId);intervalId=setInterval(()=>{if(messageCallback){if(Math.random()>0.5){messageCallback('hey');}else{messageCallback('lol');}}},3000);},disconnect(){clearInterval(intervalId);messageCallback=null;console.log('❌ Disconnected from "'+roomId+'" room at '+serverUrl+'');},on(event,callback){if(messageCallback){throwError('Cannot add the handler twice.');}if(event!=='message'){throwError('Only "message" event is supported.');}messageCallback=callback;},};}
Каждый раз, когда ваш компонент ChatRoom перерисовывается, он передает последние значения roomId и serverUrl вашему Hook. Именно поэтому ваш Эффект повторно подключается к чату, когда их значения меняются после повторного рендеринга. (Если вы когда-нибудь работали с программами для обработки аудио или видео, то такое построение цепочки хуков может напомнить вам построение цепочки визуальных или звуковых эффектов. Это как если бы выход useState "вливался" во вход useChatRoom).
Передача обработчиков событий пользовательским хукам {/passing-event-handlers-to-custom-hooks/}¶
\<Wip>
Этот раздел описывает экспериментальный API, который еще не был выпущен в стабильной версии React.
\</Wip>
Когда вы начнете использовать useChatRoom в большем количестве компонентов, вы, возможно, захотите позволить компонентам настраивать его поведение. Например, в настоящее время логика того, что делать, когда приходит сообщение, жестко закодирована внутри Hook:
Чтобы это работало, измените свой пользовательский хук так, чтобы он принимал onReceiveMessage в качестве одной из именованных опций:
1 2 3 4 5 6 7 8 9101112131415161718
exportfunctionuseChatRoom({serverUrl,roomId,onReceiveMessage,}){useEffect(()=>{constoptions={serverUrl:serverUrl,roomId:roomId,};constconnection=createConnection(options);connection.connect();connection.on('message',(msg)=>{onReceiveMessage(msg);});return()=>connection.disconnect();},[roomId,serverUrl,onReceiveMessage]);// ✅ All dependencies declared}
Это будет работать, но есть еще одно улучшение, которое вы можете сделать, когда ваш пользовательский Hook принимает обработчики событий.
import{useEffect,useEffectEvent}from'react';// ...exportfunctionuseChatRoom({serverUrl,roomId,onReceiveMessage,}){constonMessage=useEffectEvent(onReceiveMessage);useEffect(()=>{constoptions={serverUrl:serverUrl,roomId:roomId,};constconnection=createConnection(options);connection.connect();connection.on('message',(msg)=>{onMessage(msg);});return()=>connection.disconnect();},[roomId,serverUrl]);// ✅ All dependencies declared}
Теперь чат не будет подключаться заново каждый раз, когда компонент ChatRoom перерисовывается. Вот полностью рабочий демонстрационный пример передачи обработчика события пользовательскому Hook, с которым вы можете поиграть:
exportfunctioncreateConnection({serverUrl,roomId}){// A real implementation would actually connect to the serverif(typeofserverUrl!=='string'){throwError('Expected serverUrl to be a string. Received: '+serverUrl);}if(typeofroomId!=='string'){throwError('Expected roomId to be a string. Received: '+roomId);}letintervalId;letmessageCallback;return{connect(){console.log('✅ Connecting to "'+roomId+'" room at '+serverUrl+'...');clearInterval(intervalId);intervalId=setInterval(()=>{if(messageCallback){if(Math.random()>0.5){messageCallback('hey');}else{messageCallback('lol');}}},3000);},disconnect(){clearInterval(intervalId);messageCallback=null;console.log('❌ Disconnected from "'+roomId+'" room at '+serverUrl+'');},on(event,callback){if(messageCallback){throwError('Cannot add the handler twice.');}if(event!=='message'){throwError('Only "message" event is supported.');}messageCallback=callback;},};}
Обратите внимание, что вам больше не нужно знать, как работает useChatRoom, чтобы использовать его. Вы можете добавить его в любой другой компонент, передать любые другие параметры, и он будет работать точно так же. В этом и заключается сила пользовательских хуков.
Когда использовать пользовательские хуки {/when-to-use-custom-hooks/}¶
Вам не нужно извлекать пользовательский хук для каждого маленького дублирующегося кусочка кода. Некоторое дублирование вполне нормально. Например, извлечение хука useFormInput для обертывания одного вызова useState, как это было ранее, вероятно, не нужно.
Однако всякий раз, когда вы пишете Эффект, подумайте, не будет ли яснее, если его также обернуть в пользовательский Хук. Эффекты не должны требоваться очень часто, поэтому если вы пишете эффект, это означает, что вам нужно "выйти за пределы React", чтобы синхронизироваться с какой-то внешней системой или сделать что-то, для чего у React нет встроенного API. Обернув это в пользовательский хук, вы можете точно передать свое намерение и то, как данные проходят через него.
Например, рассмотрим компонент ShippingForm, который отображает два выпадающих списка: один показывает список городов, а другой - список областей в выбранном городе. Вы можете начать с кода, который выглядит следующим образом:
functionShippingForm({country}){const[cities,setCities]=useState(null);// This Effect fetches cities for a countryuseEffect(()=>{letignore=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);// This Effect fetches areas for the selected cityuseEffect(()=>{if(city){letignore=false;fetch(`/api/areas?city=${city}`).then(response=>response.json()).then(json=>{if(!ignore){setAreas(json);}});return()=>{ignore=true;};}},[city]);// ...
Хотя этот код довольно повторяющийся, правильно держать эти Эффекты отдельно друг от друга. Они синхронизируют две разные вещи, поэтому не стоит объединять их в один Эффект. Вместо этого вы можете упростить компонент ShippingForm выше, извлекая общую логику между ними в свой собственный хук useData:
Извлечение пользовательского Hook делает поток данных явным. Вы вводите url и получаете data. "Пряча" свой Эффект внутри useData, вы также не позволяете кому-то, работающему над компонентом ShippingForm, добавить к нему ненужные зависимости. Со временем большая часть Эффектов вашего приложения будет находиться в пользовательских Hooks.
Сосредоточьте ваши пользовательские хуки на конкретных высокоуровневых сценариях использования {/keep-your-custom-hooks-focused-on-concrete-high-level-use-cases/}¶
Начните с выбора имени вашего пользовательского хука. Если вы не можете выбрать четкое имя, это может означать, что ваш Эффект слишком связан с остальной логикой вашего компонента и еще не готов к извлечению.
В идеале название вашего пользовательского хука должно быть достаточно понятным, чтобы даже человек, который не часто пишет код, мог догадаться, что делает ваш пользовательский хук, что он принимает и что возвращает:
✅ useData(url)
✅ useImpressionLog(eventName, extraData)
✅ useChatRoom(options).
Когда вы синхронизируетесь с внешней системой, ваше пользовательское имя Hook может быть более техническим и использовать жаргон, характерный для этой системы. Это хорошо, если оно будет понятно человеку, знакомому с этой системой:
✅ useMediaQuery(query).
✅ useSocket(url)
✅ useIntersectionObserver(ref, options)
**Избегайте создания и использования пользовательских хуков "жизненного цикла", которые действуют как альтернативы и удобные обертки для самого API useEffect:
🔴 useMount(fn)
🔴 useEffectOnce(fn)
🔴 useUpdateEffect(fn).
Например, этот хук useMount пытается обеспечить выполнение некоторого кода только "при монтировании":
functionChatRoom({roomId}){const[serverUrl,setServerUrl]=useState('https://localhost:1234');// 🔴 Avoid: using custom "lifecycle" HooksuseMount(()=>{constconnection=createConnection({roomId,serverUrl,});connection.connect();post('/analytics/event',{eventName:'visit_chat',});});// ...}// 🔴 Avoid: creating custom "lifecycle" HooksfunctionuseMount(fn){useEffect(()=>{fn();},[]);// 🔴 React Hook useEffect has a missing dependency: 'fn'}
Пользовательские хуки "жизненного цикла", такие как useMount, плохо вписываются в парадигму React. Например, в этом примере кода есть ошибка (он не "реагирует" на изменения roomId или serverUrl), но линтер не предупредит вас об этом, потому что линтер проверяет только прямые вызовы useEffect. Он не будет знать о вашем хуке.
Если вы пишете эффект, начните с прямого использования React API:
1 2 3 4 5 6 7 8 910111213141516171819202122232425
functionChatRoom({roomId}){const[serverUrl,setServerUrl]=useState('https://localhost:1234');// ✅ Good: two raw Effects separated by purposeuseEffect(()=>{constconnection=createConnection({serverUrl,roomId,});connection.connect();return()=>connection.disconnect();},[serverUrl,roomId]);useEffect(()=>{post('/analytics/event',{eventName:'visit_chat',roomId,});},[roomId]);// ...}
Затем вы можете (но не обязаны) извлекать пользовательские хуки для различных высокоуровневых сценариев использования:
1 2 3 4 5 6 7 8 910
functionChatRoom({roomId}){const[serverUrl,setServerUrl]=useState('https://localhost:1234');// ✅ Great: custom Hooks named after their purposeuseChatRoom({serverUrl,roomId});useImpressionLog('visit_chat',{roomId});// ...}
**Например, useChatRoom(options) может только подключаться к чату, а useImpressionLog(eventName, extraData) может только отправлять журнал впечатлений аналитику. Если ваш пользовательский API Hook не ограничивает сценарии использования и является очень абстрактным, в долгосрочной перспективе он, скорее всего, создаст больше проблем, чем решит.
Пользовательские хуки помогают перейти на лучшие паттерны {/custom-hooks-help-you-migrate-to-better-patterns/}¶
Эффекты - это ["аварийный люк"] (/learn/escape-hatches): вы используете их, когда вам нужно "выйти за пределы React" и когда нет лучшего встроенного решения для вашего случая использования. Со временем цель команды React - сократить количество Эффектов в вашем приложении до минимума, предоставляя более конкретные решения для более конкретных проблем. Обертывание ваших Эффектов в пользовательские Hooks упрощает обновление вашего кода, когда эти решения становятся доступными.
В приведенном выше примере useOnlineStatus реализован с помощью пары useState и useEffect. Однако это не лучшее из возможных решений. Оно не учитывает ряд побочных ситуаций. Например, предполагается, что когда компонент монтируется, isOnline уже true, но это может быть неверно, если сеть уже отключилась. Вы можете использовать API браузера navigator.onLine для проверки этого, но его использование напрямую не будет работать на сервере для генерации начального HTML. Короче говоря, этот код можно улучшить.
К счастью, React 18 включает специальный API под названием useSyncExternalStore, который решает все эти проблемы за вас. Вот как выглядит ваш хук useOnlineStatus, переписанный для использования преимуществ этого нового API:
import{useSyncExternalStore}from'react';functionsubscribe(callback){window.addEventListener('online',callback);window.addEventListener('offline',callback);return()=>{window.removeEventListener('online',callback);window.removeEventListener('offline',callback);};}exportfunctionuseOnlineStatus(){returnuseSyncExternalStore(subscribe,()=>navigator.onLine,// How to get the value on the client()=>true// How to get the value on the server);}
Обратите внимание, что вам не нужно было менять ни один из компонентов, чтобы осуществить этот переход:
Это еще одна причина, по которой обертывание эффектов в пользовательские хуки часто оказывается полезным:
Вы делаете поток данных к эффектам и от них очень явным.
Вы позволяете компонентам сосредоточиться на замысле, а не на точной реализации ваших эффектов.
Когда React добавляет новые возможности, вы можете удалить эти Эффекты, не меняя ни одного из своих компонентов.
По аналогии с системой проектирования вы можете найти полезным начать извлекать общие идиомы из компонентов вашего приложения в пользовательские Hooks. Это позволит сфокусировать код ваших компонентов на замысле и избежать частого написания сырых эффектов. Сообщество React поддерживает множество отличных пользовательских Hooks.
Предоставит ли React какое-либо встроенное решение для получения данных? {/will-react-provide-any-built-in-solution-for-data-fetching/}¶
Мы все еще прорабатываем детали, но ожидаем, что в будущем вы будете писать выборку данных следующим образом:
1234567
import{use}from'react';// Not available yet!functionShippingForm({country}){constcities=use(fetch(`/api/cities?country=${country}`));const[city,setCity]=useState(null);constareas=city?use(fetch(`/api/areas?city=${city}`)):null;// ...
Если вы используете в своем приложении пользовательские хуки, такие как useData выше, то для перехода на рекомендуемый подход потребуется меньше изменений, чем если бы вы писали необработанные Эффекты в каждом компоненте вручную. Однако старый подход все еще будет работать, так что если вам нравится писать необработанные Эффекты, вы можете продолжать это делать.
Существует более одного способа сделать это {/there-is-more-than-one-way-to-do-it/}¶
Допустим, вы хотите реализовать анимацию затухания с нуля, используя API браузера requestAnimationFrame. Вы можете начать с Эффекта, который устанавливает цикл анимации. Во время каждого кадра анимации вы можете изменять непрозрачность узла DOM, который вы держите в ссылке, пока она не достигнет 1. Ваш код может начинаться следующим образом:
import{useState,useEffect,useRef}from'react';functionWelcome(){constref=useRef(null);useEffect(()=>{constduration=1000;constnode=ref.current;letstartTime=performance.now();letframeId=null;functiononFrame(now){consttimePassed=now-startTime;constprogress=Math.min(timePassed/duration,1);onProgress(progress);if(progress<1){// We still have more frames to paintframeId=requestAnimationFrame(onFrame);}}functiononProgress(progress){node.style.opacity=progress;}functionstart(){onProgress(0);startTime=performance.now();frameId=requestAnimationFrame(onFrame);}functionstop(){cancelAnimationFrame(frameId);startTime=null;frameId=null;}start();return()=>stop();},[]);return(<h1className="welcome"ref={ref}>Welcome</h1>);}exportdefaultfunctionApp(){const[show,setShow]=useState(false);return(<><buttononClick={()=>setShow(!show)}>{show?'Remove':'Show'}</button><hr/>{show&&<Welcome/>}</>);}
import{useEffect}from'react';exportfunctionuseFadeIn(ref,duration){useEffect(()=>{constnode=ref.current;letstartTime=performance.now();letframeId=null;functiononFrame(now){consttimePassed=now-startTime;constprogress=Math.min(timePassed/duration,1);onProgress(progress);if(progress<1){// We still have more frames to paintframeId=requestAnimationFrame(onFrame);}}functiononProgress(progress){node.style.opacity=progress;}functionstart(){onProgress(0);startTime=performance.now();frameId=requestAnimationFrame(onFrame);}functionstop(){cancelAnimationFrame(frameId);startTime=null;frameId=null;}start();return()=>stop();},[ref,duration]);}
Можно оставить код useFadeIn как есть, но можно и рефакторить его. Например, вы можете извлечь логику установки анимационного цикла из useFadeIn в пользовательский хук useAnimationLoop:
{"dependencies":{"react":"experimental","react-dom":"experimental","react-scripts":"latest"},"scripts":{"start":"react-scripts start","build":"react-scripts build","test":"react-scripts test --env=jsdom","eject":"react-scripts eject"}}
Однако вы не обязаны это делать. Как и в случае с обычными функциями, в конечном итоге вы сами решаете, где проводить границы между различными частями вашего кода. Вы также можете использовать совершенно другой подход. Вместо того чтобы держать логику в Effect, вы можете перенести большую часть императивной логики внутрь JavaScript class:
exportclassFadeInAnimation{constructor(node){this.node=node;}start(duration){this.duration=duration;this.onProgress(0);this.startTime=performance.now();this.frameId=requestAnimationFrame(()=>this.onFrame());}onFrame(){consttimePassed=performance.now()-this.startTime;constprogress=Math.min(timePassed/this.duration,1);this.onProgress(progress);if(progress===1){this.stop();}else{// We still have more frames to paintthis.frameId=requestAnimationFrame(()=>this.onFrame());}}onProgress(progress){this.node.style.opacity=progress;}stop(){cancelAnimationFrame(this.frameId);this.startTime=null;this.frameId=null;this.duration=0;}}
Эффекты позволяют подключать React к внешним системам. Чем больше координации между эффектами требуется (например, для цепочки нескольких анимаций), тем больше смысла извлекать эту логику из эффектов и хуков полностью, как в песочнице выше. Тогда извлеченный вами код станет "внешней системой". Это позволяет вашим Эффектам оставаться простыми, потому что им нужно только посылать сообщения системе, которую вы перенесли за пределы React.
Приведенные выше примеры предполагают, что логика затухания должна быть написана на JavaScript. Однако эту конкретную анимацию затухания проще и гораздо эффективнее реализовать с помощью простой CSS-анимации:
Пользовательские хуки позволяют обмениваться логикой между компонентами.
Имена пользовательских хуков должны начинаться с use и заканчиваться заглавной буквой.
Пользовательские хуки передают только логику состояния, но не само состояние.
Вы можете передавать реактивные значения от одного хука к другому, и они остаются актуальными.
Все хуки перезапускаются каждый раз, когда ваш компонент перерендеривается.
Код ваших пользовательских хуков должен быть чистым, как и код вашего компонента.
Оберните обработчики событий, получаемые пользовательскими хуками, в события Effect Events.
Не создавайте пользовательские хуки типа useMount. Их назначение должно быть конкретным.
Вам решать, как и где выбирать границы вашего кода.
\</Recap>
\<Проблемы>
Extract a useCounter Hook {/extract-a-usecounter-hook/}¶
Этот компонент использует переменную состояния и Эффект для отображения числа, которое увеличивается каждую секунду. Извлеките эту логику в пользовательский хук под названием useCounter. Ваша цель состоит в том, чтобы реализация компонента Counter выглядела именно так:
В этом примере есть переменная состояния delay, управляемая ползунком, но ее значение не используется. Передайте значение delay в ваш пользовательский хук useCounter, и измените хук useCounter, чтобы он использовал переданную delay вместо жесткого кодирования 1000 мс.
Передайте задержку вашему хуку с помощью useCounter(delay). Затем, внутри хука, используйте delay вместо жестко заданного значения 1000. Вам нужно будет добавить delay в зависимости вашего Эффекта. Это гарантирует, что изменение delay сбросит интервал.
Извлечение useInterval из useCounter {/extract-useinterval-out-of-usecounter/}¶
В настоящее время ваш хук useCounter делает две вещи. Он устанавливает интервал, а также увеличивает переменную состояния при каждом тике интервала. Выделите логику, которая устанавливает интервал, в отдельный хук под названием useInterval. Он должен принимать два аргумента: обратный вызов onTick и delay. После этого изменения ваша реализация useCounter должна выглядеть следующим образом:
Компонент App вызывает useCounter, который вызывает useInterval для обновления счетчика каждую секунду. Но компонент Appтакже вызывает useInterval для случайного обновления цвета фона страницы каждые две секунды.
По какой-то причине обратный вызов, обновляющий фон страницы, никогда не выполняется. Добавьте несколько журналов внутри useInterval:
1 2 3 4 5 6 7 8 91011121314
useEffect(()=>{console.log('✅ Setting up an interval with delay ',delay);constid=setInterval(onTick,delay);return()=>{console.log('❌ Clearing an interval with delay ',delay);clearInterval(id);};},[onTick,delay]);
Совпадают ли журналы с тем, что вы ожидаете? Если некоторые из ваших Эффектов, кажется, пересинхронизируются без необходимости, можете ли вы предположить, какая зависимость вызывает это? Есть ли способ удалить эту зависимость из вашего Эффекта?
После устранения проблемы, вы должны ожидать, что фон страницы будет обновляться каждые две секунды.
\<Hint>
Похоже, что ваш хук useInterval принимает в качестве аргумента слушатель событий. Можете ли вы придумать, как обернуть этот слушатель событий так, чтобы он не был зависим от вашего Effect?
\</Hint>
1 2 3 4 5 6 7 8 910111213
{"dependencies":{"react":"experimental","react-dom":"experimental","react-scripts":"latest"},"scripts":{"start":"react-scripts start","build":"react-scripts build","test":"react-scripts test --env=jsdom","eject":"react-scripts eject"}}
Внутри useInterval оберните обратный вызов тика в событие эффекта, как вы делали ранее на этой странице.
Это позволит вам опустить onTick из зависимостей вашего Эффекта. Эффект не будет пересинхронизироваться при каждом повторном рендере компонента, поэтому интервал изменения цвета фона страницы не будет сбрасываться каждую секунду, прежде чем успеет сработать.
Благодаря этому изменению оба интервала работают, как и ожидалось, и не мешают друг другу:
1 2 3 4 5 6 7 8 910111213
{"dependencies":{"react":"experimental","react-dom":"experimental","react-scripts":"latest"},"scripts":{"start":"react-scripts start","build":"react-scripts build","test":"react-scripts test --env=jsdom","eject":"react-scripts eject"}}
Реализация шагающего движения {/implement-a-staggering-movement/}¶
В этом примере хук usePointerPosition() отслеживает текущую позицию указателя. Попробуйте переместить курсор или палец по области предварительного просмотра и увидите, как красная точка следует за вашим движением. Ее положение сохраняется в переменной pos1.
На самом деле, в данный момент отображается пять (!) различных красных точек. Вы не видите их, потому что в настоящее время все они отображаются в одном и том же положении. Это то, что вам нужно исправить. Вместо этого вы хотите реализовать "ступенчатое" движение: каждая точка должна "следовать" по пути предыдущей точки. Например, если вы быстро перемещаете курсор, первая точка должна следовать за ним немедленно, вторая точка должна следовать за первой с небольшой задержкой, третья точка должна следовать за второй и так далее.
Вам необходимо реализовать пользовательский хук useDelayedValue. Его текущая реализация возвращает предоставленное ему значение. Вместо этого вы хотите возвращать значение, полученное от задержки миллисекунды назад. Для этого вам может понадобиться некоторое состояние и Эффект.
После реализации useDelayedValue, вы должны увидеть, как точки движутся друг за другом.
\<Намек>
Вам нужно будет хранить delayedValue как переменную состояния внутри вашего пользовательского Hook. Когда значение изменится, вы захотите запустить Эффект. Этот Эффект должен обновить delayedValue после задержки. Возможно, вам будет полезно вызвать setTimeout.
Нужно ли очистить этот Эффект? Почему или почему нет?
Вот рабочая версия. Вы храните delayedValue как переменную состояния. Когда значение обновляется, ваш Effect планирует таймаут для обновления отложенного значения. Вот почему delayedValue всегда "отстает" от фактического value.
Обратите внимание, что этот Эффект не нуждается в очистке. Если бы вы вызвали clearTimeout в функции очистки, то при каждом изменении значения сбрасывался бы уже запланированный таймаут. Чтобы движение было непрерывным, нужно, чтобы срабатывали все таймауты.