From ff54be5215913194ebd388b39808b070516fc196 Mon Sep 17 00:00:00 2001
From: "translate-react-bot[bot]"
<251169733+translate-react-bot[bot]@users.noreply.github.com>
Date: Thu, 14 May 2026 15:30:52 +0000
Subject: [PATCH 1/3] =?UTF-8?q?docs:=20translate=20`reusing-logic-with-cus?=
=?UTF-8?q?tom-hooks.md`=20to=20=D0=A0=D1=83=D1=81=D1=81=D0=BA=D0=B8=D0=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../learn/reusing-logic-with-custom-hooks.md | 1679 +----------------
1 file changed, 79 insertions(+), 1600 deletions(-)
diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md
index b6562e2df5..466c7bac4f 100644
--- a/src/content/learn/reusing-logic-with-custom-hooks.md
+++ b/src/content/learn/reusing-logic-with-custom-hooks.md
@@ -1,30 +1,30 @@
---
-title: 'Reusing Logic with Custom Hooks'
+title: 'Повторное использование логики с пользовательскими хуками'
---
-
+```html
-React comes with several built-in Hooks like `useState`, `useContext`, and `useEffect`. Sometimes, you'll wish that there was a Hook for some more specific purpose: for example, to fetch data, to keep track of whether the user is online, or to connect to a chat room. You might not find these Hooks in React, but you can create your own Hooks for your application's needs.
+React предоставляет несколько встроенных хуков, таких как `useState`, `useContext` и `useEffect`. Иногда вам захочется, чтобы был хук для какой-то более конкретной цели: например, для получения данных, для отслеживания того, находится ли пользователь в сети, или для подключения к чат-комнате. Вы можете не найти эти хуки в React, но вы можете создать свои собственные хуки для нужд вашего приложения.
-- What custom Hooks are, and how to write your own
-- How to reuse logic between components
-- How to name and structure your custom Hooks
-- When and why to extract custom Hooks
+- Что такое пользовательские хуки и как писать свои собственные
+- Как повторно использовать логику между компонентами
+- Как называть и структурировать свои пользовательские хуки
+- Когда и почему следует извлекать пользовательские хуки
-## Custom Hooks: Sharing logic between components {/*custom-hooks-sharing-logic-between-components*/}
+## Пользовательские хуки: совместное использование логики между компонентами {/*custom-hooks-sharing-logic-between-components*/}
-Imagine you're developing an app that heavily relies on the network (as most apps do). You want to warn the user if their network connection has accidentally gone off while they were using your app. How would you go about it? It seems like you'll need two things in your component:
+Представьте, что вы разрабатываете приложение, которое сильно зависит от сети (как и большинство приложений). Вы хотите предупредить пользователя, если его сетевое соединение случайно отключилось во время использования вашего приложения. Как бы вы это сделали? Кажется, вам понадобятся две вещи в вашем компоненте:
-1. A piece of state that tracks whether the network is online.
-2. An Effect that subscribes to the global [`online`](https://developer.mozilla.org/en-US/docs/Web/API/Window/online_event) and [`offline`](https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event) events, and updates that state.
+1. Часть состояния, которая отслеживает, находится ли сеть в сети.
+2. Эффект, который подписывается на глобальные события [`online`](https://developer.mozilla.org/ru/docs/Web/API/Window/online_event) и [`offline`](https://developer.mozilla.org/ru/docs/Web/API/Window/offline_event) и обновляет это состояние.
-This will keep your component [synchronized](/learn/synchronizing-with-effects) with the network status. You might start with something like this:
+Это будет держать ваш компонент [синхронизированным](/learn/synchronizing-with-effects) со статусом сети. Вы можете начать с чего-то вроде этого:
@@ -54,11 +54,11 @@ export default function StatusBar() {
-Try turning your network on and off, and notice how this `StatusBar` updates in response to your actions.
+Попробуйте включать и выключать сеть и обратите внимание, как этот `StatusBar` обновляется в ответ на ваши действия.
-Now imagine you *also* want to use the same logic in a different component. You want to implement a Save button that will become disabled and show "Reconnecting..." instead of "Save" while the network is off.
+Теперь представьте, что вы *также* хотите использовать ту же логику в другом компоненте. Вы хотите реализовать кнопку «Сохранить», которая будет отключаться и отображать «Переподключение...» вместо «Сохранить», пока сеть отключена.
-To start, you can copy and paste the `isOnline` state and the Effect into `SaveButton`:
+Для начала вы можете скопировать и вставить состояние `isOnline` и эффект в `SaveButton`:
@@ -96,13 +96,13 @@ export default function SaveButton() {
-Verify that, if you turn off the network, the button will change its appearance.
+Убедитесь, что при отключении сети кнопка изменит свой внешний вид.
-These two components work fine, but the duplication in logic between them is unfortunate. It seems like even though they have different *visual appearance,* you want to reuse the logic between them.
+Эти два компонента работают нормально, но дублирование логики между ними вызывает сожаление. Кажется, что, хотя у них разный *внешний вид*, вы хотите повторно использовать логику между ними.
-### Extracting your own custom Hook from a component {/*extracting-your-own-custom-hook-from-a-component*/}
+### Извлечение собственного пользовательского хука из компонента {/*extracting-your-own-custom-hook-from-a-component*/}
-Imagine for a moment that, similar to [`useState`](/reference/react/useState) and [`useEffect`](/reference/react/useEffect), there was a built-in `useOnlineStatus` Hook. Then both of these components could be simplified and you could remove the duplication between them:
+Представьте себе на мгновение, что, аналогично [`useState`](/reference/react/useState) и [`useEffect`](/reference/react/useEffect), существует встроенный хук `useOnlineStatus`. Тогда оба этих компонента можно было бы упростить, и вы могли бы удалить дублирование между ними:
```js {2,7}
function StatusBar() {
@@ -125,7 +125,7 @@ function SaveButton() {
}
```
-Although there is no such built-in Hook, you can write it yourself. Declare a function called `useOnlineStatus` and move all the duplicated code into it from the components you wrote earlier:
+Хотя такого встроенного хука нет, вы можете написать его сами. Объявите функцию с именем `useOnlineStatus` и переместите в нее весь дублирующийся код из компонентов, которые вы написали ранее:
```js {2-16}
function useOnlineStatus() {
@@ -148,7 +148,7 @@ function useOnlineStatus() {
}
```
-At the end of the function, return `isOnline`. This lets your components read that value:
+В конце функции верните `isOnline`. Это позволяет вашим компонентам читать это значение:
@@ -209,89 +209,89 @@ export function useOnlineStatus() {
-Verify that switching the network on and off updates both components.
+Убедитесь, что включение и выключение сети обновляет оба компонента.
-Now your components don't have as much repetitive logic. **More importantly, the code inside them describes *what they want to do* (use the online status!) rather than *how to do it* (by subscribing to the browser events).**
+Теперь в ваших компонентах не так много повторяющейся логики. **Что еще важнее, код внутри них описывает *что они хотят сделать* (использовать статус сети!), а не *как это сделать* (подписавшись на события браузера).**
-When you extract logic into custom Hooks, you can hide the gnarly details of how you deal with some external system or a browser API. The code of your components expresses your intent, not the implementation.
+Когда вы извлекаете логику в пользовательские хуки, вы можете скрыть неприятные детали того, как вы имеете дело с какой-либо внешней системой или API браузера. Код ваших компонентов выражает ваше намерение, а не реализацию.
-### Hook names always start with `use` {/*hook-names-always-start-with-use*/}
+### Имена хуков всегда начинаются с `use` {/*hook-names-always-start-with-use*/}
-React applications are built from components. Components are built from Hooks, whether built-in or custom. You'll likely often use custom Hooks created by others, but occasionally you might write one yourself!
+Приложения React состоят из компонентов. Компоненты состоят из хуков, будь то встроенные или пользовательские. Вы, вероятно, часто будете использовать пользовательские хуки, созданные другими, но иногда вы можете написать один сами!
-You must follow these naming conventions:
+Вы должны следовать этим соглашениям об именах:
-1. **React component names must start with a capital letter,** like `StatusBar` and `SaveButton`. React components also need to return something that React knows how to display, like a piece of JSX.
-2. **Hook names must start with `use` followed by a capital letter,** like [`useState`](/reference/react/useState) (built-in) or `useOnlineStatus` (custom, like earlier on the page). Hooks may return arbitrary values.
+1. **Имена компонентов React должны начинаться с заглавной буквы,** например, `StatusBar` и `SaveButton`. Компоненты React также должны возвращать что-то, что React умеет отображать, например, фрагмент JSX.
+2. **Имена хуков должны начинаться с `use`, за которым следует заглавная буква,** например, [`useState`](/reference/react/useState) (встроенный) или `useOnlineStatus` (пользовательский, как ранее на странице). Хуки могут возвращать произвольные значения.
-This convention guarantees that you can always look at a component and know where its state, Effects, and other React features might "hide". For example, if you see a `getColor()` function call inside your component, you can be sure that it can't possibly contain React state inside because its name doesn't start with `use`. However, a function call like `useOnlineStatus()` will most likely contain calls to other Hooks inside!
+Это соглашение гарантирует, что вы всегда можете посмотреть на компонент и узнать, где могут «скрываться» его состояние, эффекты и другие функции React. Например, если вы видите вызов функции `getColor()` внутри вашего компонента, вы можете быть уверены, что она никак не может содержать состояние React, потому что ее имя не начинается с `use`. Однако вызов функции, такой как `useOnlineStatus()`, скорее всего, будет содержать вызовы других хуков внутри!
-If your linter is [configured for React,](/learn/editor-setup#linting) it will enforce this naming convention. Scroll up to the sandbox above and rename `useOnlineStatus` to `getOnlineStatus`. Notice that the linter won't allow you to call `useState` or `useEffect` inside of it anymore. Only Hooks and components can call other Hooks!
+Если ваш линтер [настроен для React,](/learn/editor-setup#linting) он будет применять это соглашение об именах. Прокрутите вверх к песочнице выше и переименуйте `useOnlineStatus` в `getOnlineStatus`. Обратите внимание, что линтер больше не позволит вам вызывать `useState` или `useEffect` внутри него. Только хуки и компоненты могут вызывать другие хуки!
-#### Should all functions called during rendering start with the use prefix? {/*should-all-functions-called-during-rendering-start-with-the-use-prefix*/}
+#### Должны ли все функции, вызываемые во время рендеринга, начинаться с префикса use? {/*should-all-functions-called-during-rendering-start-with-the-use-prefix*/}
-No. Functions that don't *call* Hooks don't need to *be* Hooks.
+Нет. Функции, которые не *вызывают* хуки, не должны *быть* хуками.
-If your function doesn't call any Hooks, avoid the `use` prefix. Instead, write it as a regular function *without* the `use` prefix. For example, `useSorted` below doesn't call Hooks, so call it `getSorted` instead:
+Если ваша функция не вызывает никаких хуков, избегайте префикса `use`. Вместо этого напишите ее как обычную функцию *без* префикса `use`. Например, `useSorted` ниже не вызывает хуки, поэтому вместо этого вызовите ее `getSorted`:
```js
-// 🔴 Avoid: A Hook that doesn't use Hooks
+// 🔴 Избегайте: хук, который не использует хуки
function useSorted(items) {
return items.slice().sort();
}
-// ✅ Good: A regular function that doesn't use Hooks
+// ✅ Хорошо: обычная функция, которая не использует хуки
function getSorted(items) {
return items.slice().sort();
}
```
-This ensures that your code can call this regular function anywhere, including conditions:
+Это гарантирует, что ваш код может вызывать эту обычную функцию в любом месте, включая условия:
```js
function List({ items, shouldSort }) {
let displayedItems = items;
if (shouldSort) {
- // ✅ It's ok to call getSorted() conditionally because it's not a Hook
+ // ✅ Можно условно вызывать getSorted(), потому что это не хук
displayedItems = getSorted(items);
}
// ...
}
```
-You should give `use` prefix to a function (and thus make it a Hook) if it uses at least one Hook inside of it:
+Вы должны дать префикс `use` функции (и, таким образом, сделать ее хуком), если она использует хотя бы один хук внутри себя:
```js
-// ✅ Good: A Hook that uses other Hooks
+// ✅ Хорошо: хук, который использует другие хуки
function useAuth() {
return useContext(Auth);
}
```
-Technically, this isn't enforced by React. In principle, you could make a Hook that doesn't call other Hooks. This is often confusing and limiting so it's best to avoid that pattern. However, there may be rare cases where it is helpful. For example, maybe your function doesn't use any Hooks right now, but you plan to add some Hook calls to it in the future. Then it makes sense to name it with the `use` prefix:
+Технически, это не применяется React. В принципе, вы можете сделать хук, который не вызывает другие хуки. Это часто сбивает с толку и ограничивает, поэтому лучше избегать этого шаблона. Однако могут быть редкие случаи, когда это полезно. Например, возможно, ваша функция сейчас не использует никаких хуков, но вы планируете добавить в нее какие-то вызовы хуков в будущем. Тогда имеет смысл назвать ее с префиксом `use`:
```js {3-4}
-// ✅ Good: A Hook that will likely use some other Hooks later
+// ✅ Хорошо: хук, который, вероятно, будет использовать какие-то другие хуки позже
function useAuth() {
- // TODO: Replace with this line when authentication is implemented:
+ // TODO: Замените этой строкой, когда будет реализована аутентификация:
// return useContext(Auth);
return TEST_USER;
}
```
-Then components won't be able to call it conditionally. This will become important when you actually add Hook calls inside. If you don't plan to use Hooks inside it (now or later), don't make it a Hook.
+Тогда компоненты не смогут вызывать его условно. Это станет важным, когда вы на самом деле добавите вызовы хуков внутрь. Если вы не планируете использовать хуки внутри (сейчас или позже), не делайте его хуком.
-### Custom Hooks let you share stateful logic, not state itself {/*custom-hooks-let-you-share-stateful-logic-not-state-itself*/}
+### Пользовательские хуки позволяют совместно использовать логику с состоянием, а не само состояние {/*custom-hooks-let-you-share-stateful-logic-not-state-itself*/}
-In the earlier example, when you turned the network on and off, both components updated together. However, it's wrong to think that a single `isOnline` state variable is shared between them. Look at this code:
+В предыдущем примере, когда вы включали и выключали сеть, оба компонента обновлялись вместе. Однако неправильно думать, что между ними совместно используется одна переменная состояния `isOnline`. Посмотрите на этот код:
```js {2,7}
function StatusBar() {
@@ -305,7 +305,7 @@ function SaveButton() {
}
```
-It works the same way as before you extracted the duplication:
+Он работает так же, как и до того, как вы извлекли дублирование:
```js {2-5,10-13}
function StatusBar() {
@@ -325,9 +325,9 @@ function SaveButton() {
}
```
-These are two completely independent state variables and Effects! They happened to have the same value at the same time because you synchronized them with the same external value (whether the network is on).
+Это две совершенно независимые переменные состояния и эффекты! Они оказались одного и того же значения в одно и то же время, потому что вы синхронизировали их с одним и тем же внешним значением (находится ли сеть в сети).
-To better illustrate this, we'll need a different example. Consider this `Form` component:
+Чтобы лучше проиллюстрировать это, нам понадобится другой пример. Рассмотрим этот компонент `Form`:
@@ -369,13 +369,13 @@ input { margin-left: 10px; }
-There's some repetitive logic for each form field:
+Существует некоторая повторяющаяся логика для каждого поля формы:
-1. There's a piece of state (`firstName` and `lastName`).
-1. There's a change handler (`handleFirstNameChange` and `handleLastNameChange`).
-1. There's a piece of JSX that specifies the `value` and `onChange` attributes for that input.
+1. Существует часть состояния (`firstName` и `lastName`).
+2. Существует обработчик изменений (`handleFirstNameChange` и `handleLastNameChange`).
+3. Существует часть JSX, которая указывает атрибуты `value` и `onChange` для этого ввода.
-You can extract the repetitive logic into this `useFormInput` custom Hook:
+Вы можете извлечь повторяющуюся логику в этот пользовательский хук `useFormInput`:
@@ -428,9 +428,9 @@ input { margin-left: 10px; }
-Notice that it only declares *one* state variable called `value`.
+Обратите внимание, что он объявляет только *одну* переменную состояния с именем `value`.
-However, the `Form` component calls `useFormInput` *two times:*
+Однако компонент `Form` вызывает `useFormInput` *дважды:*
```js
function Form() {
@@ -439,17 +439,17 @@ function Form() {
// ...
```
-This is why it works like declaring two separate state variables!
+Вот почему это работает как объявление двух отдельных переменных состояния!
-**Custom Hooks let you share *stateful logic* but not *state itself.* Each call to a Hook is completely independent from every other call to the same Hook.** This is why the two sandboxes above are completely equivalent. If you'd like, scroll back up and compare them. The behavior before and after extracting a custom Hook is identical.
+**Пользовательские хуки позволяют совместно использовать *логику с состоянием*, но не *само состояние*. Каждый вызов хука полностью независим от каждого другого вызова того же хука.** Вот почему две песочницы выше полностью эквивалентны. Если хотите, прокрутитесь назад и сравните их. Поведение до и после извлечения пользовательского хука идентично.
-When you need to share the state itself between multiple components, [lift it up and pass it down](/learn/sharing-state-between-components) instead.
+Когда вам нужно совместно использовать само состояние между несколькими компонентами, [поднимите его и передайте вниз](/learn/sharing-state-between-components) вместо этого.
-## Passing reactive values between Hooks {/*passing-reactive-values-between-hooks*/}
+## Передача реактивных значений между хуками {/*passing-reactive-values-between-hooks*/}
-The code inside your custom Hooks will re-run during every re-render of your component. This is why, like components, custom Hooks [need to be pure.](/learn/keeping-components-pure) Think of custom Hooks' code as part of your component's body!
+Код внутри ваших пользовательских хуков будет перезапускаться во время каждого повторного рендеринга вашего компонента. Вот почему, как и компоненты, пользовательские хуки [должны быть чистыми.](/learn/keeping-components-pure) Думайте о коде пользовательских хуков как о части тела вашего компонента!
-Because custom Hooks re-render together with your component, they always receive the latest props and state. To see what this means, consider this chat room example. Change the server URL or the chat room:
+Поскольку пользовательские хуки перерисовываются вместе с вашим компонентом, они всегда получают последние пропсы и состояние. Чтобы увидеть, что это значит, рассмотрим этот пример чат-комнаты. Измените URL-адрес сервера или чат-комнату:
@@ -599,9 +599,9 @@ button { margin-left: 10px; }
-When you change `serverUrl` or `roomId`, the Effect ["reacts" to your changes](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) and re-synchronizes. You can tell by the console messages that the chat re-connects every time that you change your Effect's dependencies.
+Когда вы изменяете `serverUrl` или `roomId`, эффект ["реагирует" на ваши изменения](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) и повторно синхронизируется. Вы можете сказать по сообщениям в консоли, что чат переподключается каждый раз, когда вы изменяете зависимости вашего эффекта.
-Now move the Effect's code into a custom Hook:
+Теперь переместите код эффекта в пользовательский хук:
```js {2-13}
export function useChatRoom({ serverUrl, roomId }) {
@@ -620,7 +620,7 @@ export function useChatRoom({ serverUrl, roomId }) {
}
```
-This lets your `ChatRoom` component call your custom Hook without worrying about how it works inside:
+Это позволяет вашему компоненту `ChatRoom` вызывать ваш пользовательский хук, не беспокоясь о том, как он работает внутри:
```js {4-7}
export default function ChatRoom({ roomId }) {
@@ -643,9 +643,9 @@ export default function ChatRoom({ roomId }) {
}
```
-This looks much simpler! (But it does the same thing.)
+Это выглядит намного проще! (Но делает то же самое.)
-Notice that the logic *still responds* to prop and state changes. Try editing the server URL or the selected room:
+Обратите внимание, что логика *по-прежнему реагирует* на изменения пропсов и состояния. Попробуйте отредактировать URL-адрес сервера или выбранную комнату:
@@ -807,7 +807,7 @@ button { margin-left: 10px; }
-Notice how you're taking the return value of one Hook:
+Обратите внимание, как вы берете возвращаемое значение одного хука:
```js {2}
export default function ChatRoom({ roomId }) {
@@ -820,7 +820,7 @@ export default function ChatRoom({ roomId }) {
// ...
```
-and passing it as an input to another Hook:
+и передаете его в качестве входных данных другому хуку:
```js {6}
export default function ChatRoom({ roomId }) {
@@ -833,17 +833,17 @@ export default function ChatRoom({ roomId }) {
// ...
```
-Every time your `ChatRoom` component re-renders, it passes the latest `roomId` and `serverUrl` to your Hook. This is why your Effect re-connects to the chat whenever their values are different after a re-render. (If you ever worked with audio or video processing software, chaining Hooks like this might remind you of chaining visual or audio effects. It's as if the output of `useState` "feeds into" the input of the `useChatRoom`.)
+Каждый раз, когда ваш компонент `ChatRoom` перерисовывается, он передает последние `roomId` и `serverUrl` вашему хуку. Вот почему ваш эффект переподключается к чату всякий раз, когда их значения отличаются после перерисовки. (Если вы когда-либо работали с программным обеспечением для обработки аудио или видео, объединение хуков подобным образом может напомнить вам о цепочке визуальных или звуковых эффектов. Как будто выход `useState` «поступает» во вход `useChatRoom`.)
-### Passing event handlers to custom Hooks {/*passing-event-handlers-to-custom-hooks*/}
+### Передача обработчиков событий в пользовательские хуки {/*passing-event-handlers-to-custom-hooks*/}
-This section describes an **experimental API that has not yet been released** in a stable version of React.
+В этом разделе описывается **экспериментальный API, который еще не был выпущен** в стабильной версии React.
-As you start using `useChatRoom` in more components, you might want to let components customize its behavior. For example, currently, the logic for what to do when a message arrives is hardcoded inside the Hook:
+Когда вы начнете использовать `useChatRoom` в большем количестве компонентов, вы можете захотеть, чтобы компоненты настраивали его поведение. Например, в настоящее время логика того, что делать при поступлении сообщения, жестко закодирована внутри хука:
```js {9-11}
export function useChatRoom({ serverUrl, roomId }) {
@@ -862,7 +862,7 @@ export function useChatRoom({ serverUrl, roomId }) {
}
```
-Let's say you want to move this logic back to your component:
+Допустим, вы хотите переместить эту логику обратно в свой компонент:
```js {7-9}
export default function ChatRoom({ roomId }) {
@@ -878,7 +878,7 @@ export default function ChatRoom({ roomId }) {
// ...
```
-To make this work, change your custom Hook to take `onReceiveMessage` as one of its named options:
+Чтобы это работало, измените свой пользовательский хук, чтобы он принимал `onReceiveMessage` в качестве одного из его именованных параметров:
```js {1,10,13}
export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
@@ -897,9 +897,9 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
}
```
-This will work, but there's one more improvement you can do when your custom Hook accepts event handlers.
+Это будет работать, но есть еще одно улучшение, которое вы можете сделать, когда ваш пользовательский хук принимает обработчики событий.
-Adding a dependency on `onReceiveMessage` is not ideal because it will cause the chat to re-connect every time the component re-renders. [Wrap this event handler into an Effect Event to remove it from the dependencies:](/learn/removing-effect-dependencies#wrapping-an-event-handler-from-the-props)
+Добавление зависимости от `onReceiveMessage` не идеально, потому что это приведет к повторному подключению чата каждый раз, когда компонент перерисовывается. [Оберните этот обработчик событий в Effect Event, чтобы удалить его из зависимостей:](/learn/removing-effect-dependencies#wrapping-an-event-handler-from-the-props)
```js {1,4,5,15,18}
import { useEffect, useEffectEvent } from 'react';
@@ -923,7 +923,7 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
}
```
-Now the chat won't re-connect every time that the `ChatRoom` component re-renders. Here is a fully working demo of passing an event handler to a custom Hook that you can play with:
+Теперь чат не будет переподключаться каждый раз, когда компонент `ChatRoom` перерисовывается. Вот полностью рабочий пример передачи обработчика событий в пользовательский хук, с которым вы можете поиграть:
@@ -967,1525 +967,4 @@ export default function ChatRoom({ roomId }) {
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
- showNotification('New message: ' + msg);
- }
- });
-
- return (
- <>
-
-
Welcome to the {roomId} room!
- >
- );
-}
-```
-
-```js src/useChatRoom.js
-import { useEffect } from 'react';
-import { experimental_useEffectEvent as useEffectEvent } from 'react';
-import { createConnection } from './chat.js';
-
-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]);
-}
-```
-
-```js src/chat.js
-export function createConnection({ serverUrl, roomId }) {
- // A real implementation would actually connect to the server
- if (typeof serverUrl !== 'string') {
- throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);
- }
- if (typeof roomId !== 'string') {
- throw Error('Expected roomId to be a string. Received: ' + roomId);
- }
- let intervalId;
- let messageCallback;
- 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) {
- throw Error('Cannot add the handler twice.');
- }
- if (event !== 'message') {
- throw Error('Only "message" event is supported.');
- }
- messageCallback = callback;
- },
- };
-}
-```
-
-```js src/notifications.js
-import Toastify from 'toastify-js';
-import 'toastify-js/src/toastify.css';
-
-export function showNotification(message, theme = 'dark') {
- Toastify({
- text: message,
- duration: 2000,
- gravity: 'top',
- position: 'right',
- style: {
- background: theme === 'dark' ? 'black' : 'white',
- color: theme === 'dark' ? 'white' : 'black',
- },
- }).showToast();
-}
-```
-
-```json package.json hidden
-{
- "dependencies": {
- "react": "experimental",
- "react-dom": "experimental",
- "react-scripts": "latest",
- "toastify-js": "1.12.0"
- },
- "scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test --env=jsdom",
- "eject": "react-scripts eject"
- }
-}
-```
-
-```css
-input { display: block; margin-bottom: 20px; }
-button { margin-left: 10px; }
-```
-
-
-
-Notice how you no longer need to know *how* `useChatRoom` works in order to use it. You could add it to any other component, pass any other options, and it would work the same way. That's the power of custom Hooks.
-
-## When to use custom Hooks {/*when-to-use-custom-hooks*/}
-
-You don't need to extract a custom Hook for every little duplicated bit of code. Some duplication is fine. For example, extracting a `useFormInput` Hook to wrap a single `useState` call like earlier is probably unnecessary.
-
-However, whenever you write an Effect, consider whether it would be clearer to also wrap it in a custom Hook. [You shouldn't need Effects very often,](/learn/you-might-not-need-an-effect) so if you're writing one, it means that you need to "step outside React" to synchronize with some external system or to do something that React doesn't have a built-in API for. Wrapping it into a custom Hook lets you precisely communicate your intent and how the data flows through it.
-
-For example, consider a `ShippingForm` component that displays two dropdowns: one shows the list of cities, and another shows the list of areas in the selected city. You might start with some code that looks like this:
-
-```js {3-16,20-35}
-function ShippingForm({ country }) {
- const [cities, setCities] = useState(null);
- // This Effect fetches cities for a country
- 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);
- // This Effect fetches areas for the selected city
- 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]);
-
- // ...
-```
-
-Although this code is quite repetitive, [it's correct to keep these Effects separate from each other.](/learn/removing-effect-dependencies#is-your-effect-doing-several-unrelated-things) They synchronize two different things, so you shouldn't merge them into one Effect. Instead, you can simplify the `ShippingForm` component above by extracting the common logic between them into your own `useData` Hook:
-
-```js {2-18}
-function useData(url) {
- const [data, setData] = useState(null);
- useEffect(() => {
- if (url) {
- let ignore = false;
- fetch(url)
- .then(response => response.json())
- .then(json => {
- if (!ignore) {
- setData(json);
- }
- });
- return () => {
- ignore = true;
- };
- }
- }, [url]);
- return data;
-}
-```
-
-Now you can replace both Effects in the `ShippingForm` components with calls to `useData`:
-
-```js {2,4}
-function ShippingForm({ country }) {
- const cities = useData(`/api/cities?country=${country}`);
- const [city, setCity] = useState(null);
- const areas = useData(city ? `/api/areas?city=${city}` : null);
- // ...
-```
-
-Extracting a custom Hook makes the data flow explicit. You feed the `url` in and you get the `data` out. By "hiding" your Effect inside `useData`, you also prevent someone working on the `ShippingForm` component from adding [unnecessary dependencies](/learn/removing-effect-dependencies) to it. With time, most of your app's Effects will be in custom Hooks.
-
-
-
-#### Keep your custom Hooks focused on concrete high-level use cases {/*keep-your-custom-hooks-focused-on-concrete-high-level-use-cases*/}
-
-Start by choosing your custom Hook's name. If you struggle to pick a clear name, it might mean that your Effect is too coupled to the rest of your component's logic, and is not yet ready to be extracted.
-
-Ideally, your custom Hook's name should be clear enough that even a person who doesn't write code often could have a good guess about what your custom Hook does, what it takes, and what it returns:
-
-* ✅ `useData(url)`
-* ✅ `useImpressionLog(eventName, extraData)`
-* ✅ `useChatRoom(options)`
-
-When you synchronize with an external system, your custom Hook name may be more technical and use jargon specific to that system. It's good as long as it would be clear to a person familiar with that system:
-
-* ✅ `useMediaQuery(query)`
-* ✅ `useSocket(url)`
-* ✅ `useIntersectionObserver(ref, options)`
-
-**Keep custom Hooks focused on concrete high-level use cases.** Avoid creating and using custom "lifecycle" Hooks that act as alternatives and convenience wrappers for the `useEffect` API itself:
-
-* 🔴 `useMount(fn)`
-* 🔴 `useEffectOnce(fn)`
-* 🔴 `useUpdateEffect(fn)`
-
-For example, this `useMount` Hook tries to ensure some code only runs "on mount":
-
-```js {4-5,14-15}
-function ChatRoom({ roomId }) {
- const [serverUrl, setServerUrl] = useState('https://localhost:1234');
-
- // 🔴 Avoid: using custom "lifecycle" Hooks
- useMount(() => {
- const connection = createConnection({ roomId, serverUrl });
- connection.connect();
-
- post('/analytics/event', { eventName: 'visit_chat' });
- });
- // ...
-}
-
-// 🔴 Avoid: creating custom "lifecycle" Hooks
-function useMount(fn) {
- useEffect(() => {
- fn();
- }, []); // 🔴 React Hook useEffect has a missing dependency: 'fn'
-}
-```
-
-**Custom "lifecycle" Hooks like `useMount` don't fit well into the React paradigm.** For example, this code example has a mistake (it doesn't "react" to `roomId` or `serverUrl` changes), but the linter won't warn you about it because the linter only checks direct `useEffect` calls. It won't know about your Hook.
-
-If you're writing an Effect, start by using the React API directly:
-
-```js
-function ChatRoom({ roomId }) {
- const [serverUrl, setServerUrl] = useState('https://localhost:1234');
-
- // ✅ Good: two raw Effects separated by purpose
-
- useEffect(() => {
- const connection = createConnection({ serverUrl, roomId });
- connection.connect();
- return () => connection.disconnect();
- }, [serverUrl, roomId]);
-
- useEffect(() => {
- post('/analytics/event', { eventName: 'visit_chat', roomId });
- }, [roomId]);
-
- // ...
-}
-```
-
-Then, you can (but don't have to) extract custom Hooks for different high-level use cases:
-
-```js
-function ChatRoom({ roomId }) {
- const [serverUrl, setServerUrl] = useState('https://localhost:1234');
-
- // ✅ Great: custom Hooks named after their purpose
- useChatRoom({ serverUrl, roomId });
- useImpressionLog('visit_chat', { roomId });
- // ...
-}
-```
-
-**A good custom Hook makes the calling code more declarative by constraining what it does.** For example, `useChatRoom(options)` can only connect to the chat room, while `useImpressionLog(eventName, extraData)` can only send an impression log to the analytics. If your custom Hook API doesn't constrain the use cases and is very abstract, in the long run it's likely to introduce more problems than it solves.
-
-
-
-### Custom Hooks help you migrate to better patterns {/*custom-hooks-help-you-migrate-to-better-patterns*/}
-
-Effects are an ["escape hatch"](/learn/escape-hatches): you use them when you need to "step outside React" and when there is no better built-in solution for your use case. With time, the React team's goal is to reduce the number of the Effects in your app to the minimum by providing more specific solutions to more specific problems. Wrapping your Effects in custom Hooks makes it easier to upgrade your code when these solutions become available.
-
-Let's return to this example:
-
-
-
-```js
-import { useOnlineStatus } from './useOnlineStatus.js';
-
-function StatusBar() {
- const isOnline = useOnlineStatus();
- return
{isOnline ? '✅ Online' : '❌ Disconnected'}
;
-}
-
-function SaveButton() {
- const isOnline = useOnlineStatus();
-
- function handleSaveClick() {
- console.log('✅ Progress saved');
- }
-
- return (
-
- );
-}
-
-export default function App() {
- return (
- <>
-
-
- >
- );
-}
-```
-
-```js src/useOnlineStatus.js active
-import { useState, useEffect } from 'react';
-
-export 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;
-}
-```
-
-
-
-In the above example, `useOnlineStatus` is implemented with a pair of [`useState`](/reference/react/useState) and [`useEffect`.](/reference/react/useEffect) However, this isn't the best possible solution. There is a number of edge cases it doesn't consider. For example, it assumes that when the component mounts, `isOnline` is already `true`, but this may be wrong if the network already went offline. You can use the browser [`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine) API to check for that, but using it directly would not work on the server for generating the initial HTML. In short, this code could be improved.
-
-React includes a dedicated API called [`useSyncExternalStore`](/reference/react/useSyncExternalStore) which takes care of all of these problems for you. Here is your `useOnlineStatus` Hook, rewritten to take advantage of this new API:
-
-
-
-```js
-import { useOnlineStatus } from './useOnlineStatus.js';
-
-function StatusBar() {
- const isOnline = useOnlineStatus();
- return
{isOnline ? '✅ Online' : '❌ Disconnected'}
;
-}
-
-function SaveButton() {
- const isOnline = useOnlineStatus();
-
- function handleSaveClick() {
- console.log('✅ Progress saved');
- }
-
- return (
-
- );
-}
-
-export default function App() {
- return (
- <>
-
-
- >
- );
-}
-```
-
-```js src/useOnlineStatus.js active
-import { useSyncExternalStore } from 'react';
-
-function subscribe(callback) {
- window.addEventListener('online', callback);
- window.addEventListener('offline', callback);
- return () => {
- window.removeEventListener('online', callback);
- window.removeEventListener('offline', callback);
- };
-}
-
-export function useOnlineStatus() {
- return useSyncExternalStore(
- subscribe,
- () => navigator.onLine, // How to get the value on the client
- () => true // How to get the value on the server
- );
-}
-
-```
-
-
-
-Notice how **you didn't need to change any of the components** to make this migration:
-
-```js {2,7}
-function StatusBar() {
- const isOnline = useOnlineStatus();
- // ...
-}
-
-function SaveButton() {
- const isOnline = useOnlineStatus();
- // ...
-}
-```
-
-This is another reason for why wrapping Effects in custom Hooks is often beneficial:
-
-1. You make the data flow to and from your Effects very explicit.
-2. You let your components focus on the intent rather than on the exact implementation of your Effects.
-3. When React adds new features, you can remove those Effects without changing any of your components.
-
-Similar to a [design system,](https://uxdesign.cc/everything-you-need-to-know-about-design-systems-54b109851969) you might find it helpful to start extracting common idioms from your app's components into custom Hooks. This will keep your components' code focused on the intent, and let you avoid writing raw Effects very often. Many excellent custom Hooks are maintained by the React community.
-
-
-
-#### Will React provide any built-in solution for data fetching? {/*will-react-provide-any-built-in-solution-for-data-fetching*/}
-
-We're still working out the details, but we expect that in the future, you'll write data fetching like this:
-
-```js {1,4,6}
-import { use } from 'react'; // Not available yet!
-
-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;
- // ...
-```
-
-If you use custom Hooks like `useData` above in your app, it will require fewer changes to migrate to the eventually recommended approach than if you write raw Effects in every component manually. However, the old approach will still work fine, so if you feel happy writing raw Effects, you can continue to do that.
-
-
-
-### There is more than one way to do it {/*there-is-more-than-one-way-to-do-it*/}
-
-Let's say you want to implement a fade-in animation *from scratch* using the browser [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) API. You might start with an Effect that sets up an animation loop. During each frame of the animation, you could change the opacity of the DOM node you [hold in a ref](/learn/manipulating-the-dom-with-refs) until it reaches `1`. Your code might start like this:
-
-
-
-```js
-import { useState, useEffect, useRef } from 'react';
-
-function Welcome() {
- const ref = useRef(null);
-
- useEffect(() => {
- const duration = 1000;
- const node = ref.current;
-
- let startTime = performance.now();
- let frameId = null;
-
- function onFrame(now) {
- const timePassed = now - startTime;
- const progress = Math.min(timePassed / duration, 1);
- onProgress(progress);
- if (progress < 1) {
- // We still have more frames to paint
- frameId = requestAnimationFrame(onFrame);
- }
- }
-
- function onProgress(progress) {
- node.style.opacity = progress;
- }
-
- function start() {
- onProgress(0);
- startTime = performance.now();
- frameId = requestAnimationFrame(onFrame);
- }
-
- function stop() {
- cancelAnimationFrame(frameId);
- startTime = null;
- frameId = null;
- }
-
- start();
- return () => stop();
- }, []);
-
- return (
-
- );
-}
-
-export default function App() {
- const [show, setShow] = useState(false);
- return (
- <>
-
-
- {show && }
- >
- );
-}
-```
-
-```js src/useFadeIn.js
-import { useEffect } from 'react';
-
-export function useFadeIn(ref, duration) {
- useEffect(() => {
- const node = ref.current;
-
- let startTime = performance.now();
- let frameId = null;
-
- function onFrame(now) {
- const timePassed = now - startTime;
- const progress = Math.min(timePassed / duration, 1);
- onProgress(progress);
- if (progress < 1) {
- // We still have more frames to paint
- frameId = requestAnimationFrame(onFrame);
- }
- }
-
- function onProgress(progress) {
- node.style.opacity = progress;
- }
-
- function start() {
- onProgress(0);
- startTime = performance.now();
- frameId = requestAnimationFrame(onFrame);
- }
-
- function stop() {
- cancelAnimationFrame(frameId);
- startTime = null;
- frameId = null;
- }
-
- start();
- return () => stop();
- }, [ref, duration]);
-}
-```
-
-```css
-label, button { display: block; margin-bottom: 20px; }
-html, body { min-height: 300px; }
-.welcome {
- opacity: 0;
- color: white;
- padding: 50px;
- text-align: center;
- font-size: 50px;
- background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);
-}
-```
-
-
-
-You could keep the `useFadeIn` code as is, but you could also refactor it more. For example, you could extract the logic for setting up the animation loop out of `useFadeIn` into a custom `useAnimationLoop` Hook:
-
-
-
-```js
-import { useState, useEffect, useRef } from 'react';
-import { useFadeIn } from './useFadeIn.js';
-
-function Welcome() {
- const ref = useRef(null);
-
- useFadeIn(ref, 1000);
-
- return (
-
- Welcome
-
- );
-}
-
-export default function App() {
- const [show, setShow] = useState(false);
- return (
- <>
-
-
- {show && }
- >
- );
-}
-```
-
-```js src/useFadeIn.js active
-import { useState, useEffect } from 'react';
-import { experimental_useEffectEvent as useEffectEvent } from 'react';
-
-export function useFadeIn(ref, duration) {
- const [isRunning, setIsRunning] = useState(true);
-
- useAnimationLoop(isRunning, (timePassed) => {
- const progress = Math.min(timePassed / duration, 1);
- ref.current.style.opacity = progress;
- if (progress === 1) {
- setIsRunning(false);
- }
- });
-}
-
-function useAnimationLoop(isRunning, drawFrame) {
- const onFrame = useEffectEvent(drawFrame);
-
- useEffect(() => {
- if (!isRunning) {
- return;
- }
-
- const startTime = performance.now();
- let frameId = null;
-
- function tick(now) {
- const timePassed = now - startTime;
- onFrame(timePassed);
- frameId = requestAnimationFrame(tick);
- }
-
- tick();
- return () => cancelAnimationFrame(frameId);
- }, [isRunning]);
-}
-```
-
-```css
-label, button { display: block; margin-bottom: 20px; }
-html, body { min-height: 300px; }
-.welcome {
- opacity: 0;
- color: white;
- padding: 50px;
- text-align: center;
- font-size: 50px;
- background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);
-}
-```
-
-```json package.json hidden
-{
- "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"
- }
-}
-```
-
-
-
-However, you didn't *have to* do that. As with regular functions, ultimately you decide where to draw the boundaries between different parts of your code. You could also take a very different approach. Instead of keeping the logic in the Effect, you could move most of the imperative logic inside a JavaScript [class:](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes)
-
-
-
-```js
-import { useState, useEffect, useRef } from 'react';
-import { useFadeIn } from './useFadeIn.js';
-
-function Welcome() {
- const ref = useRef(null);
-
- useFadeIn(ref, 1000);
-
- return (
-
- Welcome
-
- );
-}
-
-export default function App() {
- const [show, setShow] = useState(false);
- return (
- <>
-
-
- {show && }
- >
- );
-}
-```
-
-```js src/useFadeIn.js active
-import { useState, useEffect } from 'react';
-import { FadeInAnimation } from './animation.js';
-
-export function useFadeIn(ref, duration) {
- useEffect(() => {
- const animation = new FadeInAnimation(ref.current);
- animation.start(duration);
- return () => {
- animation.stop();
- };
- }, [ref, duration]);
-}
-```
-
-```js src/animation.js
-export class FadeInAnimation {
- constructor(node) {
- this.node = node;
- }
- start(duration) {
- this.duration = duration;
- this.onProgress(0);
- this.startTime = performance.now();
- this.frameId = requestAnimationFrame(() => this.onFrame());
- }
- onFrame() {
- const timePassed = performance.now() - this.startTime;
- const progress = Math.min(timePassed / this.duration, 1);
- this.onProgress(progress);
- if (progress === 1) {
- this.stop();
- } else {
- // We still have more frames to paint
- this.frameId = requestAnimationFrame(() => this.onFrame());
- }
- }
- onProgress(progress) {
- this.node.style.opacity = progress;
- }
- stop() {
- cancelAnimationFrame(this.frameId);
- this.startTime = null;
- this.frameId = null;
- this.duration = 0;
- }
-}
-```
-
-```css
-label, button { display: block; margin-bottom: 20px; }
-html, body { min-height: 300px; }
-.welcome {
- opacity: 0;
- color: white;
- padding: 50px;
- text-align: center;
- font-size: 50px;
- background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);
-}
-```
-
-
-
-Effects let you connect React to external systems. The more coordination between Effects is needed (for example, to chain multiple animations), the more it makes sense to extract that logic out of Effects and Hooks *completely* like in the sandbox above. Then, the code you extracted *becomes* the "external system". This lets your Effects stay simple because they only need to send messages to the system you've moved outside React.
-
-The examples above assume that the fade-in logic needs to be written in JavaScript. However, this particular fade-in animation is both simpler and much more efficient to implement with a plain [CSS Animation:](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations)
-
-
-
-```js
-import { useState, useEffect, useRef } from 'react';
-import './welcome.css';
-
-function Welcome() {
- return (
-
- Welcome
-
- );
-}
-
-export default function App() {
- const [show, setShow] = useState(false);
- return (
- <>
-
-
- {show && }
- >
- );
-}
-```
-
-```css src/styles.css
-label, button { display: block; margin-bottom: 20px; }
-html, body { min-height: 300px; }
-```
-
-```css src/welcome.css active
-.welcome {
- color: white;
- padding: 50px;
- text-align: center;
- font-size: 50px;
- background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);
-
- animation: fadeIn 1000ms;
-}
-
-@keyframes fadeIn {
- 0% { opacity: 0; }
- 100% { opacity: 1; }
-}
-
-```
-
-
-
-Sometimes, you don't even need a Hook!
-
-
-
-- Custom Hooks let you share logic between components.
-- Custom Hooks must be named starting with `use` followed by a capital letter.
-- Custom Hooks only share stateful logic, not state itself.
-- You can pass reactive values from one Hook to another, and they stay up-to-date.
-- All Hooks re-run every time your component re-renders.
-- The code of your custom Hooks should be pure, like your component's code.
-- Wrap event handlers received by custom Hooks into Effect Events.
-- Don't create custom Hooks like `useMount`. Keep their purpose specific.
-- It's up to you how and where to choose the boundaries of your code.
-
-
-
-
-
-#### Extract a `useCounter` Hook {/*extract-a-usecounter-hook*/}
-
-This component uses a state variable and an Effect to display a number that increments every second. Extract this logic into a custom Hook called `useCounter`. Your goal is to make the `Counter` component implementation look exactly like this:
-
-```js
-export default function Counter() {
- const count = useCounter();
- return
Seconds passed: {count}
;
-}
-```
-
-You'll need to write your custom Hook in `useCounter.js` and import it into the `App.js` file.
-
-
-
-```js
-import { useState, useEffect } from 'react';
-
-export default function Counter() {
- const [count, setCount] = useState(0);
- useEffect(() => {
- const id = setInterval(() => {
- setCount(c => c + 1);
- }, 1000);
- return () => clearInterval(id);
- }, []);
- return
Seconds passed: {count}
;
-}
-```
-
-```js src/useCounter.js
-// Write your custom Hook in this file!
-```
-
-
-
-
-
-Your code should look like this:
-
-
-
-```js
-import { useCounter } from './useCounter.js';
-
-export default function Counter() {
- const count = useCounter();
- return
Seconds passed: {count}
;
-}
-```
-
-```js src/useCounter.js
-import { useState, useEffect } from 'react';
-
-export function useCounter() {
- const [count, setCount] = useState(0);
- useEffect(() => {
- const id = setInterval(() => {
- setCount(c => c + 1);
- }, 1000);
- return () => clearInterval(id);
- }, []);
- return count;
-}
-```
-
-
-
-Notice that `App.js` doesn't need to import `useState` or `useEffect` anymore.
-
-
-
-#### Make the counter delay configurable {/*make-the-counter-delay-configurable*/}
-
-In this example, there is a `delay` state variable controlled by a slider, but its value is not used. Pass the `delay` value to your custom `useCounter` Hook, and change the `useCounter` Hook to use the passed `delay` instead of hardcoding `1000` ms.
-
-
-
-```js
-import { useState } from 'react';
-import { useCounter } from './useCounter.js';
-
-export default function Counter() {
- const [delay, setDelay] = useState(1000);
- const count = useCounter();
- return (
- <>
-
-
-
Ticks: {count}
- >
- );
-}
-```
-
-```js src/useCounter.js
-import { useState, useEffect } from 'react';
-
-export function useCounter() {
- const [count, setCount] = useState(0);
- useEffect(() => {
- const id = setInterval(() => {
- setCount(c => c + 1);
- }, 1000);
- return () => clearInterval(id);
- }, []);
- return count;
-}
-```
-
-
-
-
-
-Pass the `delay` to your Hook with `useCounter(delay)`. Then, inside the Hook, use `delay` instead of the hardcoded `1000` value. You'll need to add `delay` to your Effect's dependencies. This ensures that a change in `delay` will reset the interval.
-
-
-
-```js
-import { useState } from 'react';
-import { useCounter } from './useCounter.js';
-
-export default function Counter() {
- const [delay, setDelay] = useState(1000);
- const count = useCounter(delay);
- return (
- <>
-
-
-
Ticks: {count}
- >
- );
-}
-```
-
-```js src/useCounter.js
-import { useState, useEffect } from 'react';
-
-export function useCounter(delay) {
- const [count, setCount] = useState(0);
- useEffect(() => {
- const id = setInterval(() => {
- setCount(c => c + 1);
- }, delay);
- return () => clearInterval(id);
- }, [delay]);
- return count;
-}
-```
-
-
-
-
-
-#### Extract `useInterval` out of `useCounter` {/*extract-useinterval-out-of-usecounter*/}
-
-Currently, your `useCounter` Hook does two things. It sets up an interval, and it also increments a state variable on every interval tick. Split out the logic that sets up the interval into a separate Hook called `useInterval`. It should take two arguments: the `onTick` callback, and the `delay`. After this change, your `useCounter` implementation should look like this:
-
-```js
-export function useCounter(delay) {
- const [count, setCount] = useState(0);
- useInterval(() => {
- setCount(c => c + 1);
- }, delay);
- return count;
-}
-```
-
-Write `useInterval` in the `useInterval.js` file and import it into the `useCounter.js` file.
-
-
-
-```js
-import { useCounter } from './useCounter.js';
-
-export default function Counter() {
- const count = useCounter(1000);
- return
Seconds passed: {count}
;
-}
-```
-
-```js src/useCounter.js
-import { useState, useEffect } from 'react';
-
-export function useCounter(delay) {
- const [count, setCount] = useState(0);
- useEffect(() => {
- const id = setInterval(() => {
- setCount(c => c + 1);
- }, delay);
- return () => clearInterval(id);
- }, [delay]);
- return count;
-}
-```
-
-```js src/useInterval.js
-// Write your Hook here!
-```
-
-
-
-
-
-The logic inside `useInterval` should set up and clear the interval. It doesn't need to do anything else.
-
-
-
-```js
-import { useCounter } from './useCounter.js';
-
-export default function Counter() {
- const count = useCounter(1000);
- return
Seconds passed: {count}
;
-}
-```
-
-```js src/useCounter.js
-import { useState } from 'react';
-import { useInterval } from './useInterval.js';
-
-export function useCounter(delay) {
- const [count, setCount] = useState(0);
- useInterval(() => {
- setCount(c => c + 1);
- }, delay);
- return count;
-}
-```
-
-```js src/useInterval.js active
-import { useEffect } from 'react';
-
-export function useInterval(onTick, delay) {
- useEffect(() => {
- const id = setInterval(onTick, delay);
- return () => clearInterval(id);
- }, [onTick, delay]);
-}
-```
-
-
-
-Note that there is a bit of a problem with this solution, which you'll solve in the next challenge.
-
-
-
-#### Fix a resetting interval {/*fix-a-resetting-interval*/}
-
-In this example, there are *two* separate intervals.
-
-The `App` component calls `useCounter`, which calls `useInterval` to update the counter every second. But the `App` component *also* calls `useInterval` to randomly update the page background color every two seconds.
-
-For some reason, the callback that updates the page background never runs. Add some logs inside `useInterval`:
-
-```js {2,5}
- useEffect(() => {
- console.log('✅ Setting up an interval with delay ', delay)
- const id = setInterval(onTick, delay);
- return () => {
- console.log('❌ Clearing an interval with delay ', delay)
- clearInterval(id);
- };
- }, [onTick, delay]);
-```
-
-Do the logs match what you expect to happen? If some of your Effects seem to re-synchronize unnecessarily, can you guess which dependency is causing that to happen? Is there some way to [remove that dependency](/learn/removing-effect-dependencies) from your Effect?
-
-After you fix the issue, you should expect the page background to update every two seconds.
-
-
-
-It looks like your `useInterval` Hook accepts an event listener as an argument. Can you think of some way to wrap that event listener so that it doesn't need to be a dependency of your Effect?
-
-
-
-
-
-```json package.json hidden
-{
- "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"
- }
-}
-```
-
-```js
-import { useCounter } from './useCounter.js';
-import { useInterval } from './useInterval.js';
-
-export default function Counter() {
- const count = useCounter(1000);
-
- useInterval(() => {
- const randomColor = `hsla(${Math.random() * 360}, 100%, 50%, 0.2)`;
- document.body.style.backgroundColor = randomColor;
- }, 2000);
-
- return
Seconds passed: {count}
;
-}
-```
-
-```js src/useCounter.js
-import { useState } from 'react';
-import { useInterval } from './useInterval.js';
-
-export function useCounter(delay) {
- const [count, setCount] = useState(0);
- useInterval(() => {
- setCount(c => c + 1);
- }, delay);
- return count;
-}
-```
-
-```js src/useInterval.js
-import { useEffect } from 'react';
-import { experimental_useEffectEvent as useEffectEvent } from 'react';
-
-export function useInterval(onTick, delay) {
- useEffect(() => {
- const id = setInterval(onTick, delay);
- return () => {
- clearInterval(id);
- };
- }, [onTick, delay]);
-}
-```
-
-
-
-
-
-Inside `useInterval`, wrap the tick callback into an Effect Event, as you did [earlier on this page.](/learn/reusing-logic-with-custom-hooks#passing-event-handlers-to-custom-hooks)
-
-This will allow you to omit `onTick` from dependencies of your Effect. The Effect won't re-synchronize on every re-render of the component, so the page background color change interval won't get reset every second before it has a chance to fire.
-
-With this change, both intervals work as expected and don't interfere with each other:
-
-
-
-```json package.json hidden
-{
- "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"
- }
-}
-```
-
-
-```js
-import { useCounter } from './useCounter.js';
-import { useInterval } from './useInterval.js';
-
-export default function Counter() {
- const count = useCounter(1000);
-
- useInterval(() => {
- const randomColor = `hsla(${Math.random() * 360}, 100%, 50%, 0.2)`;
- document.body.style.backgroundColor = randomColor;
- }, 2000);
-
- return
Seconds passed: {count}
;
-}
-```
-
-```js src/useCounter.js
-import { useState } from 'react';
-import { useInterval } from './useInterval.js';
-
-export function useCounter(delay) {
- const [count, setCount] = useState(0);
- useInterval(() => {
- setCount(c => c + 1);
- }, delay);
- return count;
-}
-```
-
-```js src/useInterval.js active
-import { useEffect } from 'react';
-import { experimental_useEffectEvent as useEffectEvent } from 'react';
-
-export function useInterval(callback, delay) {
- const onTick = useEffectEvent(callback);
- useEffect(() => {
- const id = setInterval(onTick, delay);
- return () => clearInterval(id);
- }, [delay]);
-}
-```
-
-
-
-
-
-#### Implement a staggering movement {/*implement-a-staggering-movement*/}
-
-In this example, the `usePointerPosition()` Hook tracks the current pointer position. Try moving your cursor or your finger over the preview area and see the red dot follow your movement. Its position is saved in the `pos1` variable.
-
-In fact, there are five (!) different red dots being rendered. You don't see them because currently they all appear at the same position. This is what you need to fix. What you want to implement instead is a "staggered" movement: each dot should "follow" the previous dot's path. For example, if you quickly move your cursor, the first dot should follow it immediately, the second dot should follow the first dot with a small delay, the third dot should follow the second dot, and so on.
-
-You need to implement the `useDelayedValue` custom Hook. Its current implementation returns the `value` provided to it. Instead, you want to return the value back from `delay` milliseconds ago. You might need some state and an Effect to do this.
-
-After you implement `useDelayedValue`, you should see the dots move following one another.
-
-
-
-You'll need to store the `delayedValue` as a state variable inside your custom Hook. When the `value` changes, you'll want to run an Effect. This Effect should update `delayedValue` after the `delay`. You might find it helpful to call `setTimeout`.
-
-Does this Effect need cleanup? Why or why not?
-
-
-
-
-
-```js
-import { usePointerPosition } from './usePointerPosition.js';
-
-function useDelayedValue(value, delay) {
- // TODO: Implement this Hook
- return value;
-}
-
-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(pos3, 50);
- return (
- <>
-
-
-
-
-
- >
- );
-}
-
-function Dot({ position, opacity }) {
- return (
-
- );
-}
-```
-
-```js src/usePointerPosition.js
-import { useState, useEffect } from 'react';
-
-export function usePointerPosition() {
- 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 position;
-}
-```
-
-```css
-body { min-height: 300px; }
-```
-
-
-
-
-
-Here is a working version. You keep the `delayedValue` as a state variable. When `value` updates, your Effect schedules a timeout to update the `delayedValue`. This is why the `delayedValue` always "lags behind" the actual `value`.
-
-
-
-```js
-import { useState, useEffect } from 'react';
-import { usePointerPosition } from './usePointerPosition.js';
-
-function useDelayedValue(value, delay) {
- const [delayedValue, setDelayedValue] = useState(value);
-
- useEffect(() => {
- setTimeout(() => {
- setDelayedValue(value);
- }, delay);
- }, [value, delay]);
-
- return delayedValue;
-}
-
-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(pos3, 50);
- return (
- <>
-
-
-
-
-
- >
- );
-}
-
-function Dot({ position, opacity }) {
- return (
-
- );
-}
-```
-
-```js src/usePointerPosition.js
-import { useState, useEffect } from 'react';
-
-export function usePointerPosition() {
- 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 position;
-}
-```
-
-```css
-body { min-height: 300px; }
-```
-
-
-
-Note that this Effect *does not* need cleanup. If you called `clearTimeout` in the cleanup function, then each time the `value` changes, it would reset the already scheduled timeout. To keep the movement continuous, you want all the timeouts to fire.
-
-
-
-
+ showNotification('New message: ' + msg);
\ No newline at end of file
From e777736f0ed4b29cd8a7cbb4ec8cce9f1f7db248 Mon Sep 17 00:00:00 2001
From: "translate-react-bot[bot]"
<251169733+translate-react-bot[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 16:17:50 +0000
Subject: [PATCH 2/3] =?UTF-8?q?docs:=20translate=20`reusing-logic-with-cus?=
=?UTF-8?q?tom-hooks.md`=20to=20=D0=A0=D1=83=D1=81=D1=81=D0=BA=D0=B8=D0=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../learn/reusing-logic-with-custom-hooks.md | 1654 ++++++++++++++++-
1 file changed, 1588 insertions(+), 66 deletions(-)
diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md
index 466c7bac4f..126c18f7ed 100644
--- a/src/content/learn/reusing-logic-with-custom-hooks.md
+++ b/src/content/learn/reusing-logic-with-custom-hooks.md
@@ -1,30 +1,29 @@
---
-title: 'Повторное использование логики с пользовательскими хуками'
+title: 'Повторное использование логики с помощью пользовательских хуков'
---
-```html
-React предоставляет несколько встроенных хуков, таких как `useState`, `useContext` и `useEffect`. Иногда вам захочется, чтобы был хук для какой-то более конкретной цели: например, для получения данных, для отслеживания того, находится ли пользователь в сети, или для подключения к чат-комнате. Вы можете не найти эти хуки в React, но вы можете создать свои собственные хуки для нужд вашего приложения.
+React поставляется с несколькими встроенными хуками, такими как `useState`, `useContext` и `useEffect`. Иногда вам может захотеться иметь хук для более специфической цели: например, для получения данных, отслеживания статуса пользователя (онлайн/офлайн) или подключения к чату. Возможно, вы не найдете таких хуков в React, но вы можете создавать свои собственные хуки для нужд вашего приложения.
-- Что такое пользовательские хуки и как писать свои собственные
+- Что такое пользовательские хуки и как их писать
- Как повторно использовать логику между компонентами
-- Как называть и структурировать свои пользовательские хуки
-- Когда и почему следует извлекать пользовательские хуки
+- Как называть и структурировать пользовательские хуки
+- Когда и зачем извлекать пользовательские хуки
-## Пользовательские хуки: совместное использование логики между компонентами {/*custom-hooks-sharing-logic-between-components*/}
+## Пользовательские хуки: Повторное использование логики между компонентами {/*custom-hooks-sharing-logic-between-components*/}
-Представьте, что вы разрабатываете приложение, которое сильно зависит от сети (как и большинство приложений). Вы хотите предупредить пользователя, если его сетевое соединение случайно отключилось во время использования вашего приложения. Как бы вы это сделали? Кажется, вам понадобятся две вещи в вашем компоненте:
+Представьте, что вы разрабатываете приложение, которое сильно зависит от сети (как и большинство приложений). Вы хотите предупредить пользователя, если его сетевое соединение случайно отключилось во время использования вашего приложения. Как бы вы это сделали? Похоже, вам понадобятся две вещи в вашем компоненте:
-1. Часть состояния, которая отслеживает, находится ли сеть в сети.
-2. Эффект, который подписывается на глобальные события [`online`](https://developer.mozilla.org/ru/docs/Web/API/Window/online_event) и [`offline`](https://developer.mozilla.org/ru/docs/Web/API/Window/offline_event) и обновляет это состояние.
+1. Часть состояния, отслеживающая, находится ли сеть в сети.
+2. Эффект, который подписывается на глобальные события [`online`](https://developer.mozilla.org/en-US/docs/Web/API/Window/online_event) и [`offline`](https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event) и обновляет это состояние.
-Это будет держать ваш компонент [синхронизированным](/learn/synchronizing-with-effects) со статусом сети. Вы можете начать с чего-то вроде этого:
+Это позволит вашему компоненту [синхронизироваться](/learn/synchronizing-with-effects) со статусом сети. Вы можете начать с чего-то вроде этого:
@@ -54,9 +53,9 @@ export default function StatusBar() {
-Попробуйте включать и выключать сеть и обратите внимание, как этот `StatusBar` обновляется в ответ на ваши действия.
+Попробуйте включить и выключить сеть и заметьте, как этот `StatusBar` обновляется в ответ на ваши действия.
-Теперь представьте, что вы *также* хотите использовать ту же логику в другом компоненте. Вы хотите реализовать кнопку «Сохранить», которая будет отключаться и отображать «Переподключение...» вместо «Сохранить», пока сеть отключена.
+Теперь представьте, что вы *также* хотите использовать ту же логику в другом компоненте. Вы хотите реализовать кнопку «Сохранить», которая будет отключена и будет показывать «Переподключение...» вместо «Сохранить», пока сеть отключена.
Для начала вы можете скопировать и вставить состояние `isOnline` и эффект в `SaveButton`:
@@ -96,13 +95,13 @@ export default function SaveButton() {
-Убедитесь, что при отключении сети кнопка изменит свой внешний вид.
+Убедитесь, что если вы отключите сеть, кнопка изменит свой внешний вид.
-Эти два компонента работают нормально, но дублирование логики между ними вызывает сожаление. Кажется, что, хотя у них разный *внешний вид*, вы хотите повторно использовать логику между ними.
+Эти два компонента работают нормально, но дублирование логики между ними печально. Похоже, что, хотя у них и разный *визуальный вид*, вы хотите повторно использовать логику между ними.
### Извлечение собственного пользовательского хука из компонента {/*extracting-your-own-custom-hook-from-a-component*/}
-Представьте себе на мгновение, что, аналогично [`useState`](/reference/react/useState) и [`useEffect`](/reference/react/useEffect), существует встроенный хук `useOnlineStatus`. Тогда оба этих компонента можно было бы упростить, и вы могли бы удалить дублирование между ними:
+Представьте на мгновение, что, подобно [`useState`](/reference/react/useState) и [`useEffect`](/reference/react/useEffect), существует встроенный хук `useOnlineStatus`. Тогда оба этих компонента можно было бы упростить, и вы могли бы устранить дублирование между ними:
```js {2,7}
function StatusBar() {
@@ -125,7 +124,7 @@ function SaveButton() {
}
```
-Хотя такого встроенного хука нет, вы можете написать его сами. Объявите функцию с именем `useOnlineStatus` и переместите в нее весь дублирующийся код из компонентов, которые вы написали ранее:
+Хотя такого встроенного хука не существует, вы можете написать его сами. Объявите функцию с именем `useOnlineStatus` и переместите в нее весь дублирующийся код из ранее написанных вами компонентов:
```js {2-16}
function useOnlineStatus() {
@@ -148,7 +147,7 @@ function useOnlineStatus() {
}
```
-В конце функции верните `isOnline`. Это позволяет вашим компонентам читать это значение:
+В конце функции верните `isOnline`. Это позволит вашим компонентам читать это значение:
@@ -209,89 +208,89 @@ export function useOnlineStatus() {
-Убедитесь, что включение и выключение сети обновляет оба компонента.
+Убедитесь, что переключение сети вкл/выкл обновляет оба компонента.
-Теперь в ваших компонентах не так много повторяющейся логики. **Что еще важнее, код внутри них описывает *что они хотят сделать* (использовать статус сети!), а не *как это сделать* (подписавшись на события браузера).**
+Теперь в ваших компонентах стало меньше повторяющейся логики. **Что еще более важно, код внутри них описывает *что они хотят делать* (использовать статус сети!), а не *как это сделать* (подписываясь на события браузера).**
-Когда вы извлекаете логику в пользовательские хуки, вы можете скрыть неприятные детали того, как вы имеете дело с какой-либо внешней системой или API браузера. Код ваших компонентов выражает ваше намерение, а не реализацию.
+Когда вы извлекаете логику в пользовательские хуки, вы можете скрыть сложные детали того, как вы работаете с некоторой внешней системой или API браузера. Код ваших компонентов выражает ваше намерение, а не реализацию.
### Имена хуков всегда начинаются с `use` {/*hook-names-always-start-with-use*/}
-Приложения React состоят из компонентов. Компоненты состоят из хуков, будь то встроенные или пользовательские. Вы, вероятно, часто будете использовать пользовательские хуки, созданные другими, но иногда вы можете написать один сами!
+Приложения React строятся из компонентов. Компоненты строятся из хуков, как встроенных, так и пользовательских. Вы, вероятно, часто будете использовать пользовательские хуки, созданные другими, но иногда вы можете написать один сами!
-Вы должны следовать этим соглашениям об именах:
+Вы должны следовать этим соглашениям об именовании:
-1. **Имена компонентов React должны начинаться с заглавной буквы,** например, `StatusBar` и `SaveButton`. Компоненты React также должны возвращать что-то, что React умеет отображать, например, фрагмент JSX.
-2. **Имена хуков должны начинаться с `use`, за которым следует заглавная буква,** например, [`useState`](/reference/react/useState) (встроенный) или `useOnlineStatus` (пользовательский, как ранее на странице). Хуки могут возвращать произвольные значения.
+1. **Имена компонентов React должны начинаться с заглавной буквы,** как `StatusBar` и `SaveButton`. Компоненты React также должны возвращать что-то, что React знает, как отобразить, например, фрагмент JSX.
+2. **Имена хуков должны начинаться с `use`, за которым следует заглавная буква,** как [`useState`](/reference/react/useState) (встроенный) или `useOnlineStatus` (пользовательский, как показано ранее на странице). Хуки могут возвращать произвольные значения.
-Это соглашение гарантирует, что вы всегда можете посмотреть на компонент и узнать, где могут «скрываться» его состояние, эффекты и другие функции React. Например, если вы видите вызов функции `getColor()` внутри вашего компонента, вы можете быть уверены, что она никак не может содержать состояние React, потому что ее имя не начинается с `use`. Однако вызов функции, такой как `useOnlineStatus()`, скорее всего, будет содержать вызовы других хуков внутри!
+Это соглашение гарантирует, что вы всегда сможете взглянуть на компонент и понять, где могут «скрываться» его состояние, эффекты и другие функции React. Например, если вы видите вызов функции `getColor()` внутри вашего компонента, вы можете быть уверены, что она не может содержать состояние React, потому что ее имя не начинается с `use`. Однако вызов функции, такой как `useOnlineStatus()`, скорее всего, будет содержать вызовы других хуков!
-Если ваш линтер [настроен для React,](/learn/editor-setup#linting) он будет применять это соглашение об именах. Прокрутите вверх к песочнице выше и переименуйте `useOnlineStatus` в `getOnlineStatus`. Обратите внимание, что линтер больше не позволит вам вызывать `useState` или `useEffect` внутри него. Только хуки и компоненты могут вызывать другие хуки!
+Если ваш линтер [настроен для React,](/learn/editor-setup#linting) он будет обеспечивать соблюдение этого соглашения об именовании. Прокрутите вверх до песочницы выше и переименуйте `useOnlineStatus` в `getOnlineStatus`. Обратите внимание, что линтер больше не позволит вам вызывать `useState` или `useEffect` внутри него. Только хуки и компоненты могут вызывать другие хуки!
-#### Должны ли все функции, вызываемые во время рендеринга, начинаться с префикса use? {/*should-all-functions-called-during-rendering-start-with-the-use-prefix*/}
+#### Должны ли все функции, вызываемые во время рендеринга, начинаться с префикса `use`? {/*should-all-functions-called-during-rendering-start-with-the-use-prefix*/}
-Нет. Функции, которые не *вызывают* хуки, не должны *быть* хуками.
+Нет. Функции, которые *не вызывают* хуки, не должны *быть* хуками.
-Если ваша функция не вызывает никаких хуков, избегайте префикса `use`. Вместо этого напишите ее как обычную функцию *без* префикса `use`. Например, `useSorted` ниже не вызывает хуки, поэтому вместо этого вызовите ее `getSorted`:
+Если ваша функция не вызывает никаких хуков, избегайте префикса `use`. Вместо этого напишите ее как обычную функцию *без* префикса `use`. Например, `useSorted` ниже не вызывает хуков, поэтому назовите ее `getSorted` вместо этого:
```js
-// 🔴 Избегайте: хук, который не использует хуки
+// 🔴 Избегать: Хук, который не использует хуки
function useSorted(items) {
return items.slice().sort();
}
-// ✅ Хорошо: обычная функция, которая не использует хуки
+// ✅ Хорошо: Обычная функция, которая не использует хуки
function getSorted(items) {
return items.slice().sort();
}
```
-Это гарантирует, что ваш код может вызывать эту обычную функцию в любом месте, включая условия:
+Это гарантирует, что ваш код может вызывать эту обычную функцию где угодно, в том числе в условных выражениях:
```js
function List({ items, shouldSort }) {
let displayedItems = items;
if (shouldSort) {
- // ✅ Можно условно вызывать getSorted(), потому что это не хук
+ // ✅ Можно вызывать getSorted() условно, потому что это не хук
displayedItems = getSorted(items);
}
// ...
}
```
-Вы должны дать префикс `use` функции (и, таким образом, сделать ее хуком), если она использует хотя бы один хук внутри себя:
+Вы должны дать префикс `use` функции (и, следовательно, сделать ее хуком), если она вызывает хотя бы один хук внутри себя:
```js
-// ✅ Хорошо: хук, который использует другие хуки
+// ✅ Хорошо: Хук, который использует другие хуки
function useAuth() {
return useContext(Auth);
}
```
-Технически, это не применяется React. В принципе, вы можете сделать хук, который не вызывает другие хуки. Это часто сбивает с толку и ограничивает, поэтому лучше избегать этого шаблона. Однако могут быть редкие случаи, когда это полезно. Например, возможно, ваша функция сейчас не использует никаких хуков, но вы планируете добавить в нее какие-то вызовы хуков в будущем. Тогда имеет смысл назвать ее с префиксом `use`:
+Технически это не enforced React. В принципе, вы можете создать хук, который не вызывает других хуков. Это часто сбивает с толку и ограничивает, поэтому лучше избегать такого шаблона. Однако могут быть редкие случаи, когда это полезно. Например, возможно, ваша функция пока не использует никаких хуков, но вы планируете добавить в нее вызовы хуков в будущем. Тогда имеет смысл назвать ее с префиксом `use`:
```js {3-4}
-// ✅ Хорошо: хук, который, вероятно, будет использовать какие-то другие хуки позже
+// ✅ Хорошо: Хук, который, вероятно, будет использовать другие хуки позже
function useAuth() {
- // TODO: Замените этой строкой, когда будет реализована аутентификация:
+ // TODO: Заменить этой строкой, когда будет реализована аутентификация:
// return useContext(Auth);
return TEST_USER;
}
```
-Тогда компоненты не смогут вызывать его условно. Это станет важным, когда вы на самом деле добавите вызовы хуков внутрь. Если вы не планируете использовать хуки внутри (сейчас или позже), не делайте его хуком.
+Тогда компоненты не смогут вызывать ее условно. Это станет важным, когда вы фактически добавите вызовы хуков внутрь. Если вы не планируете использовать в ней хуки (сейчас или позже), не делайте ее хуком.
### Пользовательские хуки позволяют совместно использовать логику с состоянием, а не само состояние {/*custom-hooks-let-you-share-stateful-logic-not-state-itself*/}
-В предыдущем примере, когда вы включали и выключали сеть, оба компонента обновлялись вместе. Однако неправильно думать, что между ними совместно используется одна переменная состояния `isOnline`. Посмотрите на этот код:
+В предыдущем примере, когда вы включали и выключали сеть, оба компонента обновлялись одновременно. Однако ошибочно полагать, что одна переменная состояния `isOnline` разделяется между ними. Рассмотрим этот код:
```js {2,7}
function StatusBar() {
@@ -305,7 +304,7 @@ function SaveButton() {
}
```
-Он работает так же, как и до того, как вы извлекли дублирование:
+Это работает так же, как и до извлечения дублирования:
```js {2-5,10-13}
function StatusBar() {
@@ -325,7 +324,7 @@ function SaveButton() {
}
```
-Это две совершенно независимые переменные состояния и эффекты! Они оказались одного и того же значения в одно и то же время, потому что вы синхронизировали их с одним и тем же внешним значением (находится ли сеть в сети).
+Это две совершенно независимые переменные состояния и эффекты! Они случайно имели одинаковое значение в одно и то же время, потому что вы синхронизировали их с одним и тем же внешним значением (включена ли сеть).
Чтобы лучше проиллюстрировать это, нам понадобится другой пример. Рассмотрим этот компонент `Form`:
@@ -369,11 +368,11 @@ input { margin-left: 10px; }
-Существует некоторая повторяющаяся логика для каждого поля формы:
+Для каждого поля формы есть некоторая повторяющаяся логика:
-1. Существует часть состояния (`firstName` и `lastName`).
-2. Существует обработчик изменений (`handleFirstNameChange` и `handleLastNameChange`).
-3. Существует часть JSX, которая указывает атрибуты `value` и `onChange` для этого ввода.
+1. Есть часть состояния (`firstName` и `lastName`).
+1. Есть обработчик изменений (`handleFirstNameChange` и `handleLastNameChange`).
+1. Есть фрагмент JSX, который определяет атрибуты `value` и `onChange` для этого ввода.
Вы можете извлечь повторяющуюся логику в этот пользовательский хук `useFormInput`:
@@ -428,9 +427,9 @@ input { margin-left: 10px; }
-Обратите внимание, что он объявляет только *одну* переменную состояния с именем `value`.
+Обратите внимание, что он объявляет только *одну* переменную состояния под названием `value`.
-Однако компонент `Form` вызывает `useFormInput` *дважды:*
+Однако компонент `Form` вызывает `useFormInput` *два раза*:
```js
function Form() {
@@ -441,15 +440,15 @@ function Form() {
Вот почему это работает как объявление двух отдельных переменных состояния!
-**Пользовательские хуки позволяют совместно использовать *логику с состоянием*, но не *само состояние*. Каждый вызов хука полностью независим от каждого другого вызова того же хука.** Вот почему две песочницы выше полностью эквивалентны. Если хотите, прокрутитесь назад и сравните их. Поведение до и после извлечения пользовательского хука идентично.
+**Пользовательские хуки позволяют совместно использовать *логику с состоянием*, но не *само состояние*. Каждый вызов хука полностью независим от любого другого вызова того же хука.** Вот почему два приведенных выше песочницы полностью эквивалентны. Если хотите, прокрутите вверх и сравните их. Поведение до и после извлечения пользовательского хука идентично.
-Когда вам нужно совместно использовать само состояние между несколькими компонентами, [поднимите его и передайте вниз](/learn/sharing-state-between-components) вместо этого.
+Когда вам нужно совместно использовать само состояние между несколькими компонентами, [поднимите его вверх и передайте вниз](/learn/sharing-state-between-components) вместо этого.
## Передача реактивных значений между хуками {/*passing-reactive-values-between-hooks*/}
-Код внутри ваших пользовательских хуков будет перезапускаться во время каждого повторного рендеринга вашего компонента. Вот почему, как и компоненты, пользовательские хуки [должны быть чистыми.](/learn/keeping-components-pure) Думайте о коде пользовательских хуков как о части тела вашего компонента!
+Код внутри ваших пользовательских хуков будет перезапускаться при каждом повторном рендеринге вашего компонента. Именно поэтому, как и компоненты, пользовательские хуки [должны быть чистыми.](/learn/keeping-components-pure) Считайте код пользовательских хуков частью тела вашего компонента!
-Поскольку пользовательские хуки перерисовываются вместе с вашим компонентом, они всегда получают последние пропсы и состояние. Чтобы увидеть, что это значит, рассмотрим этот пример чат-комнаты. Измените URL-адрес сервера или чат-комнату:
+Поскольку пользовательские хуки перезапускаются вместе с вашим компонентом, они всегда получают последние пропсы и состояние. Чтобы увидеть, что это значит, рассмотрите пример чат-комнаты. Измените URL сервера или чат-комнату:
@@ -599,7 +598,7 @@ button { margin-left: 10px; }
-Когда вы изменяете `serverUrl` или `roomId`, эффект ["реагирует" на ваши изменения](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) и повторно синхронизируется. Вы можете сказать по сообщениям в консоли, что чат переподключается каждый раз, когда вы изменяете зависимости вашего эффекта.
+Когда вы изменяете `serverUrl` или `roomId`, эффект ["реагирует" на ваши изменения](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) и повторно синхронизируется. Вы можете увидеть по сообщениям в консоли, что чат переподключается каждый раз, когда вы изменяете зависимости вашего эффекта.
Теперь переместите код эффекта в пользовательский хук:
@@ -645,7 +644,7 @@ export default function ChatRoom({ roomId }) {
Это выглядит намного проще! (Но делает то же самое.)
-Обратите внимание, что логика *по-прежнему реагирует* на изменения пропсов и состояния. Попробуйте отредактировать URL-адрес сервера или выбранную комнату:
+Обратите внимание, что логика *по-прежнему реагирует* на изменения пропсов и состояния. Попробуйте отредактировать URL сервера или выбрать другую комнату:
@@ -820,7 +819,7 @@ export default function ChatRoom({ roomId }) {
// ...
```
-и передаете его в качестве входных данных другому хуку:
+и передаете его как входное значение другому хуку:
```js {6}
export default function ChatRoom({ roomId }) {
@@ -833,17 +832,17 @@ export default function ChatRoom({ roomId }) {
// ...
```
-Каждый раз, когда ваш компонент `ChatRoom` перерисовывается, он передает последние `roomId` и `serverUrl` вашему хуку. Вот почему ваш эффект переподключается к чату всякий раз, когда их значения отличаются после перерисовки. (Если вы когда-либо работали с программным обеспечением для обработки аудио или видео, объединение хуков подобным образом может напомнить вам о цепочке визуальных или звуковых эффектов. Как будто выход `useState` «поступает» во вход `useChatRoom`.)
+Каждый раз, когда ваш компонент `ChatRoom` повторно рендерится, он передает последний `roomId` и `serverUrl` вашему хуку. Вот почему ваш эффект переподключается к чату всякий раз, когда их значения отличаются после повторного рендеринга. (Если вы когда-либо работали с программным обеспечением для обработки аудио или видео, цепочка хуков, подобная этой, может напомнить вам цепочку визуальных или аудиоэффектов. Как будто вывод `useState` "поступает" во входные данные `useChatRoom`.)
### Передача обработчиков событий в пользовательские хуки {/*passing-event-handlers-to-custom-hooks*/}
-В этом разделе описывается **экспериментальный API, который еще не был выпущен** в стабильной версии React.
+Этот раздел описывает **экспериментальный API, который еще не был выпущен** в стабильной версии React.
-Когда вы начнете использовать `useChatRoom` в большем количестве компонентов, вы можете захотеть, чтобы компоненты настраивали его поведение. Например, в настоящее время логика того, что делать при поступлении сообщения, жестко закодирована внутри хука:
+Когда вы начнете использовать `useChatRoom` в большем количестве компонентов, вы, возможно, захотите позволить компонентам настраивать его поведение. Например, в настоящее время логика того, что делать при получении сообщения, жестко закодирована внутри хука:
```js {9-11}
export function useChatRoom({ serverUrl, roomId }) {
@@ -862,7 +861,7 @@ export function useChatRoom({ serverUrl, roomId }) {
}
```
-Допустим, вы хотите переместить эту логику обратно в свой компонент:
+Допустим, вы хотите переместить эту логику обратно в ваш компонент:
```js {7-9}
export default function ChatRoom({ roomId }) {
@@ -878,7 +877,7 @@ export default function ChatRoom({ roomId }) {
// ...
```
-Чтобы это работало, измените свой пользовательский хук, чтобы он принимал `onReceiveMessage` в качестве одного из его именованных параметров:
+Чтобы это заработало, измените ваш пользовательский хук так, чтобы он принимал `onReceiveMessage` как одну из своих именованных опций:
```js {1,10,13}
export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
@@ -893,13 +892,13 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
onReceiveMessage(msg);
});
return () => connection.disconnect();
- }, [roomId, serverUrl, onReceiveMessage]); // ✅ All dependencies declared
+ }, [roomId, serverUrl, onReceiveMessage]); // ✅ Все зависимости объявлены
}
```
Это будет работать, но есть еще одно улучшение, которое вы можете сделать, когда ваш пользовательский хук принимает обработчики событий.
-Добавление зависимости от `onReceiveMessage` не идеально, потому что это приведет к повторному подключению чата каждый раз, когда компонент перерисовывается. [Оберните этот обработчик событий в Effect Event, чтобы удалить его из зависимостей:](/learn/removing-effect-dependencies#wrapping-an-event-handler-from-the-props)
+Добавление зависимости от `onReceiveMessage` не является идеальным, потому что это приведет к повторному подключению чата каждый раз, когда компонент повторно рендерится. [Оберните этот обработчик событий в Event Effect, чтобы удалить его из зависимостей:](/learn/removing-effect-dependencies#wrapping-an-event-handler-from-the-props)
```js {1,4,5,15,18}
import { useEffect, useEffectEvent } from 'react';
@@ -919,11 +918,11 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
onMessage(msg);
});
return () => connection.disconnect();
- }, [roomId, serverUrl]); // ✅ All dependencies declared
+ }, [roomId, serverUrl]); // ✅ Все зависимости объявлены
}
```
-Теперь чат не будет переподключаться каждый раз, когда компонент `ChatRoom` перерисовывается. Вот полностью рабочий пример передачи обработчика событий в пользовательский хук, с которым вы можете поиграть:
+Теперь чат не будет переподключаться каждый раз, когда компонент `ChatRoom` повторно рендерится. Вот полностью рабочая демонстрация передачи обработчика событий в пользовательский хук, с которой вы можете поиграть:
@@ -967,4 +966,1527 @@ export default function ChatRoom({ roomId }) {
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
- showNotification('New message: ' + msg);
\ No newline at end of file
+ showNotification('New message: ' + msg);
+ }
+ });
+
+ return (
+ <>
+
+
Welcome to the {roomId} room!
+ >
+ );
+}
+```
+
+```js src/useChatRoom.js
+import { useEffect } from 'react';
+import { experimental_useEffectEvent as useEffectEvent } from 'react';
+import { createConnection } from './chat.js';
+
+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]);
+}
+```
+
+```js src/chat.js
+export function createConnection({ serverUrl, roomId }) {
+ // A real implementation would actually connect to the server
+ if (typeof serverUrl !== 'string') {
+ throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);
+ }
+ if (typeof roomId !== 'string') {
+ throw Error('Expected roomId to be a string. Received: ' + roomId);
+ }
+ let intervalId;
+ let messageCallback;
+ 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) {
+ throw Error('Cannot add the handler twice.');
+ }
+ if (event !== 'message') {
+ throw Error('Only "message" event is supported.');
+ }
+ messageCallback = callback;
+ },
+ };
+}
+```
+
+```js src/notifications.js
+import Toastify from 'toastify-js';
+import 'toastify-js/src/toastify.css';
+
+export function showNotification(message, theme = 'dark') {
+ Toastify({
+ text: message,
+ duration: 2000,
+ gravity: 'top',
+ position: 'right',
+ style: {
+ background: theme === 'dark' ? 'black' : 'white',
+ color: theme === 'dark' ? 'white' : 'black',
+ },
+ }).showToast();
+}
+```
+
+```json package.json hidden
+{
+ "dependencies": {
+ "react": "experimental",
+ "react-dom": "experimental",
+ "react-scripts": "latest",
+ "toastify-js": "1.12.0"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test --env=jsdom",
+ "eject": "react-scripts eject"
+ }
+}
+```
+
+```css
+input { display: block; margin-bottom: 20px; }
+button { margin-left: 10px; }
+```
+
+
+
+Обратите внимание, что вам больше не нужно знать, *как* работает `useChatRoom`, чтобы использовать его. Вы можете добавить его в любой другой компонент, передать любые другие опции, и он будет работать одинаково. В этом и заключается сила пользовательских хуков.
+
+## Когда использовать пользовательские хуки {/*when-to-use-custom-hooks*/}
+
+Вам не нужно извлекать пользовательский хук для каждого мелкого дублирующегося фрагмента кода. Некоторое дублирование — это нормально. Например, извлечение хука `useFormInput` для обертывания одного вызова `useState`, как показано ранее, вероятно, излишне.
+
+Однако, когда вы пишете эффект, подумайте, не будет ли понятнее обернуть его в пользовательский хук. [Эффекты вам не понадобятся очень часто](/learn/you-might-not-need-an-effect), поэтому, если вы пишете один, это означает, что вам нужно «выйти за пределы React», чтобы синхронизироваться с некоторой внешней системой или сделать что-то, для чего в React нет встроенного API. Обертывание его в пользовательский хук позволяет точно передать ваше намерение и то, как данные через него проходят.
+
+Например, рассмотрим компонент `ShippingForm`, который отображает два выпадающих списка: один показывает список городов, а другой — список районов в выбранном городе. Вы можете начать с кода, который выглядит примерно так:
+
+```js {3-16,20-35}
+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`:
+
+```js {2-18}
+function useData(url) {
+ const [data, setData] = useState(null);
+ useEffect(() => {
+ 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`:
+
+```js {2,4}
+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`, вы также предотвращаете добавление [ненужных зависимостей](/learn/removing-effect-dependencies) в него кем-то, кто работает над компонентом `ShippingForm`. Со временем большинство эффектов вашего приложения будут находиться в пользовательских хуках.
+
+
+
+#### Держите ваши пользовательские хуки сфокусированными на конкретных высокоуровневых сценариях использования {/*keep-your-custom-hooks-focused-on-concrete-high-level-use-cases*/}
+
+Начните с выбора имени для вашего пользовательского хука. Если вам трудно подобрать понятное имя, это может означать, что ваш эффект слишком сильно связан с остальной логикой вашего компонента и еще не готов к извлечению.
+
+В идеале имя вашего пользовательского хука должно быть достаточно понятным, чтобы даже человек, который не часто пишет код, мог примерно догадаться, что делает ваш пользовательский хук, что он принимает и что возвращает:
+
+* ✅ `useData(url)`
+* ✅ `useImpressionLog(eventName, extraData)`
+* ✅ `useChatRoom(options)`
+
+Когда вы синхронизируетесь с внешней системой, имя вашего пользовательского хука может быть более техническим и использовать жаргон, специфичный для этой системы. Это хорошо, если это будет понятно человеку, знакомому с этой системой:
+
+* ✅ `useMediaQuery(query)`
+* ✅ `useSocket(url)`
+* ✅ `useIntersectionObserver(ref, options)`
+
+**Держите пользовательские хуки сфокусированными на конкретных высокоуровневых сценариях использования.** Избегайте создания и использования пользовательских хуков «жизненного цикла», которые действуют как альтернативы и удобные обертки для самого API `useEffect`:
+
+* 🔴 `useMount(fn)`
+* 🔴 `useEffectOnce(fn)`
+* 🔴 `useUpdateEffect(fn)`
+
+Например, этот хук `useMount` пытается гарантировать, что некоторый код выполняется только «при монтировании»:
+
+```js {4-5,14-15}
+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 Hook useEffect имеет отсутствующую зависимость: 'fn'
+}
+```
+
+**Пользовательские хуки «жизненного цикла», такие как `useMount`, плохо вписываются в парадигму React.** Например, в этом примере кода есть ошибка (он не «реагирует» на изменения `roomId` или `serverUrl`), но линтер не предупредит вас об этом, потому что линтер проверяет только прямые вызовы `useEffect`. Он не будет знать о вашем хуке.
+
+Если вы пишете эффект, начните с прямого использования API React:
+
+```js
+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]);
+
+ // ...
+}
+```
+
+Затем вы можете (но не обязаны) извлекать пользовательские хуки для различных высокоуровневых сценариев использования:
+
+```js
+function ChatRoom({ roomId }) {
+ const [serverUrl, setServerUrl] = useState('https://localhost:1234');
+
+ // ✅ Отлично: пользовательские хуки, названные по их назначению
+ useChatRoom({ serverUrl, roomId });
+ useImpressionLog('visit_chat', { roomId });
+ // ...
+}
+```
+
+**Хороший пользовательский хук делает вызывающий код более декларативным, ограничивая то, что он делает.** Например, `useChatRoom(options)` может только подключаться к чат-комнате, а `useImpressionLog(eventName, extraData)` может только отправлять лог впечатлений в аналитику. Если API вашего пользовательского хука не ограничивает сценарии использования и является очень абстрактным, в долгосрочной перспективе он, вероятно, принесет больше проблем, чем решит.
+
+
+
+### Пользовательские хуки помогают мигрировать на лучшие паттерны {/*custom-hooks-help-you-migrate-to-better-patterns*/}
+
+Эффекты — это «[лазейка](/learn/escape-hatches)»: вы используете их, когда вам нужно «выйти за пределы React» и когда нет лучшего встроенного решения для вашего сценария использования. Со временем цель команды React — свести к минимуму количество эффектов в вашем приложении, предоставляя более конкретные решения для более конкретных проблем. Обертывание ваших эффектов в пользовательские хуки упрощает обновление вашего кода, когда эти решения становятся доступными.
+
+Вернемся к этому примеру:
+
+
+
+```js
+import { useOnlineStatus } from './useOnlineStatus.js';
+
+function StatusBar() {
+ const isOnline = useOnlineStatus();
+ return
{isOnline ? '✅ Online' : '❌ Disconnected'}
;
+}
+
+function SaveButton() {
+ const isOnline = useOnlineStatus();
+
+ function handleSaveClick() {
+ console.log('✅ Progress saved');
+ }
+
+ return (
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+ >
+ );
+}
+```
+
+```js src/useOnlineStatus.js active
+import { useState, useEffect } from 'react';
+
+export 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;
+}
+```
+
+
+
+В приведенном выше примере `useOnlineStatus` реализован с помощью пары [`useState`](/reference/react/useState) и [`useEffect`.](/reference/react/useEffect) Однако это не лучшее возможное решение. Существует ряд крайних случаев, которые он не учитывает. Например, он предполагает, что при монтировании компонента `isOnline` уже равно `true`, но это может быть неверно, если сеть уже отключилась. Вы можете использовать браузерный API [`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine) для проверки этого, но его прямое использование не сработает на сервере для генерации начального HTML. Короче говоря, этот код можно улучшить.
+
+React включает специальный API под названием [`useSyncExternalStore`](/reference/react/useSyncExternalStore), который решает все эти проблемы за вас. Вот ваш хук `useOnlineStatus`, переписанный с использованием этого нового API:
+
+
+
+```js
+import { useOnlineStatus } from './useOnlineStatus.js';
+
+function StatusBar() {
+ const isOnline = useOnlineStatus();
+ return
{isOnline ? '✅ Online' : '❌ Disconnected'}
;
+}
+
+function SaveButton() {
+ const isOnline = useOnlineStatus();
+
+ function handleSaveClick() {
+ console.log('✅ Progress saved');
+ }
+
+ return (
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+ >
+ );
+}
+```
+
+```js src/useOnlineStatus.js active
+import { useSyncExternalStore } from 'react';
+
+function subscribe(callback) {
+ window.addEventListener('online', callback);
+ window.addEventListener('offline', callback);
+ return () => {
+ window.removeEventListener('online', callback);
+ window.removeEventListener('offline', callback);
+ };
+}
+
+export function useOnlineStatus() {
+ return useSyncExternalStore(
+ subscribe,
+ () => navigator.onLine, // Как получить значение на клиенте
+ () => true // Как получить значение на сервере
+ );
+}
+
+```
+
+
+
+Обратите внимание, что вам **не пришлось менять ни один из компонентов**, чтобы выполнить эту миграцию:
+
+```js {2,7}
+function StatusBar() {
+ const isOnline = useOnlineStatus();
+ // ...
+}
+
+function SaveButton() {
+ const isOnline = useOnlineStatus();
+ // ...
+}
+```
+
+Это еще одна причина, по которой обертывание эффектов в пользовательские хуки часто бывает полезным:
+
+1. Вы делаете поток данных к вашим эффектам и от них очень явным.
+2. Вы позволяете вашим компонентам сосредоточиться на намерении, а не на точной реализации ваших эффектов.
+3. Когда React добавляет новые функции, вы можете удалить эти эффекты, не изменяя ни один из ваших компонентов.
+
+Подобно [дизайн-системе,](https://uxdesign.cc/everything-you-need-to-know-about-design-systems-54b109851969) вы можете обнаружить, что полезно начать извлекать общие идиомы из компонентов вашего приложения в пользовательские хуки. Это позволит вашим компонентам сосредоточиться на намерении и избежать частого написания необработанных эффектов. Многие отличные пользовательские хуки поддерживаются сообществом React.
+
+
+
+#### Предоставит ли React какое-либо встроенное решение для получения данных? {/*will-react-provide-any-built-in-solution-for-data-fetching*/}
+
+Мы все еще прорабатываем детали, но ожидаем, что в будущем вы будете писать получение данных так:
+
+```js {1,4,6}
+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` выше, для миграции на в конечном итоге рекомендуемый подход потребуется меньше изменений, чем если бы вы писали необработанные эффекты в каждом компоненте вручную. Однако старый подход все равно будет работать нормально, поэтому, если вы довольны написанием необработанных эффектов, вы можете продолжать это делать.
+
+
+
+### Существует несколько способов сделать это {/*there-is-more-than-one-way-to-do-it*/}
+
+Допустим, вы хотите реализовать анимацию плавного появления *с нуля*, используя браузерный API [`requestAnimationFrame`](https://developer.mozilla.org/ru/docs/Web/API/window/requestAnimationFrame). Вы можете начать с эффекта, который настраивает цикл анимации. Во время каждого кадра анимации вы можете изменять прозрачность DOM-узла, который [хранится в рефе](/learn/manipulating-the-dom-with-refs), пока она не достигнет `1`. Ваш код может выглядеть так:
+
+
+
+```js
+import { useState, useEffect, useRef } from 'react';
+
+function Welcome() {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const duration = 1000;
+ const node = ref.current;
+
+ let startTime = performance.now();
+ let frameId = null;
+
+ function onFrame(now) {
+ const timePassed = now - startTime;
+ const progress = Math.min(timePassed / duration, 1);
+ onProgress(progress);
+ if (progress < 1) {
+ // Нам нужно отрисовать еще кадры
+ frameId = requestAnimationFrame(onFrame);
+ }
+ }
+
+ function onProgress(progress) {
+ node.style.opacity = progress;
+ }
+
+ function start() {
+ onProgress(0);
+ startTime = performance.now();
+ frameId = requestAnimationFrame(onFrame);
+ }
+
+ function stop() {
+ cancelAnimationFrame(frameId);
+ startTime = null;
+ frameId = null;
+ }
+
+ start();
+ return () => stop();
+ }, []);
+
+ return (
+
+ );
+}
+
+export default function App() {
+ const [show, setShow] = useState(false);
+ return (
+ <>
+
+
+ {show && }
+ >
+ );
+}
+```
+
+```js src/useFadeIn.js
+import { useEffect } from 'react';
+
+export function useFadeIn(ref, duration) {
+ useEffect(() => {
+ const node = ref.current;
+
+ let startTime = performance.now();
+ let frameId = null;
+
+ function onFrame(now) {
+ const timePassed = now - startTime;
+ const progress = Math.min(timePassed / duration, 1);
+ onProgress(progress);
+ if (progress < 1) {
+ // Нам нужно отрисовать еще кадры
+ frameId = requestAnimationFrame(onFrame);
+ }
+ }
+
+ function onProgress(progress) {
+ node.style.opacity = progress;
+ }
+
+ function start() {
+ onProgress(0);
+ startTime = performance.now();
+ frameId = requestAnimationFrame(onFrame);
+ }
+
+ function stop() {
+ cancelAnimationFrame(frameId);
+ startTime = null;
+ frameId = null;
+ }
+
+ start();
+ return () => stop();
+ }, [ref, duration]);
+}
+```
+
+```css
+label, button { display: block; margin-bottom: 20px; }
+html, body { min-height: 300px; }
+.welcome {
+ opacity: 0;
+ color: white;
+ padding: 50px;
+ text-align: center;
+ font-size: 50px;
+ background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);
+}
+```
+
+
+
+Вы можете оставить код `useFadeIn` как есть, но вы также можете провести дальнейшую рефакторизацию. Например, вы можете вынести логику настройки цикла анимации из `useFadeIn` в отдельный пользовательский хук `useAnimationLoop`:
+
+
+
+```js
+import { useState, useEffect, useRef } from 'react';
+import { useFadeIn } from './useFadeIn.js';
+
+function Welcome() {
+ const ref = useRef(null);
+
+ useFadeIn(ref, 1000);
+
+ return (
+
+ Welcome
+
+ );
+}
+
+export default function App() {
+ const [show, setShow] = useState(false);
+ return (
+ <>
+
+
+ {show && }
+ >
+ );
+}
+```
+
+```js src/useFadeIn.js active
+import { useState, useEffect } from 'react';
+import { experimental_useEffectEvent as useEffectEvent } from 'react';
+
+export function useFadeIn(ref, duration) {
+ const [isRunning, setIsRunning] = useState(true);
+
+ useAnimationLoop(isRunning, (timePassed) => {
+ const progress = Math.min(timePassed / duration, 1);
+ ref.current.style.opacity = progress;
+ if (progress === 1) {
+ setIsRunning(false);
+ }
+ });
+}
+
+function useAnimationLoop(isRunning, drawFrame) {
+ const onFrame = useEffectEvent(drawFrame);
+
+ useEffect(() => {
+ if (!isRunning) {
+ return;
+ }
+
+ const startTime = performance.now();
+ let frameId = null;
+
+ function tick(now) {
+ const timePassed = now - startTime;
+ onFrame(timePassed);
+ frameId = requestAnimationFrame(tick);
+ }
+
+ tick();
+ return () => cancelAnimationFrame(frameId);
+ }, [isRunning]);
+}
+```
+
+```css
+label, button { display: block; margin-bottom: 20px; }
+html, body { min-height: 300px; }
+.welcome {
+ opacity: 0;
+ color: white;
+ padding: 50px;
+ text-align: center;
+ font-size: 50px;
+ background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);
+}
+```
+
+```json package.json hidden
+{
+ "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"
+ }
+}
+```
+
+
+
+Однако, вам *не обязательно* было это делать. Как и в случае с обычными функциями, в конечном итоге вы сами решаете, где проводить границы между различными частями вашего кода. Вы также могли бы выбрать совершенно другой подход. Вместо того чтобы хранить логику в эффекте, вы могли бы переместить большую часть императивной логики внутрь JavaScript [класса:](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Classes)
+
+
+
+```js
+import { useState, useEffect, useRef } from 'react';
+import { useFadeIn } from './useFadeIn.js';
+
+function Welcome() {
+ const ref = useRef(null);
+
+ useFadeIn(ref, 1000);
+
+ return (
+
+ Welcome
+
+ );
+}
+
+export default function App() {
+ const [show, setShow] = useState(false);
+ return (
+ <>
+
+
+ {show && }
+ >
+ );
+}
+```
+
+```js src/useFadeIn.js active
+import { useState, useEffect } from 'react';
+import { FadeInAnimation } from './animation.js';
+
+export function useFadeIn(ref, duration) {
+ useEffect(() => {
+ const animation = new FadeInAnimation(ref.current);
+ animation.start(duration);
+ return () => {
+ animation.stop();
+ };
+ }, [ref, duration]);
+}
+```
+
+```js src/animation.js
+export class FadeInAnimation {
+ constructor(node) {
+ this.node = node;
+ }
+ start(duration) {
+ this.duration = duration;
+ this.onProgress(0);
+ this.startTime = performance.now();
+ this.frameId = requestAnimationFrame(() => this.onFrame());
+ }
+ onFrame() {
+ const timePassed = performance.now() - this.startTime;
+ const progress = Math.min(timePassed / this.duration, 1);
+ this.onProgress(progress);
+ if (progress === 1) {
+ this.stop();
+ } else {
+ // Нам нужно отрисовать еще кадры
+ this.frameId = requestAnimationFrame(() => this.onFrame());
+ }
+ }
+ onProgress(progress) {
+ this.node.style.opacity = progress;
+ }
+ stop() {
+ cancelAnimationFrame(this.frameId);
+ this.startTime = null;
+ this.frameId = null;
+ this.duration = 0;
+ }
+}
+```
+
+```css
+label, button { display: block; margin-bottom: 20px; }
+html, body { min-height: 300px; }
+.welcome {
+ opacity: 0;
+ color: white;
+ padding: 50px;
+ text-align: center;
+ font-size: 50px;
+ background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);
+}
+```
+
+
+
+Эффекты позволяют вам связывать React с внешними системами. Чем больше координации между эффектами требуется (например, для цепочки нескольких анимаций), тем разумнее полностью вынести эту логику из эффектов и хуков, как в примере выше. Тогда код, который вы вынесли, *становится* «внешней системой». Это позволяет вашим эффектам оставаться простыми, поскольку им нужно только отправлять сообщения системе, которую вы переместили за пределы React.
+
+Приведенные примеры предполагают, что логика плавного появления должна быть написана на JavaScript. Однако, эта конкретная анимация плавного появления является как более простой, так и гораздо более эффективной для реализации с помощью обычной [CSS-анимации:](https://developer.mozilla.org/ru/docs/Web/CSS/CSS_Animations/Using_CSS_animations)
+
+
+
+```js
+import { useState, useEffect, useRef } from 'react';
+import './welcome.css';
+
+function Welcome() {
+ return (
+
+ Welcome
+
+ );
+}
+
+export default function App() {
+ const [show, setShow] = useState(false);
+ return (
+ <>
+
+
+ {show && }
+ >
+ );
+}
+```
+
+```css src/styles.css
+label, button { display: block; margin-bottom: 20px; }
+html, body { min-height: 300px; }
+```
+
+```css src/welcome.css active
+.welcome {
+ color: white;
+ padding: 50px;
+ text-align: center;
+ font-size: 50px;
+ background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);
+
+ animation: fadeIn 1000ms;
+}
+
+@keyframes fadeIn {
+ 0% { opacity: 0; }
+ 100% { opacity: 1; }
+}
+
+```
+
+
+
+Иногда вам даже не нужен хук!
+
+
+
+- Пользовательские хуки позволяют вам совместно использовать логику между компонентами.
+- Пользовательские хуки должны называться, начиная с `use`, за которым следует заглавная буква.
+- Пользовательские хуки разделяют только логику состояния, но не само состояние.
+- Вы можете передавать реактивные значения от одного хука к другому, и они будут оставаться актуальными.
+- Все хуки перезапускаются при каждом перерендере вашего компонента.
+- Код ваших пользовательских хуков должен быть чистым, как и код вашего компонента.
+- Оборачивайте обработчики событий, полученные пользовательскими хуками, в Хуки событий.
+- Не создавайте пользовательские хуки вроде `useMount`. Сохраняйте их назначение конкретным.
+- Вы сами решаете, как и где выбирать границы вашего кода.
+
+
+
+
+
+#### Извлеките хук `useCounter` {/*extract-a-usecounter-hook*/}
+
+Этот компонент использует переменную состояния и эффект для отображения числа, которое увеличивается каждую секунду. Извлеките эту логику в пользовательский хук под названием `useCounter`. Ваша цель — добиться того, чтобы реализация компонента `Counter` выглядела точно так:
+
+```js
+export default function Counter() {
+ const count = useCounter();
+ return
Seconds passed: {count}
;
+}
+```
+
+Вам нужно будет написать свой пользовательский хук в `useCounter.js` и импортировать его в файл `App.js`.
+
+
+
+```js
+import { useState, useEffect } from 'react';
+
+export default function Counter() {
+ const [count, setCount] = useState(0);
+ useEffect(() => {
+ const id = setInterval(() => {
+ setCount(c => c + 1);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+ return
Seconds passed: {count}
;
+}
+```
+
+```js src/useCounter.js
+// Напишите свой пользовательский хук здесь!
+```
+
+
+
+
+
+Ваш код должен выглядеть так:
+
+
+
+```js
+import { useCounter } from './useCounter.js';
+
+export default function Counter() {
+ const count = useCounter();
+ return
Seconds passed: {count}
;
+}
+```
+
+```js src/useCounter.js
+import { useState, useEffect } from 'react';
+
+export function useCounter() {
+ const [count, setCount] = useState(0);
+ useEffect(() => {
+ const id = setInterval(() => {
+ setCount(c => c + 1);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+ return count;
+}
+```
+
+
+
+Обратите внимание, что `App.js` больше не нужно импортировать `useState` или `useEffect`.
+
+
+
+#### Сделайте задержку счетчика настраиваемой {/*make-the-counter-delay-configurable*/}
+
+В этом примере есть переменная состояния `delay`, управляемая ползунком, но ее значение не используется. Передайте значение `delay` вашему пользовательскому хуку `useCounter` и измените хук `useCounter` так, чтобы он использовал переданный `delay` вместо жестко закодированного значения `1000` мс.
+
+
+
+```js
+import { useState } from 'react';
+import { useCounter } from './useCounter.js';
+
+export default function Counter() {
+ const [delay, setDelay] = useState(1000);
+ const count = useCounter();
+ return (
+ <>
+
+
+
Тиков: {count}
+ >
+ );
+}
+```
+
+```js src/useCounter.js
+import { useState, useEffect } from 'react';
+
+export function useCounter() {
+ const [count, setCount] = useState(0);
+ useEffect(() => {
+ const id = setInterval(() => {
+ setCount(c => c + 1);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+ return count;
+}
+```
+
+
+
+
+
+Передайте `delay` в хук с помощью `useCounter(delay)`. Затем, внутри хука, используйте `delay` вместо жестко закодированного значения `1000`. Вам нужно будет добавить `delay` в зависимости вашего эффекта. Это гарантирует, что изменение `delay` сбросит интервал.
+
+
+
+```js
+import { useState } from 'react';
+import { useCounter } from './useCounter.js';
+
+export default function Counter() {
+ const [delay, setDelay] = useState(1000);
+ const count = useCounter(delay);
+ return (
+ <>
+
+
+
Тиков: {count}
+ >
+ );
+}
+```
+
+```js src/useCounter.js
+import { useState, useEffect } from 'react';
+
+export function useCounter(delay) {
+ const [count, setCount] = useState(0);
+ useEffect(() => {
+ const id = setInterval(() => {
+ setCount(c => c + 1);
+ }, delay);
+ return () => clearInterval(id);
+ }, [delay]);
+ return count;
+}
+```
+
+
+
+
+
+#### Извлеките `useInterval` из `useCounter` {/*extract-useinterval-out-of-usecounter*/}
+
+В настоящее время ваш хук `useCounter` делает две вещи. Он настраивает интервал и также увеличивает переменную состояния при каждом тике интервала. Разделите логику настройки интервала на отдельный хук под названием `useInterval`. Он должен принимать два аргумента: колбэк `onTick` и `delay`. После этого изменения реализация вашего `useCounter` должна выглядеть так:
+
+```js
+export function useCounter(delay) {
+ const [count, setCount] = useState(0);
+ useInterval(() => {
+ setCount(c => c + 1);
+ }, delay);
+ return count;
+}
+```
+
+Напишите `useInterval` в файле `useInterval.js` и импортируйте его в файл `useCounter.js`.
+
+
+
+```js
+import { useCounter } from './useCounter.js';
+
+export default function Counter() {
+ const count = useCounter(1000);
+ return
Seconds passed: {count}
;
+}
+```
+
+```js src/useCounter.js
+import { useState, useEffect } from 'react';
+
+export function useCounter(delay) {
+ const [count, setCount] = useState(0);
+ useEffect(() => {
+ const id = setInterval(() => {
+ setCount(c => c + 1);
+ }, delay);
+ return () => clearInterval(id);
+ }, [delay]);
+ return count;
+}
+```
+
+```js src/useInterval.js
+// Напишите свой хук здесь!
+```
+
+
+
+
+
+Логика внутри `useInterval` должна настраивать и очищать интервал. Ей не нужно делать ничего другого.
+
+
+
+```js
+import { useCounter } from './useCounter.js';
+
+export default function Counter() {
+ const count = useCounter(1000);
+ return
Seconds passed: {count}
;
+}
+```
+
+```js src/useCounter.js
+import { useState } from 'react';
+import { useInterval } from './useInterval.js';
+
+export function useCounter(delay) {
+ const [count, setCount] = useState(0);
+ useInterval(() => {
+ setCount(c => c + 1);
+ }, delay);
+ return count;
+}
+```
+
+```js src/useInterval.js active
+import { useEffect } from 'react';
+
+export function useInterval(onTick, delay) {
+ useEffect(() => {
+ const id = setInterval(onTick, delay);
+ return () => clearInterval(id);
+ }, [onTick, delay]);
+}
+```
+
+
+
+Обратите внимание, что в этом решении есть небольшая проблема, которую вы решите в следующем задании.
+
+
+
+#### Исправьте сбрасывающийся интервал {/*fix-a-resetting-interval*/}
+
+В этом примере есть *два* отдельных интервала.
+
+Компонент `App` вызывает `useCounter`, который вызывает `useInterval` для обновления счетчика каждую секунду. Но компонент `App` *также* вызывает `useInterval` для случайного обновления цвета фона страницы каждые две секунды.
+
+По какой-то причине колбэк, обновляющий цвет фона страницы, никогда не выполняется. Добавьте несколько логов внутрь `useInterval`:
+
+```js {2,5}
+ useEffect(() => {
+ console.log('✅ Setting up an interval with delay ', delay)
+ const id = setInterval(onTick, delay);
+ return () => {
+ console.log('❌ Clearing an interval with delay ', delay)
+ clearInterval(id);
+ };
+ }, [onTick, delay]);
+```
+
+Соответствуют ли логи тому, что вы ожидаете увидеть? Если некоторые из ваших эффектов, кажется, повторно синхронизируются без необходимости, можете ли вы предположить, какая зависимость вызывает это? Есть ли какой-нибудь способ [удалить эту зависимость](/learn/removing-effect-dependencies) из вашего эффекта?
+
+После исправления проблемы вы должны ожидать, что цвет фона страницы будет обновляться каждые две секунды.
+
+
+
+Похоже, ваш хук `useInterval` принимает в качестве аргумента обработчик событий. Можете ли вы придумать способ обернуть этот обработчик событий так, чтобы он не требовал зависимости от вашего эффекта?
+
+
+
+
+
+```json package.json hidden
+{
+ "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"
+ }
+}
+```
+
+```js
+import { useCounter } from './useCounter.js';
+import { useInterval } from './useInterval.js';
+
+export default function Counter() {
+ const count = useCounter(1000);
+
+ useInterval(() => {
+ const randomColor = `hsla(${Math.random() * 360}, 100%, 50%, 0.2)`;
+ document.body.style.backgroundColor = randomColor;
+ }, 2000);
+
+ return
Seconds passed: {count}
;
+}
+```
+
+```js src/useCounter.js
+import { useState } from 'react';
+import { useInterval } from './useInterval.js';
+
+export function useCounter(delay) {
+ const [count, setCount] = useState(0);
+ useInterval(() => {
+ setCount(c => c + 1);
+ }, delay);
+ return count;
+}
+```
+
+```js src/useInterval.js
+import { useEffect } from 'react';
+import { experimental_useEffectEvent as useEffectEvent } from 'react';
+
+export function useInterval(onTick, delay) {
+ useEffect(() => {
+ const id = setInterval(onTick, delay);
+ return () => {
+ clearInterval(id);
+ };
+ }, [onTick, delay]);
+}
+```
+
+
+
+
+
+Внутри `useInterval` оберните колбэк тика в Хук события, как вы делали [ранее на этой странице.](/learn/reusing-logic-with-custom-hooks#passing-event-handlers-to-custom-hooks)
+
+Это позволит вам опустить `onTick` из зависимостей вашего эффекта. Эффект не будет повторно синхронизироваться при каждом перерендере компонента, поэтому интервал изменения цвета фона страницы не будет сбрасываться каждую секунду до того, как он успеет сработать.
+
+С этим изменением оба интервала работают как положено и не мешают друг другу:
+
+
+
+```json package.json hidden
+{
+ "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"
+ }
+}
+```
+
+
+```js
+import { useCounter } from './useCounter.js';
+import { useInterval } from './useInterval.js';
+
+export default function Counter() {
+ const count = useCounter(1000);
+
+ useInterval(() => {
+ const randomColor = `hsla(${Math.random() * 360}, 100%, 50%, 0.2)`;
+ document.body.style.backgroundColor = randomColor;
+ }, 2000);
+
+ return
Seconds passed: {count}
;
+}
+```
+
+```js src/useCounter.js
+import { useState } from 'react';
+import { useInterval } from './useInterval.js';
+
+export function useCounter(delay) {
+ const [count, setCount] = useState(0);
+ useInterval(() => {
+ setCount(c => c + 1);
+ }, delay);
+ return count;
+}
+```
+
+```js src/useInterval.js active
+import { useEffect } from 'react';
+import { experimental_useEffectEvent as useEffectEvent } from 'react';
+
+export function useInterval(callback, delay) {
+ const onTick = useEffectEvent(callback);
+ useEffect(() => {
+ const id = setInterval(onTick, delay);
+ return () => clearInterval(id);
+ }, [delay]);
+}
+```
+
+
+
+
+
+#### Реализуйте эффект «догоняющей» анимации {/*implement-a-staggering-movement*/}
+
+В этом примере хук `usePointerPosition()` отслеживает текущее положение указателя. Попробуйте переместить курсор или палец по области предварительного просмотра, и вы увидите, как красная точка следует за вашим движением. Её положение сохраняется в переменной `pos1`.
+
+На самом деле отрисовывается пять (!) разных красных точек. Вы их не видите, потому что в данный момент все они находятся в одном и том же положении. Это то, что вам нужно исправить. Вместо этого вы хотите реализовать «догоняющую» анимацию: каждая точка должна «следовать» за траекторией предыдущей точки. Например, если вы быстро переместите курсор, первая точка должна немедленно последовать за ним, вторая точка должна следовать за первой с небольшой задержкой, третья точка должна следовать за второй и так далее.
+
+Вам нужно реализовать пользовательский хук `useDelayedValue`. Его текущая реализация возвращает значение `value`, переданное ему. Вместо этого вы хотите возвращать значение, полученное `delay` миллисекунд назад. Вам может понадобиться некоторое состояние и эффект для этого.
+
+После того как вы реализуете `useDelayedValue`, вы увидите, как точки движутся, следуя друг за другом.
+
+
+
+Вам нужно будет сохранить `delayedValue` как переменную состояния внутри вашего пользовательского хука. Когда `value` изменится, вы захотите запустить эффект. Этот эффект должен обновить `delayedValue` через `delay` миллисекунд. Вам может быть полезно вызвать `setTimeout`.
+
+Нужна ли этому эффекту очистка? Почему да или почему нет?
+
+
+
+
+
+```js
+import { usePointerPosition } from './usePointerPosition.js';
+
+function useDelayedValue(value, delay) {
+ // TODO: Implement this Hook
+ return value;
+}
+
+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(pos3, 50);
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
+
+function Dot({ position, opacity }) {
+ return (
+
+ );
+}
+```
+
+```js src/usePointerPosition.js
+import { useState, useEffect } from 'react';
+
+export function usePointerPosition() {
+ 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 position;
+}
+```
+
+```css
+body { min-height: 300px; }
+```
+
+
+
+
+
+Вот рабочая версия. Вы сохраняете `delayedValue` как переменную состояния. Когда `value` обновляется, ваш эффект планирует тайм-аут для обновления `delayedValue`. Именно поэтому `delayedValue` всегда «отстает» от фактического `value`.
+
+
+
+```js
+import { useState, useEffect } from 'react';
+import { usePointerPosition } from './usePointerPosition.js';
+
+function useDelayedValue(value, delay) {
+ const [delayedValue, setDelayedValue] = useState(value);
+
+ useEffect(() => {
+ setTimeout(() => {
+ setDelayedValue(value);
+ }, delay);
+ }, [value, delay]);
+
+ return delayedValue;
+}
+
+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(pos3, 50);
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
+
+function Dot({ position, opacity }) {
+ return (
+
+ );
+}
+```
+
+```js src/usePointerPosition.js
+import { useState, useEffect } from 'react';
+
+export function usePointerPosition() {
+ 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 position;
+}
+```
+
+```css
+body { min-height: 300px; }
+```
+
+
+
+Обратите внимание, что этому эффекту *не* нужна очистка. Если бы вы вызвали `clearTimeout` в функции очистки, то каждый раз, когда `value` изменяется, он сбрасывал бы уже запланированный тайм-аут. Чтобы движение было непрерывным, вы хотите, чтобы все тайм-ауты срабатывали.
+
+
+
+
\ No newline at end of file
From 00762ec4f7c2992faddf313322aa0b157c6ab2a5 Mon Sep 17 00:00:00 2001
From: nivaldo
Date: Mon, 18 May 2026 14:53:36 -0300
Subject: [PATCH 3/3] chore: keep frontmatter title as-is
---
src/content/learn/reusing-logic-with-custom-hooks.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md
index 126c18f7ed..0f5da06407 100644
--- a/src/content/learn/reusing-logic-with-custom-hooks.md
+++ b/src/content/learn/reusing-logic-with-custom-hooks.md
@@ -1,6 +1,7 @@
---
-title: 'Повторное использование логики с помощью пользовательских хуков'
+title: 'Reusing Logic with Custom Hooks'
---
+
React поставляется с несколькими встроенными хуками, такими как `useState`, `useContext` и `useEffect`. Иногда вам может захотеться иметь хук для более специфической цели: например, для получения данных, отслеживания статуса пользователя (онлайн/офлайн) или подключения к чату. Возможно, вы не найдете таких хуков в React, но вы можете создавать свои собственные хуки для нужд вашего приложения.
@@ -2489,4 +2490,4 @@ body { min-height: 300px; }
-
\ No newline at end of file
+