From 8e7f385b5398e27fef39c74198f4f1bb2ddd055a Mon Sep 17 00:00:00 2001 From: yanyue Date: Wed, 26 Apr 2023 17:35:57 +0800 Subject: [PATCH 01/14] docs(cn): reusing-logic-with-custom-hooks --- .../learn/reusing-logic-with-custom-hooks.md | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index 679a9bac21..e04b22c2c7 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: 使用自定义Hook重用逻辑 --- -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带有一些内置的Hook,比如`useState`, `useContext`和`useEffect`。有时候你需要一个用途更加特定的Hook:比如远程获取数据,追踪用户是否在线,或者连接一个聊天室。在React中可能找不到这些Hook,但是你可以根据自己应用的需求取创建自己的Hook。 -- 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 +- 什么是自定义Hook,以及如何写自己的Hook +- 如何在组件间重用逻辑 +- 如何命名和构建你的自定义Hook +- 提取自定义Hook的时机和原因 -## Custom Hooks: Sharing logic between components {/*custom-hooks-sharing-logic-between-components*/} +## 自定义Hook:在组件间共享逻辑 {/*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. 一个追踪网络是否在线的state。 +2. 一个订阅全局[`在线`](https://developer.mozilla.org/en-US/docs/Web/API/Window/online_event)和[`离线`](https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event)事件以及更新上述state的Effect。 -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. +现在假设你想要在一个不同的组件里**也**使用这段相同的逻辑。你想实现一个Save按钮,当网络离线时,这个按钮会变成不可用,并且显示"Reconnecting..."而不是"Save"。 -To start, you can copy and paste the `isOnline` state and the Effect into `SaveButton`: +你可以通过复制和粘贴`isOnline` state和Effect到`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*/} +### 从组件中提取出你的自定义Hook {/*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`Hook。那么就可以简化这两个组件并且移除他们之间的重复部分: ```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: +尽管目前没有这样的内置Hook,但是你可以自己写。声明一个`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,78 +209,78 @@ 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. +当提取逻辑到自定义Hook中时,你可以隐藏如何处理一些外部系统或者浏览器API的艰难细节。组件中的代码表达的是你的目的而不是实现。 -### Hook names always start with `use` {/*hook-names-always-start-with-use*/} +### Hook的名称必须永远以 `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 应用是由组件构建的。组件是由内置的或者自定义的Hook构建的。你可能经常使用别人创建的自定义Hook,但是偶尔也可能要自己写! -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. **Hook的名称必须以 `use`开头,后面跟一个大写字母,** 像 [`useState`](/reference/react/useState) (内置) or `useOnlineStatus` (像文章早前的自定义Hook)。 Hook可能会返回任意值。 -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! +这个公约保证了你始终可以查看组件并且知道它的state,Effect以及其他的React特性可能“隐藏”在哪里。比如,如果你在组件内部看见`getColor()`的函数调用,你可以确定它内部不可能包含React state,因为它的名称没有以`use`开头。但是,像`useOnlineStatus()`这样的函数调用将极有可能包含对内部其他Hook的调用! -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)检查工具,它会强制执行这个命名公约。滑动到上面的sandbox,并将`useOnlineStatus`重命名为`getOnlineStatus`。注意检查工具将不会再允许你在内部调用`useState` 或者 `useEffect`。只有Hook和组件可以调用其他Hook! -#### 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. +不是。没有**调用**Hook的函数不需要**成为**Hook。 -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: +如果你的函数没有调用任何Hook,请避免使用`use` 前缀。 而是**不带**`use`前缀,把它作为常规函数去写。比如, 下面的`useSorted` 没有调用Hook, 所以叫它 `getSorted`: ```js -// 🔴 Avoid: A Hook that doesn't use Hooks +// 🔴 避免: 没有调用其他Hook的Hook function useSorted(items) { return items.slice().sort(); } -// ✅ Good: A regular function that doesn't use Hooks +// ✅ Good: 没有使用Hook的常规函数 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()是没问题的,因为它不是Hook 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: +如果内部至少使用了一个Hook,你应该给这个函数`use`前缀(从而让它成为一个Hook): ```js -// ✅ Good: A Hook that uses other Hooks +// ✅ Good: 一个使用了其他Hook的Hook 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强制的。原则上,你可以写一个不调用其他Hook的Hook。这常常会令人迷惑且受到限制,所以最好是避免那种方式。但是在极少一些场景下,它可能是有帮助的。比如,也许你的函数现在没有使用任何Hook,但是计划在未来会添加一些Hook调用。那么使用`use`前缀给它命名就很有意义: ```js {3-4} -// ✅ Good: A Hook that will likely use some other Hooks later +// ✅ Good: 之后即将可能使用一些其他Hook的一个Hook function useAuth() { - // TODO: Replace with this line when authentication is implemented: - // return useContext(Auth); + // TODO: 当认证功能实现以后,替换这一行: + // 返回 useContext(Auth); return TEST_USER; } ``` From f0fc21ee730cc68b6a89ce65dc28328906dbfd84 Mon Sep 17 00:00:00 2001 From: yanyue Date: Wed, 26 Apr 2023 18:00:06 +0800 Subject: [PATCH 02/14] docs(cn): update reusing-logic-with-custom-hooks --- src/content/learn/reuse.md | 2492 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2492 insertions(+) create mode 100644 src/content/learn/reuse.md diff --git a/src/content/learn/reuse.md b/src/content/learn/reuse.md new file mode 100644 index 0000000000..679a9bac21 --- /dev/null +++ b/src/content/learn/reuse.md @@ -0,0 +1,2492 @@ +--- +title: 'Reusing Logic with Custom Hooks' +--- + + + +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. + + + + + +- 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*/} + +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. + +This will keep your component [synchronized](/learn/synchronizing-with-effects) with the network status. You might start with something like this: + + + +```js +import { useState, useEffect } from 'react'; + +export default function StatusBar() { + const [isOnline, setIsOnline] = useState(true); + useEffect(() => { + function handleOnline() { + setIsOnline(true); + } + function handleOffline() { + setIsOnline(false); + } + window.addEventListener('online', handleOnline); + window.addEventListener('offline', handleOffline); + return () => { + window.removeEventListener('online', handleOnline); + window.removeEventListener('offline', handleOffline); + }; + }, []); + + return

{isOnline ? '✅ Online' : '❌ Disconnected'}

; +} +``` + +
+ +Try turning your network on and off, and notice how this `StatusBar` updates in response to your actions. + +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`: + + + +```js +import { useState, useEffect } from 'react'; + +export default function SaveButton() { + const [isOnline, setIsOnline] = useState(true); + useEffect(() => { + function handleOnline() { + setIsOnline(true); + } + function handleOffline() { + setIsOnline(false); + } + window.addEventListener('online', handleOnline); + window.addEventListener('offline', handleOffline); + return () => { + window.removeEventListener('online', handleOnline); + window.removeEventListener('offline', handleOffline); + }; + }, []); + + function handleSaveClick() { + console.log('✅ Progress saved'); + } + + return ( + + ); +} +``` + + + +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*/} + +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: + +```js {2,7} +function StatusBar() { + const isOnline = useOnlineStatus(); + return

{isOnline ? '✅ Online' : '❌ Disconnected'}

; +} + +function SaveButton() { + const isOnline = useOnlineStatus(); + + function handleSaveClick() { + console.log('✅ Progress saved'); + } + + return ( + + ); +} +``` + +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: + +```js {2-16} +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; +} +``` + +At the end of the function, return `isOnline`. This lets your components read that value: + + + +```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 useOnlineStatus.js +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; +} +``` + +
+ +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. + +### Hook names always start with `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! + +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. + +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! + + + +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! + + + + + +#### Should all functions called during rendering start with the use prefix? {/*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: + +```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 + 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: + +```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: + +```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: + // 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*/} + +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: + +```js {2,7} +function StatusBar() { + const isOnline = useOnlineStatus(); + // ... +} + +function SaveButton() { + const isOnline = useOnlineStatus(); + // ... +} +``` + +It works the same way as before you extracted the duplication: + +```js {2-5,10-13} +function StatusBar() { + const [isOnline, setIsOnline] = useState(true); + useEffect(() => { + // ... + }, []); + // ... +} + +function SaveButton() { + const [isOnline, setIsOnline] = useState(true); + useEffect(() => { + // ... + }, []); + // ... +} +``` + +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: + + + +```js +import { useState } from 'react'; + +export default function Form() { + const [firstName, setFirstName] = useState('Mary'); + const [lastName, setLastName] = useState('Poppins'); + + function handleFirstNameChange(e) { + setFirstName(e.target.value); + } + + function handleLastNameChange(e) { + setLastName(e.target.value); + } + + return ( + <> + + +

Good morning, {firstName} {lastName}.

+ + ); +} +``` + +```css +label { display: block; } +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. + +You can extract the repetitive logic into this `useFormInput` custom Hook: + + + +```js +import { useFormInput } from './useFormInput.js'; + +export default function Form() { + const firstNameProps = useFormInput('Mary'); + const lastNameProps = useFormInput('Poppins'); + + return ( + <> + + +

Good morning, {firstNameProps.value} {lastNameProps.value}.

+ + ); +} +``` + +```js useFormInput.js active +import { useState } from 'react'; + +export function useFormInput(initialValue) { + const [value, setValue] = useState(initialValue); + + function handleChange(e) { + setValue(e.target.value); + } + + const inputProps = { + value: value, + onChange: handleChange + }; + + return inputProps; +} +``` + +```css +label { display: block; } +input { margin-left: 10px; } +``` + +
+ +Notice that it only declares *one* state variable called `value`. + +However, the `Form` component calls `useFormInput` *two times:* + +```js +function Form() { + const firstNameProps = useFormInput('Mary'); + const lastNameProps = useFormInput('Poppins'); + // ... +``` + +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. + +## 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! + +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: + + + +```js App.js +import { useState } from 'react'; +import ChatRoom from './ChatRoom.js'; + +export default function App() { + const [roomId, setRoomId] = useState('general'); + return ( + <> + +
+ + + ); +} +``` + +```js ChatRoom.js active +import { useState, useEffect } from 'react'; +import { createConnection } from './chat.js'; +import { showNotification } from './notifications.js'; + +export default function ChatRoom({ roomId }) { + const [serverUrl, setServerUrl] = useState('https://localhost:1234'); + + useEffect(() => { + const options = { + serverUrl: serverUrl, + roomId: roomId + }; + const connection = createConnection(options); + connection.on('message', (msg) => { + showNotification('New message: ' + msg); + }); + connection.connect(); + return () => connection.disconnect(); + }, [roomId, serverUrl]); + + return ( + <> + +

Welcome to the {roomId} room!

+ + ); +} +``` + +```js 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 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": "latest", + "react-dom": "latest", + "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; } +``` + +
+ +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. + +Now move the Effect's code into a custom Hook: + +```js {2-13} +export function useChatRoom({ serverUrl, roomId }) { + useEffect(() => { + const options = { + serverUrl: serverUrl, + roomId: roomId + }; + const connection = createConnection(options); + connection.connect(); + connection.on('message', (msg) => { + showNotification('New message: ' + msg); + }); + return () => connection.disconnect(); + }, [roomId, serverUrl]); +} +``` + +This lets your `ChatRoom` component call your custom Hook without worrying about how it works inside: + +```js {4-7} +export default function ChatRoom({ roomId }) { + const [serverUrl, setServerUrl] = useState('https://localhost:1234'); + + useChatRoom({ + roomId: roomId, + serverUrl: serverUrl + }); + + return ( + <> + +

Welcome to the {roomId} room!

+ + ); +} +``` + +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: + + + +```js App.js +import { useState } from 'react'; +import ChatRoom from './ChatRoom.js'; + +export default function App() { + const [roomId, setRoomId] = useState('general'); + return ( + <> + +
+ + + ); +} +``` + +```js ChatRoom.js active +import { useState } from 'react'; +import { useChatRoom } from './useChatRoom.js'; + +export default function ChatRoom({ roomId }) { + const [serverUrl, setServerUrl] = useState('https://localhost:1234'); + + useChatRoom({ + roomId: roomId, + serverUrl: serverUrl + }); + + return ( + <> + +

Welcome to the {roomId} room!

+ + ); +} +``` + +```js useChatRoom.js +import { useEffect } from 'react'; +import { createConnection } from './chat.js'; +import { showNotification } from './notifications.js'; + +export function useChatRoom({ serverUrl, roomId }) { + useEffect(() => { + const options = { + serverUrl: serverUrl, + roomId: roomId + }; + const connection = createConnection(options); + connection.connect(); + connection.on('message', (msg) => { + showNotification('New message: ' + msg); + }); + return () => connection.disconnect(); + }, [roomId, serverUrl]); +} +``` + +```js 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 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": "latest", + "react-dom": "latest", + "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're taking the return value of one Hook: + +```js {2} +export default function ChatRoom({ roomId }) { + const [serverUrl, setServerUrl] = useState('https://localhost:1234'); + + useChatRoom({ + roomId: roomId, + serverUrl: serverUrl + }); + // ... +``` + +and pass it as an input to another Hook: + +```js {6} +export default function ChatRoom({ roomId }) { + const [serverUrl, setServerUrl] = useState('https://localhost:1234'); + + useChatRoom({ + roomId: roomId, + serverUrl: serverUrl + }); + // ... +``` + +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`.) + +### 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. + + + +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: + +```js {9-11} +export function useChatRoom({ serverUrl, roomId }) { + useEffect(() => { + const options = { + serverUrl: serverUrl, + roomId: roomId + }; + const connection = createConnection(options); + connection.connect(); + connection.on('message', (msg) => { + showNotification('New message: ' + msg); + }); + return () => connection.disconnect(); + }, [roomId, serverUrl]); +} +``` + +Let's say you want to move this logic back to your component: + +```js {7-9} +export default function ChatRoom({ roomId }) { + const [serverUrl, setServerUrl] = useState('https://localhost:1234'); + + useChatRoom({ + roomId: roomId, + serverUrl: serverUrl, + onReceiveMessage(msg) { + showNotification('New message: ' + msg); + } + }); + // ... +``` + +To make this work, change your custom Hook to take `onReceiveMessage` as one of its named options: + +```js {1,10,13} +export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { + useEffect(() => { + const options = { + serverUrl: serverUrl, + roomId: roomId + }; + const connection = createConnection(options); + connection.connect(); + connection.on('message', (msg) => { + onReceiveMessage(msg); + }); + return () => connection.disconnect(); + }, [roomId, serverUrl, onReceiveMessage]); // ✅ All dependencies declared +} +``` + +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) + +```js {1,4,5,15,18} +import { useEffect, useEffectEvent } from 'react'; +// ... + +export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { + const onMessage = useEffectEvent(onReceiveMessage); + + useEffect(() => { + const options = { + serverUrl: serverUrl, + roomId: roomId + }; + const connection = createConnection(options); + connection.connect(); + connection.on('message', (msg) => { + onMessage(msg); + }); + return () => connection.disconnect(); + }, [roomId, serverUrl]); // ✅ All dependencies declared +} +``` + +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: + + + +```js App.js +import { useState } from 'react'; +import ChatRoom from './ChatRoom.js'; + +export default function App() { + const [roomId, setRoomId] = useState('general'); + return ( + <> + +
+ + + ); +} +``` + +```js ChatRoom.js active +import { useState } from 'react'; +import { useChatRoom } from './useChatRoom.js'; +import { showNotification } from './notifications.js'; + +export default function ChatRoom({ roomId }) { + const [serverUrl, setServerUrl] = useState('https://localhost:1234'); + + useChatRoom({ + roomId: roomId, + serverUrl: serverUrl, + onReceiveMessage(msg) { + showNotification('New message: ' + msg); + } + }); + + return ( + <> + +

Welcome to the {roomId} room!

+ + ); +} +``` + +```js 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 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 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 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. + +Luckily, React 18 includes a dedicated API called [`useSyncExternalStore`](/reference/react/useSyncExternalStore) which takes care of all of these problems for you. Here is how 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 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 ( +

+ Welcome +

+ ); +} + +export default function App() { + const [show, setShow] = useState(false); + return ( + <> + +
+ {show && } + + ); +} +``` + +```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%); +} +``` + +
+ +To make the component more readable, you might extract the logic into a `useFadeIn` custom 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 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 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 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 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 styles.css +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } +``` + +```css 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 `Counter.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 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 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 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 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 { useState } from 'react'; +import { useCounter } from './useCounter.js'; + +export default function Counter() { + const count = useCounter(1000); + return

Seconds passed: {count}

; +} +``` + +```js 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 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 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 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 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 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 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 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 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 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. + + + + From 5e363a977b27a9a7e833801373adc2786eceb633 Mon Sep 17 00:00:00 2001 From: yanyue Date: Wed, 26 Apr 2023 18:01:23 +0800 Subject: [PATCH 03/14] docs(cn): update reusing-logic-with-custom-hooks --- src/content/learn/reusing-logic-with-custom-hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index e04b22c2c7..11950e694a 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -285,7 +285,7 @@ function useAuth() { } ``` -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. +接下来组件就不能在条件分支里调用这个函数。当你在里面添加了Hook调用时,这一点将变得很重要。如果你没有计划在内部使用Hook(现在或者之后),就不要让它成为一个Hook。 From 7c2583d6abd6e6464a55401809a128610af911ea Mon Sep 17 00:00:00 2001 From: yanyue Date: Thu, 27 Apr 2023 17:40:20 +0800 Subject: [PATCH 04/14] docs(cn): update reusing-logic-with-custom-hooks --- src/content/learn/reuse.md | 2492 ----------------- .../learn/reusing-logic-with-custom-hooks.md | 72 +- 2 files changed, 36 insertions(+), 2528 deletions(-) delete mode 100644 src/content/learn/reuse.md diff --git a/src/content/learn/reuse.md b/src/content/learn/reuse.md deleted file mode 100644 index 679a9bac21..0000000000 --- a/src/content/learn/reuse.md +++ /dev/null @@ -1,2492 +0,0 @@ ---- -title: 'Reusing Logic with Custom Hooks' ---- - - - -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. - - - - - -- 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*/} - -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. - -This will keep your component [synchronized](/learn/synchronizing-with-effects) with the network status. You might start with something like this: - - - -```js -import { useState, useEffect } from 'react'; - -export default function StatusBar() { - const [isOnline, setIsOnline] = useState(true); - useEffect(() => { - function handleOnline() { - setIsOnline(true); - } - function handleOffline() { - setIsOnline(false); - } - window.addEventListener('online', handleOnline); - window.addEventListener('offline', handleOffline); - return () => { - window.removeEventListener('online', handleOnline); - window.removeEventListener('offline', handleOffline); - }; - }, []); - - return

{isOnline ? '✅ Online' : '❌ Disconnected'}

; -} -``` - -
- -Try turning your network on and off, and notice how this `StatusBar` updates in response to your actions. - -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`: - - - -```js -import { useState, useEffect } from 'react'; - -export default function SaveButton() { - const [isOnline, setIsOnline] = useState(true); - useEffect(() => { - function handleOnline() { - setIsOnline(true); - } - function handleOffline() { - setIsOnline(false); - } - window.addEventListener('online', handleOnline); - window.addEventListener('offline', handleOffline); - return () => { - window.removeEventListener('online', handleOnline); - window.removeEventListener('offline', handleOffline); - }; - }, []); - - function handleSaveClick() { - console.log('✅ Progress saved'); - } - - return ( - - ); -} -``` - - - -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*/} - -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: - -```js {2,7} -function StatusBar() { - const isOnline = useOnlineStatus(); - return

{isOnline ? '✅ Online' : '❌ Disconnected'}

; -} - -function SaveButton() { - const isOnline = useOnlineStatus(); - - function handleSaveClick() { - console.log('✅ Progress saved'); - } - - return ( - - ); -} -``` - -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: - -```js {2-16} -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; -} -``` - -At the end of the function, return `isOnline`. This lets your components read that value: - - - -```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 useOnlineStatus.js -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; -} -``` - -
- -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. - -### Hook names always start with `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! - -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. - -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! - - - -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! - - - - - -#### Should all functions called during rendering start with the use prefix? {/*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: - -```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 - 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: - -```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: - -```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: - // 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*/} - -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: - -```js {2,7} -function StatusBar() { - const isOnline = useOnlineStatus(); - // ... -} - -function SaveButton() { - const isOnline = useOnlineStatus(); - // ... -} -``` - -It works the same way as before you extracted the duplication: - -```js {2-5,10-13} -function StatusBar() { - const [isOnline, setIsOnline] = useState(true); - useEffect(() => { - // ... - }, []); - // ... -} - -function SaveButton() { - const [isOnline, setIsOnline] = useState(true); - useEffect(() => { - // ... - }, []); - // ... -} -``` - -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: - - - -```js -import { useState } from 'react'; - -export default function Form() { - const [firstName, setFirstName] = useState('Mary'); - const [lastName, setLastName] = useState('Poppins'); - - function handleFirstNameChange(e) { - setFirstName(e.target.value); - } - - function handleLastNameChange(e) { - setLastName(e.target.value); - } - - return ( - <> - - -

Good morning, {firstName} {lastName}.

- - ); -} -``` - -```css -label { display: block; } -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. - -You can extract the repetitive logic into this `useFormInput` custom Hook: - - - -```js -import { useFormInput } from './useFormInput.js'; - -export default function Form() { - const firstNameProps = useFormInput('Mary'); - const lastNameProps = useFormInput('Poppins'); - - return ( - <> - - -

Good morning, {firstNameProps.value} {lastNameProps.value}.

- - ); -} -``` - -```js useFormInput.js active -import { useState } from 'react'; - -export function useFormInput(initialValue) { - const [value, setValue] = useState(initialValue); - - function handleChange(e) { - setValue(e.target.value); - } - - const inputProps = { - value: value, - onChange: handleChange - }; - - return inputProps; -} -``` - -```css -label { display: block; } -input { margin-left: 10px; } -``` - -
- -Notice that it only declares *one* state variable called `value`. - -However, the `Form` component calls `useFormInput` *two times:* - -```js -function Form() { - const firstNameProps = useFormInput('Mary'); - const lastNameProps = useFormInput('Poppins'); - // ... -``` - -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. - -## 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! - -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: - - - -```js App.js -import { useState } from 'react'; -import ChatRoom from './ChatRoom.js'; - -export default function App() { - const [roomId, setRoomId] = useState('general'); - return ( - <> - -
- - - ); -} -``` - -```js ChatRoom.js active -import { useState, useEffect } from 'react'; -import { createConnection } from './chat.js'; -import { showNotification } from './notifications.js'; - -export default function ChatRoom({ roomId }) { - const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - - useEffect(() => { - const options = { - serverUrl: serverUrl, - roomId: roomId - }; - const connection = createConnection(options); - connection.on('message', (msg) => { - showNotification('New message: ' + msg); - }); - connection.connect(); - return () => connection.disconnect(); - }, [roomId, serverUrl]); - - return ( - <> - -

Welcome to the {roomId} room!

- - ); -} -``` - -```js 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 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": "latest", - "react-dom": "latest", - "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; } -``` - -
- -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. - -Now move the Effect's code into a custom Hook: - -```js {2-13} -export function useChatRoom({ serverUrl, roomId }) { - useEffect(() => { - const options = { - serverUrl: serverUrl, - roomId: roomId - }; - const connection = createConnection(options); - connection.connect(); - connection.on('message', (msg) => { - showNotification('New message: ' + msg); - }); - return () => connection.disconnect(); - }, [roomId, serverUrl]); -} -``` - -This lets your `ChatRoom` component call your custom Hook without worrying about how it works inside: - -```js {4-7} -export default function ChatRoom({ roomId }) { - const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - - useChatRoom({ - roomId: roomId, - serverUrl: serverUrl - }); - - return ( - <> - -

Welcome to the {roomId} room!

- - ); -} -``` - -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: - - - -```js App.js -import { useState } from 'react'; -import ChatRoom from './ChatRoom.js'; - -export default function App() { - const [roomId, setRoomId] = useState('general'); - return ( - <> - -
- - - ); -} -``` - -```js ChatRoom.js active -import { useState } from 'react'; -import { useChatRoom } from './useChatRoom.js'; - -export default function ChatRoom({ roomId }) { - const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - - useChatRoom({ - roomId: roomId, - serverUrl: serverUrl - }); - - return ( - <> - -

Welcome to the {roomId} room!

- - ); -} -``` - -```js useChatRoom.js -import { useEffect } from 'react'; -import { createConnection } from './chat.js'; -import { showNotification } from './notifications.js'; - -export function useChatRoom({ serverUrl, roomId }) { - useEffect(() => { - const options = { - serverUrl: serverUrl, - roomId: roomId - }; - const connection = createConnection(options); - connection.connect(); - connection.on('message', (msg) => { - showNotification('New message: ' + msg); - }); - return () => connection.disconnect(); - }, [roomId, serverUrl]); -} -``` - -```js 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 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": "latest", - "react-dom": "latest", - "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're taking the return value of one Hook: - -```js {2} -export default function ChatRoom({ roomId }) { - const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - - useChatRoom({ - roomId: roomId, - serverUrl: serverUrl - }); - // ... -``` - -and pass it as an input to another Hook: - -```js {6} -export default function ChatRoom({ roomId }) { - const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - - useChatRoom({ - roomId: roomId, - serverUrl: serverUrl - }); - // ... -``` - -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`.) - -### 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. - - - -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: - -```js {9-11} -export function useChatRoom({ serverUrl, roomId }) { - useEffect(() => { - const options = { - serverUrl: serverUrl, - roomId: roomId - }; - const connection = createConnection(options); - connection.connect(); - connection.on('message', (msg) => { - showNotification('New message: ' + msg); - }); - return () => connection.disconnect(); - }, [roomId, serverUrl]); -} -``` - -Let's say you want to move this logic back to your component: - -```js {7-9} -export default function ChatRoom({ roomId }) { - const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - - useChatRoom({ - roomId: roomId, - serverUrl: serverUrl, - onReceiveMessage(msg) { - showNotification('New message: ' + msg); - } - }); - // ... -``` - -To make this work, change your custom Hook to take `onReceiveMessage` as one of its named options: - -```js {1,10,13} -export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { - useEffect(() => { - const options = { - serverUrl: serverUrl, - roomId: roomId - }; - const connection = createConnection(options); - connection.connect(); - connection.on('message', (msg) => { - onReceiveMessage(msg); - }); - return () => connection.disconnect(); - }, [roomId, serverUrl, onReceiveMessage]); // ✅ All dependencies declared -} -``` - -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) - -```js {1,4,5,15,18} -import { useEffect, useEffectEvent } from 'react'; -// ... - -export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { - const onMessage = useEffectEvent(onReceiveMessage); - - useEffect(() => { - const options = { - serverUrl: serverUrl, - roomId: roomId - }; - const connection = createConnection(options); - connection.connect(); - connection.on('message', (msg) => { - onMessage(msg); - }); - return () => connection.disconnect(); - }, [roomId, serverUrl]); // ✅ All dependencies declared -} -``` - -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: - - - -```js App.js -import { useState } from 'react'; -import ChatRoom from './ChatRoom.js'; - -export default function App() { - const [roomId, setRoomId] = useState('general'); - return ( - <> - -
- - - ); -} -``` - -```js ChatRoom.js active -import { useState } from 'react'; -import { useChatRoom } from './useChatRoom.js'; -import { showNotification } from './notifications.js'; - -export default function ChatRoom({ roomId }) { - const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - - useChatRoom({ - roomId: roomId, - serverUrl: serverUrl, - onReceiveMessage(msg) { - showNotification('New message: ' + msg); - } - }); - - return ( - <> - -

Welcome to the {roomId} room!

- - ); -} -``` - -```js 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 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 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 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. - -Luckily, React 18 includes a dedicated API called [`useSyncExternalStore`](/reference/react/useSyncExternalStore) which takes care of all of these problems for you. Here is how 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 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 ( -

- Welcome -

- ); -} - -export default function App() { - const [show, setShow] = useState(false); - return ( - <> - -
- {show && } - - ); -} -``` - -```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%); -} -``` - -
- -To make the component more readable, you might extract the logic into a `useFadeIn` custom 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 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 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 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 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 styles.css -label, button { display: block; margin-bottom: 20px; } -html, body { min-height: 300px; } -``` - -```css 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 `Counter.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 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 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 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 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 { useState } from 'react'; -import { useCounter } from './useCounter.js'; - -export default function Counter() { - const count = useCounter(1000); - return

Seconds passed: {count}

; -} -``` - -```js 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 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 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 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 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 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 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 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 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 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. - - - - diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index 11950e694a..94f2cff715 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -289,9 +289,9 @@ function useAuth() { -### Custom Hooks let you share stateful logic, not state itself {/*custom-hooks-let-you-share-stateful-logic-not-state-itself*/} +### 自定义Hook共享的是状态逻辑,而不是state本身 {/*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`state变量的想法是错误的。看这段代码: ```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). +这是两个完全独立的state变量和Effect! 他们只是碰巧同时有同样的值,因为你将两个组件与相同的外部值同步(无论网络是否开启)。 -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. 都有state (`firstName` 和 `lastName`)。 +1. 都有change handler (`handleFirstNameChange` 和 `handleLastNameChange`)。 +1. 都有为输入框指定`value` 和 `onChange`属性的JSX片段。 -You can extract the repetitive logic into this `useFormInput` custom Hook: +你可以把重复的逻辑提取到自定义Hook`useFormInput`: @@ -428,9 +428,9 @@ input { margin-left: 10px; } -Notice that it only declares *one* state variable called `value`. +注意它只声明了**一个**叫做 `value` 的state变量。 -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! +这就是为什么它工作的时候像声明了两个独立的state变量! -**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. +**自定义Hook只是共享状态逻辑而不是state本身。每个Hook的调用都完全独立于对同一个Hook的其他调用。** 这就是为什么上面两个sandbox完全相同的原因。如果你愿意,可以滚动回去并比较他们。提取自定义Hook之前和之后的行为是一致的。 -When you need to share the state itself between multiple components, [lift it up and pass it down](/learn/sharing-state-between-components) instead. +而当你需要在多个组件之间共享state本身时,需要[将变量提升并传递下去](/learn/sharing-state-between-components)。 -## Passing reactive values between Hooks {/*passing-reactive-values-between-hooks*/} +## 在Hook之间传递响应值 {/*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! +组件每次重新渲染,自定义Hook中的代码也会重新运行。这就是为什么组件和自定义Hook都[需要纯粹](/learn/keeping-components-pure)的原因。我们应该把自定义Hook的代码作为组件主体的一部分。 -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: +自定义组件总会接收到最新的props和state,因为它会和你的组件一起重新渲染。想知道这意味着什么,看一下这个聊天室的例子。变更Server URL或者聊天室ID: @@ -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`时,Effect会对[你的修改做出“反应”](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values)和重新同步。你可以通过修改Effect依赖引起的聊天室重连的每次console信息来区分。 -Now move the Effect's code into a custom Hook: +现在把Effect代码移动到自定义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`组件调用自定义Hook,不需要担心内部是如何工作: ```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: +注意逻辑**仍然响应** prop和state修改。尝试编辑server URL或者选中的房间: @@ -724,7 +724,7 @@ export function useChatRoom({ serverUrl, roomId }) { ```js 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); } @@ -807,7 +807,7 @@ button { margin-left: 10px; } -Notice how you're taking the return value of one Hook: +注意你是如何获取到Hook的返回值: ```js {2} export default function ChatRoom({ roomId }) { @@ -820,7 +820,7 @@ export default function ChatRoom({ roomId }) { // ... ``` -and pass it as an input to another Hook: +并且把它作为输入传递给另外一个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`到你的Hook中。这就是为什么每当他们的值在重新渲染后不同的时候你的Effect会重连聊天室。(如果你曾经使用过音视频处理软件,像这样的链式Hook也许会让你想起链式可视化或音频effect。就好像`useState` 的输出作为 `useChatRoom`的输入)。 -### Passing event handlers to custom Hooks {/*passing-event-handlers-to-custom-hooks*/} +### 把事件处理器传到自定义Hook中 {/*passing-event-handlers-to-custom-hooks*/} -This section describes an **experimental API that has not yet been released** in a stable version of React. +这个章节描述React稳定版中**还没有发布的实验性API**。 -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`组件时,你也许想要让组件自定义它的行为。例如,现在Hook中收到信息时做什么的逻辑是硬编码: ```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: +为了完成这个工作,需要修改自定义Hook,把`onReceiveMessage`作为命名选项之一: ```js {1,10,13} export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { @@ -893,13 +893,13 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { onReceiveMessage(msg); }); return () => connection.disconnect(); - }, [roomId, serverUrl, onReceiveMessage]); // ✅ All dependencies declared + }, [roomId, serverUrl, onReceiveMessage]); // ✅ 声明了所有的依赖 } ``` -This will work, but there's one more improvement you can do when your custom Hook accepts event handlers. +这将会生效,但是当自定义Hook接受事件处理器的时候,你还可以做另一个改进。 -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'; @@ -919,7 +919,7 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { onMessage(msg); }); return () => connection.disconnect(); - }, [roomId, serverUrl]); // ✅ All dependencies declared + }, [roomId, serverUrl]); // ✅ 声明所有依赖 } ``` From 293cb17d864520a110e208d20dc83524cb16223b Mon Sep 17 00:00:00 2001 From: yanyue Date: Thu, 4 May 2023 16:56:37 +0800 Subject: [PATCH 05/14] docs(cn): update reusing-logic-with-custom-hooks --- .../learn/reusing-logic-with-custom-hooks.md | 198 +++++++++--------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index 94f2cff715..ab08c332bc 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -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` 组件重新渲染时都重新连接。这是一个传递事件处理器给自定义Hook的例子,你可以尝试一下: @@ -1008,7 +1008,7 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { ```js 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); } @@ -1091,20 +1091,20 @@ 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. +注意你不再需要为了使用它而去了解`useChatRoom`是**如何**工作的。你可以把它添加到其他任意组件,传递其他任意选项,而它会以同样的方式工作。这就是自定义Hook的强大之处。 -## When to use custom Hooks {/*when-to-use-custom-hooks*/} +## 什么时候使用自定义Hook {/*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. +你不需要把每段重复的代码提取为自定义Hook。一些重复是可以的。例如,像早前用来包裹单个`useState`调用的`useFormInput`Hook可能就是没有必要的。 -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. +但是每当你写Effect的时候,请考虑一下把它包裹在自定义Hook会不会更清晰。[你不应该经常使用Effect,](/learn/you-might-not-need-an-effect),所以如果你正在写Effect就意味着你需要"走出React"来和一些外部系统同步,或者需要做一些React中没有内置API的事。把重复代码包装进自定义Hook可以让你准确表达你的意图和数据在里面是如何流动的。 -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: +例如,假设 `ShippingForm` 组件展示两个下拉框:一个展示城市列表,另一个展示选中的城市的区域列表。你可能会像这样开始写代码: ```js {3-16,20-35} function ShippingForm({ country }) { const [cities, setCities] = useState(null); - // This Effect fetches cities for a country + // 这个Effect拉取一个国家的城市数据 useEffect(() => { let ignore = false; fetch(`/api/cities?country=${country}`) @@ -1121,7 +1121,7 @@ function ShippingForm({ country }) { const [city, setCity] = useState(null); const [areas, setAreas] = useState(null); - // This Effect fetches areas for the selected city + // 这个Effect拉取选中城市的区域列表 useEffect(() => { if (city) { let ignore = false; @@ -1141,7 +1141,7 @@ function ShippingForm({ country }) { // ... ``` -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: +尽管这部分代码是重复的,但是[把这些Effect各自分开是正确的](/learn/removing-effect-dependencies#is-your-effect-doing-several-unrelated-things)。他们同步两个不同的事情,所以你不应该把他们合并到同一个Effect。取而代之的是,你可以提取他们的通用逻辑到你自己的`useData` Hook中来简化上面的`ShippingForm` 组件: ```js {2-18} function useData(url) { @@ -1165,7 +1165,7 @@ function useData(url) { } ``` -Now you can replace both Effects in the `ShippingForm` components with calls to `useData`: +现在你可以调用`useData`代替`ShippingForm`组件中的Effect: ```js {2,4} function ShippingForm({ country }) { @@ -1175,39 +1175,39 @@ function ShippingForm({ country }) { // ... ``` -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. +提取自定义Hook可以让数据流清晰。你可以输入`url`,输出 `data`。通过把你的Effect“隐藏”在`useData`中,也可以防止一些正在处理`ShippingForm`组件的人向里面添加[不必要的依赖](/learn/removing-effect-dependencies)。随着时间的推移,你app中的大部分Effect都会存在于自定义Hook中。 -#### Keep your custom Hooks focused on concrete high-level use cases {/*keep-your-custom-hooks-focused-on-concrete-high-level-use-cases*/} +#### 让你的自定义Hook专注于具体的高级用例 {/*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. +从选择你的自定义Hook名称开始。如果你难以选择一个清晰的名称,这意味着你的Effect和组件逻辑的剩余部分耦合度太高,还没有准备好被提取出来。 -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: +理想情况下,你的自定义Hook名称应该清晰到即使一个不经常写代码的人也能很好地猜中你的自定义Hook的功能,输入和返回: * ✅ `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: +当你和外部系统同步的时候,你的自定义Hook名称可能会更加专业,并使用该系统特定的术语。只要这个名称对于熟悉这个系统的人来说清晰,那就是好的: * ✅ `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: +**保持自定义Hook专注于具体的高级用例**。避免创建和使用作为`useEffect`API本身的替代品和wrapper的自定义“生命周期”Hook: * 🔴 `useMount(fn)` * 🔴 `useEffectOnce(fn)` * 🔴 `useUpdateEffect(fn)` -For example, this `useMount` Hook tries to ensure some code only runs "on mount": +例如,这个 `useMount` Hook试图保证一些代码只在“加载”的时候运行: ```js {4-5,14-15} function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - // 🔴 Avoid: using custom "lifecycle" Hooks + // 🔴 避免: 使用自定义 "lifecycle" Hook useMount(() => { const connection = createConnection({ roomId, serverUrl }); connection.connect(); @@ -1217,23 +1217,23 @@ function ChatRoom({ roomId }) { // ... } -// 🔴 Avoid: creating custom "lifecycle" Hooks +// 🔴 避免: 创建自定义 "lifecycle" Hook function useMount(fn) { useEffect(() => { fn(); - }, []); // 🔴 React Hook useEffect has a missing dependency: 'fn' + }, []); // 🔴 React Hook useEffect 缺少依赖项: '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. +**像`useMount`这样的自定义“声明周期” Hook不能很好的适应React模式**。例如,示例代码有一个错误(它没有“响应” `roomId`或`serverUrl`的变化),但是代码检查工具并不会向你发出对应的告警,因为代码检查工具只能检测直接的`useEffect`调用。它并不了解你的Hook。 -If you're writing an Effect, start by using the React API directly: +如果你正在编写Effect,请从直接使用React API开始: ```js function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - // ✅ Good: two raw Effects separated by purpose + // ✅ Good: 通过用途分割的两个原始Effect useEffect(() => { const connection = createConnection({ serverUrl, roomId }); @@ -1249,28 +1249,28 @@ function ChatRoom({ roomId }) { } ``` -Then, you can (but don't have to) extract custom Hooks for different high-level use cases: +然后你可以(但不是必须的)为不同的高级用例提取自定义Hook: ```js function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - // ✅ Great: custom Hooks named after their purpose + // ✅ Great: 以用途命名的自定义Hook 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. +**一个好的自定义Hook通过限制功能使代码调用更具声明性**。例如,`useChatRoom(options)`只能连接聊天室,而`useImpressionLog(eventName, extraData)`只能向分析系统发送impression日志。如果你的自定义Hook API 没有约束用例且非常抽象,那么在长期的运行中,比起它解决的问题,可能会引入更多问题。 -### Custom Hooks help you migrate to better patterns {/*custom-hooks-help-you-migrate-to-better-patterns*/} +### 自定义Hook帮助你迁移到更好的模式 {/*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. +Effect是一个["应急出口"](/learn/escape-hatches):当你需要“走出React”且对于你的用例没有更好的内置解决方案的时候你可以使用他们。随着时间的推移,React团队的目标是通过给更多特定问题提供特定解决方案来最小化应用中的Effect数量。把你的Effect包裹进自定义Hook会使得这些解决方案可用的时候升级代码更加容易。 -Let's return to this example: +让我们回到这个例子: @@ -1331,9 +1331,9 @@ export function useOnlineStatus() { -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. +在上面的例子中,`useOnlineStatus`借助[`useState`](/reference/react/useState)和[`useEffect`](/reference/react/useEffect) 实现。但这不是最好的解决方案。它有许多没有考虑到的边界用例。例如,假设当组件加载的时候,`isOnline`已经是 `true`,但是如果网络已经离线的话这就是错误的。你可以使用浏览器的[`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine)API来检查,但是直接在生成初始HTML的服务器上使用它是不生效的。简而言之,这段代码可以改进。 -Luckily, React 18 includes a dedicated API called [`useSyncExternalStore`](/reference/react/useSyncExternalStore) which takes care of all of these problems for you. Here is how your `useOnlineStatus` Hook, rewritten to take advantage of this new API: +幸运的是,React 18包含了一个叫做[`useSyncExternalStore`]的专用API,它可以解决所有这些问题。这里是如何利用这个新API来重写你的`useOnlineStatus` Hook: @@ -1384,8 +1384,8 @@ function subscribe(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 + () => navigator.onLine, // 如何在客户端获取值 + () => true // 如何在服务端获取值 ); } @@ -1393,7 +1393,7 @@ export function useOnlineStatus() { -Notice how **you didn't need to change any of the components** to make this migration: +注意**不需要修改任何组件**如何来完成这次迁移: ```js {2,7} function StatusBar() { @@ -1407,22 +1407,22 @@ function SaveButton() { } ``` -This is another reason for why wrapping Effects in custom Hooks is often beneficial: +这是为什么把Effect包裹进自定义Hook是有益的另一个原因: -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. +1. 你让进出Effect的数据流非常清晰。 +2. 你让组件专注于目标,而不是Effect的实现。 +3. 当React增加新特性时,你可以在不修改你的任何组件的情况下移除这些Effect。 -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. +和[设计系统](https://uxdesign.cc/everything-you-need-to-know-about-design-systems-54b109851969)类似,你可能会发现从应用的组件中提取通用逻辑到自定义Hook是非常有帮助的。这会让你的组件代码专注于目标,避免经常写原始Effect。许多很棒的自定义Hook是由React社区维护的。 -#### Will React provide any built-in solution for data fetching? {/*will-react-provide-any-built-in-solution-for-data-fetching*/} +#### React会为远程数据获取提供内置的解决方案么? {/*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! +import { use } from 'react'; // 还不可用! function ShippingForm({ country }) { const cities = use(fetch(`/api/cities?country=${country}`)); @@ -1431,13 +1431,13 @@ function ShippingForm({ country }) { // ... ``` -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. +比起在每个组件手动写原始Effect,如果你在应用中使用像上面的`useData`这样的自定义Hook,迁移到最终推荐方式所需要的更改更少。但是旧的方式仍然可以有效工作,所以如果你喜欢写原始Effect,你可以继续这样做。 -### There is more than one way to do it {/*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: +假设你想要使用浏览器的[`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)API实现一个**从头开始的**渐入动画。你可能会从一个建立动画循环的Effect开始。在动画的每一帧中,你可以修改[ref持有的](/learn/manipulating-the-dom-with-refs)DOM节点的opacity属性直到它为`1`。你的代码可能这样开始: @@ -1459,7 +1459,7 @@ function Welcome() { const progress = Math.min(timePassed / duration, 1); onProgress(progress); if (progress < 1) { - // We still have more frames to paint + // 我们还有更多的帧需要绘制 frameId = requestAnimationFrame(onFrame); } } @@ -1520,7 +1520,7 @@ html, body { min-height: 300px; } -To make the component more readable, you might extract the logic into a `useFadeIn` custom Hook: +为了让组件更具有可读性,你可能要将逻辑提取到自定义Hook`useFadeIn`: @@ -1569,7 +1569,7 @@ export function useFadeIn(ref, duration) { const progress = Math.min(timePassed / duration, 1); onProgress(progress); if (progress < 1) { - // We still have more frames to paint + // 我们还有更多的帧需要绘制 frameId = requestAnimationFrame(onFrame); } } @@ -1611,7 +1611,7 @@ html, body { min-height: 300px; } -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: +你可以让`useFadeIn` 和原来保持一致,但是也可以更进一步重构。例如,你可以把创建动画循环的逻辑从`useFadeIn`提取到自定义Hook`useAnimationLoop`: @@ -1715,7 +1715,7 @@ html, body { min-height: 300px; } -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) +但是你**没有必要**这样做。和常规函数一样,最终是由你决定在哪里绘制代码不同部分之间的边界。你也可以采取不一样的方法。把大部分必要的逻辑移入一个[JavaScript类](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes),而不是把逻辑保留在Effect中: @@ -1782,7 +1782,7 @@ export class FadeInAnimation { if (progress === 1) { this.stop(); } else { - // We still have more frames to paint + // 我们还有更多的帧要绘制 this.frameId = requestAnimationFrame(() => this.onFrame()); } } @@ -1813,9 +1813,9 @@ html, body { min-height: 300px; } -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. +Effect可以将React和外部系统连接起来。Effect之间需要的协调越多(例如,链接多个动画),像上面的sandbox一样**完整地**从Effect和Hook中提取逻辑就越有意义。然后你提取的代码**变成**“外部系统”。这会让你的 Effect保持简单化,因为他们只需要向已经移动到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) +上面这个例子假设需要使用JavaScript写fade-in逻辑。但是使用纯[CSS 动画](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations)实现这个特定的fade-in会更加简单和高效: @@ -1870,27 +1870,27 @@ html, body { min-height: 300px; } -Sometimes, you don't even need a Hook! +某些时候你甚至不需要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. +- 自定义Hook让你可以在组件间共享逻辑。 +- 自定义Hook命名必须以后面跟着一个大写字母的`use`开头。 +- 自定义Hook共享的只是有状态的逻辑,而不是state 本身。 +- 你可以将响应值从一个Hook传到另一个,并且他们保持最新。 +- 每次你的组件重新渲染时,所有的Hook会重新运行。 +- 自定义Hook的代码应该和组件代码一样保持纯粹。 +- 把自定义Hook收到的事件处理器封装到Effect Event。 +- 不要创建像`useMount`这样的自定义Hook。保持目标具体化。 +- 如何以及在哪里选择代码边界取决于你自己。 -#### Extract a `useCounter` Hook {/*extract-a-usecounter-hook*/} +#### 提取一个 `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: +这个组件使用了一个state变量和一个Effect来展示每秒递增的一个数字。把逻辑提取到一个`useCounter`的自定义Hook中。你的目标是让`Counter` 组件的实现看上去和这个一样: ```js export default function Counter() { @@ -1899,7 +1899,7 @@ export default function Counter() { } ``` -You'll need to write your custom Hook in `useCounter.js` and import it into the `Counter.js` file. +你需要在 `useCounter.js`中编写你的自定义Hook,并且把它引入到`Counter.js` 文件。 @@ -1919,14 +1919,14 @@ export default function Counter() { ``` ```js useCounter.js -// Write your custom Hook in this file! +// 在这个文件中编写你的自定义Hook! ``` -Your code should look like this: +你的代码应该像这样: @@ -1956,13 +1956,13 @@ export function useCounter() { -Notice that `App.js` doesn't need to import `useState` or `useEffect` anymore. +注意`App.js` 不再需要引入`useState` 或者 `useEffect`。 -#### Make the counter delay configurable {/*make-the-counter-delay-configurable*/} +#### 让计时器的delay变为可配置 {/*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. +这个例子中,有一个由滑动条控制的state变量`delay`,但是它的值没有被用到。请将`delay`值传给你的自定义Hook `useCounter`,修改`useCounter` Hook,用传过去的`delay`代替硬编码`1000`毫秒。 @@ -2012,7 +2012,7 @@ export function useCounter() { -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. +使用`useCounter(delay)`将`delay`传入你的Hook。然后在Hook内部使用`delay` 而不是硬编码`1000` 。你需要向你的Effect依赖项中加入`delay` 。这保证了`delay`的变化会重置间隔时间。 @@ -2062,9 +2062,9 @@ export function useCounter(delay) { -#### Extract `useInterval` out of `useCounter` {/*extract-useinterval-out-of-usecounter*/} +#### 从 `useCounter`中提取 `useInterval` {/*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: +现在`useCounter` Hook做两件事。设置一个时间间隔,并且在每个时间间隔的tick内递增一次state变量。将设置时间间隔的逻辑拆分到到一个`useInterval`的独立Hook中。它应该输入两个参数: `onTick` 回调函数和`delay`。修改后`useCounter` 的实现应该如下所示: ```js export function useCounter(delay) { @@ -2076,7 +2076,7 @@ export function useCounter(delay) { } ``` -Write `useInterval` in the `useInterval.js` file and import it into the `useCounter.js` file. +在 `useInterval.js` 文件中编写`useInterval` 并在 `useCounter.js`文件中导入。 @@ -2106,14 +2106,14 @@ export function useCounter(delay) { ``` ```js useInterval.js -// Write your Hook here! +// 在这里编写你自己的Hook! ``` -The logic inside `useInterval` should set up and clear the interval. It doesn't need to do anything else. +`useInterval`内部的逻辑应该是设置和清除计时器。不需要做除此之外的任何事。 @@ -2152,17 +2152,17 @@ export function useInterval(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*/} +#### 修复计时器重置 {/*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. +`App`组件调用`useCounter`,这个Hook调用`useInterval`来每秒更新一次计数器。但是`App`组件**也**调用`useInterval`来每两秒随机更新一次页面背景色 -For some reason, the callback that updates the page background never runs. Add some logs inside `useInterval`: +因为一些原因,更新页面背景色的回调函数从未执行。在 `useInterval`内部添加一些log。 ```js {2,5} useEffect(() => { @@ -2175,13 +2175,13 @@ For some reason, the callback that updates the page background never runs. Add s }, [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? +这些log符合你的预期吗?如果你的一些Effect似乎不必要的重新同步了,你能猜中哪一个依赖导致这个情况发生吗?有其他方式从你的Effect中[移除依赖](/learn/removing-effect-dependencies)吗? -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? +看上去你的`useInterval` Hook接受事件监听器作为参数。你能想到一些包裹事件监听器的方法,这样它不需要成为你的Effect的依赖项吗? @@ -2250,11 +2250,11 @@ export function useInterval(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) +和[早前这个页面](/learn/reusing-logic-with-custom-hooks#passing-event-handlers-to-custom-hooks)做的一样,在`useInterval`内部,把tick回调函数包裹进一个Effect Event。 -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. +这将让你可以从Effect的依赖项中删掉`onTick`。每次组件重新渲染时,Effect将不会重新同步,所以页面背景颜色更新间隔不会在有机会触发之前每秒重置一次。 -With this change, both intervals work as expected and don't interfere with each other: +随着这个修改,两个interval都会像预期的一样工作并且不会互相干预: @@ -2321,21 +2321,21 @@ export function useInterval(callback, delay) { -#### Implement a staggering movement {/*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. +这个例子中,`usePointerPosition()`Hook追踪当前指针位置。尝试移动鼠标或你的手指到预览区域上方,看到有一个红点随着你移动。它的位置被保存在`pos1`变量中。 -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. +事实上,有5(!)个不同的红点正在被渲染。你看不见是因为他们都出现在了同一位置。这就是你需要修复的问题。你想要实现的是一个 “交错的”运动:每个圆点应该“跟随”它前一个点的路径。例如,如果你快速移动光标,第一个点应该立刻跟着它,第二个应该在小的延时后跟上第一个点,第三个点应该跟着第二个点等。 -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. +你需要实现自定义Hook `useDelayedValue`。它当前的实现是返回提供给它的`value`。而你想从`delay`毫秒之前返回`value`。你可能需要一些state和一个Effect来完成这个任务。 -After you implement `useDelayedValue`, you should see the dots move following one another. +实现`useDelayedValue`后,你应该看见这些点一个接一个运动。 -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`. +你需要在自定义Hook内部存储一个state变量`delayedValue`。当`value`变化时,你需要运行一个Effect。这个Effect应该在`delay`毫秒后更新`delayedValue`。你可能发现调用`setTimeout`很有帮助。 -Does this Effect need cleanup? Why or why not? +这个Effect 需要清除吗? 为什么? @@ -2345,7 +2345,7 @@ Does this Effect need cleanup? Why or why not? import { usePointerPosition } from './usePointerPosition.js'; function useDelayedValue(value, delay) { - // TODO: Implement this Hook + // TODO: 实现这个Hook return value; } @@ -2408,7 +2408,7 @@ 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`. +这里是一个生效的版本。你将`delayedValue`保存为一个state变量。当`value`更新的时候,你的Effect会安排一个timeout来更新`delayedValue`。这就是`delayedValue`总是滞后于真实的`value`的原因。 @@ -2485,7 +2485,7 @@ 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. +请注意这个Effect **不**需要清除。如果你在清理函数中调用了`clearTimeout`,那么每当`value`变化时,就会终止已经计划好的timeout。为了保持运动连续,你想要触发所有的timeout。 From e65b066f7d31d03c4bc78f408146bca618d94151 Mon Sep 17 00:00:00 2001 From: yanyue Date: Thu, 4 May 2023 18:02:32 +0800 Subject: [PATCH 06/14] docs(cn): correct all punctuation --- .../learn/reusing-logic-with-custom-hooks.md | 154 +++++++++--------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index ab08c332bc..5a59b072c7 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -4,7 +4,7 @@ title: 使用自定义Hook重用逻辑 -React带有一些内置的Hook,比如`useState`, `useContext`和`useEffect`。有时候你需要一个用途更加特定的Hook:比如远程获取数据,追踪用户是否在线,或者连接一个聊天室。在React中可能找不到这些Hook,但是你可以根据自己应用的需求取创建自己的Hook。 +React带有一些内置的Hook,比如`useState`, `useContext`和`useEffect`。有时候你需要一个用途更加特定的Hook:比如远程获取数据,追踪用户是否在线,或者连接一个聊天室。在React中可能找不到这些Hook,但是你可以根据自己应用的需求取创建自己的Hook。 @@ -17,7 +17,7 @@ React带有一些内置的Hook,比如`useState`, `useContext`和`useEffect`。 -## 自定义Hook:在组件间共享逻辑 {/*custom-hooks-sharing-logic-between-components*/} +## 自定义Hook:在组件间共享逻辑 {/*custom-hooks-sharing-logic-between-components*/} 假设你正在开发一款重度依赖网络的应用(和大多数应用一样)。当用户使用你的应用时,如果网络意外断开,你想要警告用户。你会如何处理这种情况呢?看上去你在组件中需要两个东西: @@ -56,7 +56,7 @@ export default function StatusBar() { 尝试开启和关闭网络,注意`StatusBar`应对你的行为是如何更新的。 -现在假设你想要在一个不同的组件里**也**使用这段相同的逻辑。你想实现一个Save按钮,当网络离线时,这个按钮会变成不可用,并且显示"Reconnecting..."而不是"Save"。 +现在假设你想要在一个不同的组件里**也**使用这段相同的逻辑。你想实现一个Save按钮,当网络离线时,这个按钮会变成不可用并且显示“Reconnecting...”而不是“Save”。 你可以通过复制和粘贴`isOnline` state和Effect到`SaveButton`开始: @@ -96,9 +96,9 @@ export default function SaveButton() { -验证一下, 如果关闭网络, 按钮会变更展示。 +验证一下,如果关闭网络,按钮会变更展示。 -这两个组件都工作正常,但是不幸的是他们之间的逻辑重复了。即使两个组件看上去有不同的**视觉界面,**你也想要复用他们之间的逻辑。 +这两个组件都工作正常,但是不幸的是他们之间的逻辑重复了。即使两个组件看上去有不同的**视觉界面**,你也想要重用他们之间的逻辑。 ### 从组件中提取出你的自定义Hook {/*extracting-your-own-custom-hook-from-a-component*/} @@ -125,7 +125,7 @@ function SaveButton() { } ``` -尽管目前没有这样的内置Hook,但是你可以自己写。声明一个`useOnlineStatus`函数,并且把早前组件里的所有重复代码移到里面: +尽管目前没有这样的内置Hook,但是你可以自己写。声明一个`useOnlineStatus`函数,并且把早前组件里的所有重复代码移到里面: ```js {2-16} function useOnlineStatus() { @@ -148,7 +148,7 @@ function useOnlineStatus() { } ``` -在函数结尾处, 返回 `isOnline`。这可以让组件读取到那个值: +在函数结尾处,返回 `isOnline`。这可以让组件读取到那个值: @@ -219,34 +219,34 @@ export function useOnlineStatus() { React 应用是由组件构建的。组件是由内置的或者自定义的Hook构建的。你可能经常使用别人创建的自定义Hook,但是偶尔也可能要自己写! -你必须遵循以下这些命名公约: +你必须遵循以下这些命名公约: -1. **React组件名称必须以大写字母开头,** 比如 `StatusBar` 和 `SaveButton`. React 组件还需要返回一些React知道如何展示的内容,比如一段JSX代码。 -2. **Hook的名称必须以 `use`开头,后面跟一个大写字母,** 像 [`useState`](/reference/react/useState) (内置) or `useOnlineStatus` (像文章早前的自定义Hook)。 Hook可能会返回任意值。 +1. **React组件名称必须以大写字母开头**, 比如 `StatusBar` 和 `SaveButton`。React 组件还需要返回一些React知道如何展示的内容,比如一段JSX代码。 +2. **Hook的名称必须以 `use`开头,后面跟一个大写字母**, 像 [`useState`](/reference/react/useState) (内置) 或者 `useOnlineStatus` (像文章早前的自定义Hook)。Hook可能会返回任意值。 -这个公约保证了你始终可以查看组件并且知道它的state,Effect以及其他的React特性可能“隐藏”在哪里。比如,如果你在组件内部看见`getColor()`的函数调用,你可以确定它内部不可能包含React state,因为它的名称没有以`use`开头。但是,像`useOnlineStatus()`这样的函数调用将极有可能包含对内部其他Hook的调用! +这个公约保证了你始终可以查看组件并且知道它的state,Effect以及其他的React特性可能“隐藏”在哪里。例如如果你在组件内部看见`getColor()`的函数调用,你可以确定它内部不可能包含React state,因为它的名称没有以`use`开头。但是像`useOnlineStatus()`这样的函数调用将极有可能包含对内部其他Hook的调用! -如果为[React配置了](/learn/editor-setup#linting)检查工具,它会强制执行这个命名公约。滑动到上面的sandbox,并将`useOnlineStatus`重命名为`getOnlineStatus`。注意检查工具将不会再允许你在内部调用`useState` 或者 `useEffect`。只有Hook和组件可以调用其他Hook! +如果为[React配置了](/learn/editor-setup#linting)检查工具,它会强制执行这个命名公约。滑动到上面的sandbox,并将`useOnlineStatus`重命名为`getOnlineStatus`。注意检查工具将不会再允许你在内部调用`useState` 或者 `useEffect`。只有Hook和组件可以调用其他Hook! -#### 渲染期间调用的所有函数都应该以前缀use开头么? {/*should-all-functions-called-during-rendering-start-with-the-use-prefix*/} +#### 渲染期间调用的所有函数都应该以前缀use开头么? {/*should-all-functions-called-during-rendering-start-with-the-use-prefix*/} 不是。没有**调用**Hook的函数不需要**成为**Hook。 -如果你的函数没有调用任何Hook,请避免使用`use` 前缀。 而是**不带**`use`前缀,把它作为常规函数去写。比如, 下面的`useSorted` 没有调用Hook, 所以叫它 `getSorted`: +如果你的函数没有调用任何Hook,请避免使用`use` 前缀。 而是**不带**`use`前缀,把它作为常规函数去写。例如下面的`useSorted` 没有调用Hook,所以叫它 `getSorted`: ```js -// 🔴 避免: 没有调用其他Hook的Hook +// 🔴 避免:没有调用其他Hook的Hook function useSorted(items) { return items.slice().sort(); } -// ✅ Good: 没有使用Hook的常规函数 +// ✅ Good:没有使用Hook的常规函数 function getSorted(items) { return items.slice().sort(); } @@ -268,19 +268,19 @@ function List({ items, shouldSort }) { 如果内部至少使用了一个Hook,你应该给这个函数`use`前缀(从而让它成为一个Hook): ```js -// ✅ Good: 一个使用了其他Hook的Hook +// ✅ Good:一个使用了其他Hook的Hook function useAuth() { return useContext(Auth); } ``` -从技术上讲,这不是React强制的。原则上,你可以写一个不调用其他Hook的Hook。这常常会令人迷惑且受到限制,所以最好是避免那种方式。但是在极少一些场景下,它可能是有帮助的。比如,也许你的函数现在没有使用任何Hook,但是计划在未来会添加一些Hook调用。那么使用`use`前缀给它命名就很有意义: +从技术上讲,这不是React强制的。原则上你可以写一个不调用其他Hook的Hook。这常常会令人迷惑且受到限制,所以最好是避免那种方式。但是在极少一些场景下,它可能是有帮助的。例如也许你的函数现在没有使用任何Hook,但是计划未来会添加一些Hook调用。那么使用`use`前缀给它命名就很有意义: ```js {3-4} -// ✅ Good: 之后即将可能使用一些其他Hook的一个Hook +// ✅ Good:之后即将可能使用一些其他Hook的一个Hook function useAuth() { - // TODO: 当认证功能实现以后,替换这一行: - // 返回 useContext(Auth); + // TODO: 当认证功能实现以后,替换这一行: + // 返回 useContext(Auth); return TEST_USER; } ``` @@ -291,7 +291,7 @@ function useAuth() { ### 自定义Hook共享的是状态逻辑,而不是state本身 {/*custom-hooks-let-you-share-stateful-logic-not-state-itself*/} -之前的例子里,当你开启或关闭网络时,两个组件一起更新了。但是,两个组件共享一个`isOnline`state变量的想法是错误的。看这段代码: +之前的例子里,当你开启或关闭网络时,两个组件一起更新了。但是两个组件共享一个`isOnline`state变量的想法是错误的。看这段代码: ```js {2,7} function StatusBar() { @@ -305,7 +305,7 @@ function SaveButton() { } ``` -它的工作方式和你提取重复部分之前一样: +它的工作方式和你提取重复部分之前一样: ```js {2-5,10-13} function StatusBar() { @@ -325,9 +325,9 @@ function SaveButton() { } ``` -这是两个完全独立的state变量和Effect! 他们只是碰巧同时有同样的值,因为你将两个组件与相同的外部值同步(无论网络是否开启)。 +这是两个完全独立的state变量和Effect!他们只是碰巧同时有同样的值,因为你将两个组件与相同的外部值同步(无论网络是否开启)。 -为了更好的说明这一点, 我们需要一个不同的例子。看下面的 `Form` 组件: +为了更好的说明这一点,我们需要一个不同的例子。看下面的 `Form` 组件: @@ -371,11 +371,11 @@ input { margin-left: 10px; } 每个表单域都有一些重复的逻辑: -1. 都有state (`firstName` 和 `lastName`)。 -1. 都有change handler (`handleFirstNameChange` 和 `handleLastNameChange`)。 +1. 都有state(`firstName` 和 `lastName`)。 +1. 都有change handler(`handleFirstNameChange` 和 `handleLastNameChange`)。 1. 都有为输入框指定`value` 和 `onChange`属性的JSX片段。 -你可以把重复的逻辑提取到自定义Hook`useFormInput`: +你可以把重复的逻辑提取到自定义Hook`useFormInput`: @@ -441,7 +441,7 @@ function Form() { 这就是为什么它工作的时候像声明了两个独立的state变量! -**自定义Hook只是共享状态逻辑而不是state本身。每个Hook的调用都完全独立于对同一个Hook的其他调用。** 这就是为什么上面两个sandbox完全相同的原因。如果你愿意,可以滚动回去并比较他们。提取自定义Hook之前和之后的行为是一致的。 +**自定义Hook只是共享状态逻辑而不是state本身。每个Hook的调用都完全独立于对同一个Hook的其他调用**。 这就是为什么上面两个sandbox完全相同的原因。如果你愿意,可以滚动回去并比较他们。提取自定义Hook之前和之后的行为是一致的。 而当你需要在多个组件之间共享state本身时,需要[将变量提升并传递下去](/learn/sharing-state-between-components)。 @@ -516,7 +516,7 @@ export default function ChatRoom({ roomId }) { ```js 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); } @@ -601,7 +601,7 @@ button { margin-left: 10px; } 当你修改`serverUrl`或者`roomId`时,Effect会对[你的修改做出“反应”](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values)和重新同步。你可以通过修改Effect依赖引起的聊天室重连的每次console信息来区分。 -现在把Effect代码移动到自定义Hook中: +现在把Effect代码移动到自定义Hook中: ```js {2-13} export function useChatRoom({ serverUrl, roomId }) { @@ -643,7 +643,7 @@ export default function ChatRoom({ roomId }) { } ``` -这看上去简洁多了! (但是它做的是同一件事情。) +这看上去简洁多了!(但是它做的是同一件事情。) 注意逻辑**仍然响应** prop和state修改。尝试编辑server URL或者选中的房间: @@ -724,7 +724,7 @@ export function useChatRoom({ serverUrl, roomId }) { ```js chat.js export function createConnection({ serverUrl, roomId }) { - // 连接到服务器的真实实现 + // 真正的实现会实际连接到服务器 if (typeof serverUrl !== 'string') { throw Error('Expected serverUrl to be a string. Received: ' + serverUrl); } @@ -843,7 +843,7 @@ export default function ChatRoom({ roomId }) { -当你在更多组件中使用`useChatRoom`组件时,你也许想要让组件自定义它的行为。例如,现在Hook中收到信息时做什么的逻辑是硬编码: +当你在更多组件中使用`useChatRoom`组件时,你也许想要让组件自定义它的行为。例如现在Hook中收到信息时做什么的逻辑是硬编码: ```js {9-11} export function useChatRoom({ serverUrl, roomId }) { @@ -878,7 +878,7 @@ export default function ChatRoom({ roomId }) { // ... ``` -为了完成这个工作,需要修改自定义Hook,把`onReceiveMessage`作为命名选项之一: +完成这个工作需要修改自定义Hook,把`onReceiveMessage`作为命名选项之一: ```js {1,10,13} export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { @@ -899,7 +899,7 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { 这将会生效,但是当自定义Hook接受事件处理器的时候,你还可以做另一个改进。 -增加一个`onReceiveMessage`依赖并不理想,每次只要组件重新渲染,聊天就会重新连接。[将这个事件处理器包装到Effect Event从而将它从依赖中移除:](/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'; @@ -1008,7 +1008,7 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { ```js chat.js export function createConnection({ serverUrl, roomId }) { - // 真实的实现会理所当然地连接到服务器 + // 真正的实现会实际连接到服务器 if (typeof serverUrl !== 'string') { throw Error('Expected serverUrl to be a string. Received: ' + serverUrl); } @@ -1095,11 +1095,11 @@ button { margin-left: 10px; } ## 什么时候使用自定义Hook {/*when-to-use-custom-hooks*/} -你不需要把每段重复的代码提取为自定义Hook。一些重复是可以的。例如,像早前用来包裹单个`useState`调用的`useFormInput`Hook可能就是没有必要的。 +你不需要把每段重复的代码提取为自定义Hook。一些重复是可以的。例如像早前用来包裹单个`useState`调用的`useFormInput`Hook可能就是没有必要的。 -但是每当你写Effect的时候,请考虑一下把它包裹在自定义Hook会不会更清晰。[你不应该经常使用Effect,](/learn/you-might-not-need-an-effect),所以如果你正在写Effect就意味着你需要"走出React"来和一些外部系统同步,或者需要做一些React中没有内置API的事。把重复代码包装进自定义Hook可以让你准确表达你的意图和数据在里面是如何流动的。 +但是每当你写Effect的时候,请考虑一下把它包裹在自定义Hook会不会更清晰。[你不应该经常使用Effect](/learn/you-might-not-need-an-effect),所以如果你正在写Effect就意味着你需要“走出React”来和一些外部系统同步,或者需要做一些React中没有内置API的事。把重复代码包装进自定义Hook可以让你准确表达你的意图和数据在里面是如何流动的。 -例如,假设 `ShippingForm` 组件展示两个下拉框:一个展示城市列表,另一个展示选中的城市的区域列表。你可能会像这样开始写代码: +例如假设 `ShippingForm` 组件展示两个下拉框:一个展示城市列表,另一个展示选中的城市的区域列表。你可能会像这样开始写代码: ```js {3-16,20-35} function ShippingForm({ country }) { @@ -1141,7 +1141,7 @@ function ShippingForm({ country }) { // ... ``` -尽管这部分代码是重复的,但是[把这些Effect各自分开是正确的](/learn/removing-effect-dependencies#is-your-effect-doing-several-unrelated-things)。他们同步两个不同的事情,所以你不应该把他们合并到同一个Effect。取而代之的是,你可以提取他们的通用逻辑到你自己的`useData` Hook中来简化上面的`ShippingForm` 组件: +尽管这部分代码是重复的,但是[把这些Effect各自分开是正确的](/learn/removing-effect-dependencies#is-your-effect-doing-several-unrelated-things)。他们同步两个不同的事情,所以你不应该把他们合并到同一个Effect。取而代之的是,你可以提取他们的通用逻辑到你自己的`useData` Hook中来简化上面的`ShippingForm` 组件: ```js {2-18} function useData(url) { @@ -1165,7 +1165,7 @@ function useData(url) { } ``` -现在你可以调用`useData`代替`ShippingForm`组件中的Effect: +现在你可以调用`useData`代替`ShippingForm`组件中的Effect: ```js {2,4} function ShippingForm({ country }) { @@ -1195,19 +1195,19 @@ function ShippingForm({ country }) { * ✅ `useSocket(url)` * ✅ `useIntersectionObserver(ref, options)` -**保持自定义Hook专注于具体的高级用例**。避免创建和使用作为`useEffect`API本身的替代品和wrapper的自定义“生命周期”Hook: +**保持自定义Hook专注于具体的高级用例**。避免创建和使用作为`useEffect`API本身的替代品和wrapper的自定义“生命周期”Hook: * 🔴 `useMount(fn)` * 🔴 `useEffectOnce(fn)` * 🔴 `useUpdateEffect(fn)` -例如,这个 `useMount` Hook试图保证一些代码只在“加载”的时候运行: +例如这个 `useMount` Hook试图保证一些代码只在“加载”的时候运行: ```js {4-5,14-15} function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - // 🔴 避免: 使用自定义 "lifecycle" Hook + // 🔴 避免:使用自定义 "lifecycle" Hook useMount(() => { const connection = createConnection({ roomId, serverUrl }); connection.connect(); @@ -1217,7 +1217,7 @@ function ChatRoom({ roomId }) { // ... } -// 🔴 避免: 创建自定义 "lifecycle" Hook +// 🔴 避免:创建自定义 "lifecycle" Hook function useMount(fn) { useEffect(() => { fn(); @@ -1225,9 +1225,9 @@ function useMount(fn) { } ``` -**像`useMount`这样的自定义“声明周期” Hook不能很好的适应React模式**。例如,示例代码有一个错误(它没有“响应” `roomId`或`serverUrl`的变化),但是代码检查工具并不会向你发出对应的告警,因为代码检查工具只能检测直接的`useEffect`调用。它并不了解你的Hook。 +**像`useMount`这样的自定义“生命周期” Hook不能很好的适应React模式**。例如示例代码有一个错误(它没有“响应” `roomId`或`serverUrl`的变化),但是代码检查工具并不会向你发出对应的告警,因为代码检查工具只能检测直接的`useEffect`调用。它并不了解你的Hook。 -如果你正在编写Effect,请从直接使用React API开始: +如果你正在编写Effect,请从直接使用React API开始: ```js function ChatRoom({ roomId }) { @@ -1249,7 +1249,7 @@ function ChatRoom({ roomId }) { } ``` -然后你可以(但不是必须的)为不同的高级用例提取自定义Hook: +然后你可以(但不是必须)为不同的高级用例提取自定义Hook: ```js function ChatRoom({ roomId }) { @@ -1262,15 +1262,15 @@ function ChatRoom({ roomId }) { } ``` -**一个好的自定义Hook通过限制功能使代码调用更具声明性**。例如,`useChatRoom(options)`只能连接聊天室,而`useImpressionLog(eventName, extraData)`只能向分析系统发送impression日志。如果你的自定义Hook API 没有约束用例且非常抽象,那么在长期的运行中,比起它解决的问题,可能会引入更多问题。 +**一个好的自定义Hook通过限制功能使代码调用更具声明性**。例如`useChatRoom(options)`只能连接聊天室,而`useImpressionLog(eventName, extraData)`只能向分析系统发送impression日志。如果你的自定义Hook API 没有约束用例且非常抽象,那么在长期的运行中,比起它解决的问题,可能会引入更多问题。 ### 自定义Hook帮助你迁移到更好的模式 {/*custom-hooks-help-you-migrate-to-better-patterns*/} -Effect是一个["应急出口"](/learn/escape-hatches):当你需要“走出React”且对于你的用例没有更好的内置解决方案的时候你可以使用他们。随着时间的推移,React团队的目标是通过给更多特定问题提供特定解决方案来最小化应用中的Effect数量。把你的Effect包裹进自定义Hook会使得这些解决方案可用的时候升级代码更加容易。 +Effect是一个[“应急出口”](/learn/escape-hatches):当你需要“走出React”且对于你的用例没有更好的内置解决方案的时候你可以使用他们。随着时间的推移,React团队的目标是通过给更多特定问题提供特定解决方案来最小化应用中的Effect数量。把你的Effect包裹进自定义Hook会使得这些解决方案可用的时候升级代码更加容易。 -让我们回到这个例子: +让我们回到这个例子: @@ -1331,9 +1331,9 @@ export function useOnlineStatus() { -在上面的例子中,`useOnlineStatus`借助[`useState`](/reference/react/useState)和[`useEffect`](/reference/react/useEffect) 实现。但这不是最好的解决方案。它有许多没有考虑到的边界用例。例如,假设当组件加载的时候,`isOnline`已经是 `true`,但是如果网络已经离线的话这就是错误的。你可以使用浏览器的[`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine)API来检查,但是直接在生成初始HTML的服务器上使用它是不生效的。简而言之,这段代码可以改进。 +在上面的例子中,`useOnlineStatus`借助[`useState`](/reference/react/useState)和[`useEffect`](/reference/react/useEffect) 实现。但这不是最好的解决方案。它有许多没有考虑到的边界用例。例如假设当组件加载的时候,`isOnline`已经是 `true`,但是如果网络已经离线的话这就是错误的。你可以使用浏览器的[`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine)API来检查,但是直接在生成初始HTML的服务器上使用它是不生效的。简而言之这段代码可以改进。 -幸运的是,React 18包含了一个叫做[`useSyncExternalStore`]的专用API,它可以解决所有这些问题。这里是如何利用这个新API来重写你的`useOnlineStatus` Hook: +幸运的是,React 18包含了一个叫做[`useSyncExternalStore`]的专用API,它可以解决所有这些问题。这里是如何利用这个新API来重写你的`useOnlineStatus` Hook: @@ -1407,7 +1407,7 @@ function SaveButton() { } ``` -这是为什么把Effect包裹进自定义Hook是有益的另一个原因: +这是为什么把Effect包裹进自定义Hook是有益的另一个原因: 1. 你让进出Effect的数据流非常清晰。 2. 你让组件专注于目标,而不是Effect的实现。 @@ -1419,10 +1419,10 @@ function SaveButton() { #### React会为远程数据获取提供内置的解决方案么? {/*will-react-provide-any-built-in-solution-for-data-fetching*/} -我们仍然在规划细节,但是期望在未来,可以像这样写远程数据获取: +我们仍然在规划细节,但是期望未来可以像这样写远程数据获取: ```js {1,4,6} -import { use } from 'react'; // 还不可用! +import { use } from 'react'; // 还不可用! function ShippingForm({ country }) { const cities = use(fetch(`/api/cities?country=${country}`)); @@ -1431,11 +1431,11 @@ function ShippingForm({ country }) { // ... ``` -比起在每个组件手动写原始Effect,如果你在应用中使用像上面的`useData`这样的自定义Hook,迁移到最终推荐方式所需要的更改更少。但是旧的方式仍然可以有效工作,所以如果你喜欢写原始Effect,你可以继续这样做。 +比起在每个组件手动写原始Effect,如果你在应用中使用像上面的`useData`这样的自定义Hook,迁移到最终推荐方式所需要的更改更少。但是旧的方式仍然可以有效工作,所以如果你喜欢写原始Effect,你可以继续这样做。 -### 有不止一个方法达到这个目的 {/*there-is-more-than-one-way-to-do-it*/} +### 不止一个方法达到这个目的 {/*there-is-more-than-one-way-to-do-it*/} 假设你想要使用浏览器的[`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)API实现一个**从头开始的**渐入动画。你可能会从一个建立动画循环的Effect开始。在动画的每一帧中,你可以修改[ref持有的](/learn/manipulating-the-dom-with-refs)DOM节点的opacity属性直到它为`1`。你的代码可能这样开始: @@ -1520,7 +1520,7 @@ html, body { min-height: 300px; } -为了让组件更具有可读性,你可能要将逻辑提取到自定义Hook`useFadeIn`: +为了让组件更具有可读性,你可能要将逻辑提取到自定义Hook`useFadeIn`: @@ -1611,7 +1611,7 @@ html, body { min-height: 300px; } -你可以让`useFadeIn` 和原来保持一致,但是也可以更进一步重构。例如,你可以把创建动画循环的逻辑从`useFadeIn`提取到自定义Hook`useAnimationLoop`: +你可以让`useFadeIn` 和原来保持一致,但是也可以更进一步重构。例如你可以把创建动画循环的逻辑从`useFadeIn`提取到自定义Hook`useAnimationLoop`: @@ -1813,7 +1813,7 @@ html, body { min-height: 300px; } -Effect可以将React和外部系统连接起来。Effect之间需要的协调越多(例如,链接多个动画),像上面的sandbox一样**完整地**从Effect和Hook中提取逻辑就越有意义。然后你提取的代码**变成**“外部系统”。这会让你的 Effect保持简单化,因为他们只需要向已经移动到React外部的系统发送消息。 +Effect可以将React和外部系统连接起来。Effect之间需要的协调越多(例如链接多个动画),像上面的sandbox一样**完整地**从Effect和Hook中提取逻辑就越有意义。然后你提取的代码**变成**“外部系统”。这会让你的 Effect保持简单化,因为他们只需要向已经移动到React外部的系统发送消息。 上面这个例子假设需要使用JavaScript写fade-in逻辑。但是使用纯[CSS 动画](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations)实现这个特定的fade-in会更加简单和高效: @@ -1870,7 +1870,7 @@ html, body { min-height: 300px; } -某些时候你甚至不需要Hook! +某些时候你甚至不需要Hook! @@ -1926,7 +1926,7 @@ export default function Counter() { -你的代码应该像这样: +你的代码应该像这样: @@ -1962,7 +1962,7 @@ export function useCounter() { #### 让计时器的delay变为可配置 {/*make-the-counter-delay-configurable*/} -这个例子中,有一个由滑动条控制的state变量`delay`,但是它的值没有被用到。请将`delay`值传给你的自定义Hook `useCounter`,修改`useCounter` Hook,用传过去的`delay`代替硬编码`1000`毫秒。 +这个例子中有一个由滑动条控制的state变量`delay`,但是它的值没有被用到。请将`delay`值传给你的自定义Hook `useCounter`,修改`useCounter` Hook,用传过去的`delay`代替硬编码`1000`毫秒。 @@ -2012,7 +2012,7 @@ export function useCounter() { -使用`useCounter(delay)`将`delay`传入你的Hook。然后在Hook内部使用`delay` 而不是硬编码`1000` 。你需要向你的Effect依赖项中加入`delay` 。这保证了`delay`的变化会重置间隔时间。 +使用`useCounter(delay)`将`delay`传入你的Hook。然后在Hook内部使用`delay` 而不是硬编码`1000`。你需要向你的Effect依赖项中加入`delay`。这保证了`delay`的变化会重置间隔时间。 @@ -2064,7 +2064,7 @@ export function useCounter(delay) { #### 从 `useCounter`中提取 `useInterval` {/*extract-useinterval-out-of-usecounter*/} -现在`useCounter` Hook做两件事。设置一个时间间隔,并且在每个时间间隔的tick内递增一次state变量。将设置时间间隔的逻辑拆分到到一个`useInterval`的独立Hook中。它应该输入两个参数: `onTick` 回调函数和`delay`。修改后`useCounter` 的实现应该如下所示: +现在`useCounter` Hook做两件事。设置一个时间间隔,并且在每个时间间隔的tick内递增一次state变量。将设置时间间隔的逻辑拆分到到一个`useInterval`的独立Hook中。它应该输入两个参数:`onTick` 回调函数和`delay`。修改后`useCounter` 的实现应该如下所示: ```js export function useCounter(delay) { @@ -2152,7 +2152,7 @@ export function useInterval(onTick, delay) { -请注意,这个解决方案有一些你将会下一个挑战中解决的问题。 +请注意这个解决方案有一些你将会下一个挑战中解决的问题。 @@ -2160,9 +2160,9 @@ export function useInterval(onTick, delay) { 这个例子有**两个**独立的计时器。 -`App`组件调用`useCounter`,这个Hook调用`useInterval`来每秒更新一次计数器。但是`App`组件**也**调用`useInterval`来每两秒随机更新一次页面背景色 +`App`组件调用`useCounter`,这个Hook调用`useInterval`来每秒更新一次计数器。但是`App`组件**也**调用`useInterval`来每两秒随机更新一次页面背景色。 -因为一些原因,更新页面背景色的回调函数从未执行。在 `useInterval`内部添加一些log。 +更新页面背景色的回调函数因为一些原因从未执行。在 `useInterval`内部添加一些log。 ```js {2,5} useEffect(() => { @@ -2177,7 +2177,7 @@ export function useInterval(onTick, delay) { 这些log符合你的预期吗?如果你的一些Effect似乎不必要的重新同步了,你能猜中哪一个依赖导致这个情况发生吗?有其他方式从你的Effect中[移除依赖](/learn/removing-effect-dependencies)吗? -你修复这个问题以后,应该希望页面背景每两秒更新一次。 +你修复这个问题以后,应该希望页面背景每两秒更新一次。 @@ -2254,7 +2254,7 @@ export function useInterval(onTick, delay) { 这将让你可以从Effect的依赖项中删掉`onTick`。每次组件重新渲染时,Effect将不会重新同步,所以页面背景颜色更新间隔不会在有机会触发之前每秒重置一次。 -随着这个修改,两个interval都会像预期的一样工作并且不会互相干预: +随着这个修改,两个interval都会像预期的一样工作并且不会互相干预: @@ -2323,19 +2323,19 @@ export function useInterval(callback, delay) { #### 实现一个交错的运动 {/*implement-a-staggering-movement*/} -这个例子中,`usePointerPosition()`Hook追踪当前指针位置。尝试移动鼠标或你的手指到预览区域上方,看到有一个红点随着你移动。它的位置被保存在`pos1`变量中。 +这个例子中,`usePointerPosition()`Hook追踪当前指针位置。尝试移动光标或你的手指到预览区域上方,看到有一个红点随着你移动。它的位置被保存在变量`pos1`中。 -事实上,有5(!)个不同的红点正在被渲染。你看不见是因为他们都出现在了同一位置。这就是你需要修复的问题。你想要实现的是一个 “交错的”运动:每个圆点应该“跟随”它前一个点的路径。例如,如果你快速移动光标,第一个点应该立刻跟着它,第二个应该在小的延时后跟上第一个点,第三个点应该跟着第二个点等。 +事实上,有5(!)个不同的红点正在被渲染。你看不见是因为他们都出现在了同一位置。这就是你需要修复的问题。你想要实现的是一个“交错的”运动:每个圆点应该“跟随”它前一个点的路径。例如如果你快速移动光标,第一个点应该立刻跟着它,第二个应该在小小的延时后跟上第一个点,第三个点应该跟着第二个点等。 你需要实现自定义Hook `useDelayedValue`。它当前的实现是返回提供给它的`value`。而你想从`delay`毫秒之前返回`value`。你可能需要一些state和一个Effect来完成这个任务。 -实现`useDelayedValue`后,你应该看见这些点一个接一个运动。 +实现`useDelayedValue`后,你应该看见这些点一个接一个运动。 你需要在自定义Hook内部存储一个state变量`delayedValue`。当`value`变化时,你需要运行一个Effect。这个Effect应该在`delay`毫秒后更新`delayedValue`。你可能发现调用`setTimeout`很有帮助。 -这个Effect 需要清除吗? 为什么? +这个Effect 需要清除吗?为什么? @@ -2485,7 +2485,7 @@ body { min-height: 300px; } -请注意这个Effect **不**需要清除。如果你在清理函数中调用了`clearTimeout`,那么每当`value`变化时,就会终止已经计划好的timeout。为了保持运动连续,你想要触发所有的timeout。 +请注意这个Effect **不**需要清理。如果你在清理函数中调用了`clearTimeout`,那么每当`value`变化时,就会终止已经计划好的timeout。为了保持运动连续,你想要触发所有的timeout。 From 5983dd7d854b63a8410880675adfca32d0b9691f Mon Sep 17 00:00:00 2001 From: yanyue Date: Fri, 5 May 2023 13:14:28 +0800 Subject: [PATCH 07/14] docs(cn): handle all whiteSpace in learn/reusing-logic-with-custom-hooks.md --- .../learn/reusing-logic-with-custom-hooks.md | 260 +++++++++--------- 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index 5a59b072c7..6a9c23c88e 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -4,27 +4,27 @@ title: 使用自定义Hook重用逻辑 -React带有一些内置的Hook,比如`useState`, `useContext`和`useEffect`。有时候你需要一个用途更加特定的Hook:比如远程获取数据,追踪用户是否在线,或者连接一个聊天室。在React中可能找不到这些Hook,但是你可以根据自己应用的需求取创建自己的Hook。 +React 带有一些内置的 Hook,比如 `useState`, `useContext` 和 `useEffect`。有时候你需要一个用途更加特定的 Hook:比如远程获取数据,追踪用户是否在线,或者连接一个聊天室。在 React 中可能找不到这些 Hook,但是你可以根据自己应用的需求取创建自己的 Hook。 -- 什么是自定义Hook,以及如何写自己的Hook +- 什么是自定义 Hook,以及如何写自己的 Hook - 如何在组件间重用逻辑 -- 如何命名和构建你的自定义Hook -- 提取自定义Hook的时机和原因 +- 如何命名和构建你的自定义 Hook +- 提取自定义 Hook 的时机和原因 -## 自定义Hook:在组件间共享逻辑 {/*custom-hooks-sharing-logic-between-components*/} +## 自定义 Hook:在组件间共享逻辑 {/*custom-hooks-sharing-logic-between-components*/} 假设你正在开发一款重度依赖网络的应用(和大多数应用一样)。当用户使用你的应用时,如果网络意外断开,你想要警告用户。你会如何处理这种情况呢?看上去你在组件中需要两个东西: -1. 一个追踪网络是否在线的state。 -2. 一个订阅全局[`在线`](https://developer.mozilla.org/en-US/docs/Web/API/Window/online_event)和[`离线`](https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event)事件以及更新上述state的Effect。 +1. 一个追踪网络是否在线的 state。 +2. 一个订阅全局 [`在线`](https://developer.mozilla.org/en-US/docs/Web/API/Window/online_event) 和 [`离线`](https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event) 事件以及更新上述 state 的 Effect。 -这将让你的组件与网络状态保持[同步](/learn/synchronizing-with-effects)。你可以像这样开始: +这将让你的组件与网络状态保持 [同步](/learn/synchronizing-with-effects)。你可以像这样开始: @@ -54,11 +54,11 @@ export default function StatusBar() { -尝试开启和关闭网络,注意`StatusBar`应对你的行为是如何更新的。 +尝试开启和关闭网络,注意 `StatusBar` 应对你的行为是如何更新的。 -现在假设你想要在一个不同的组件里**也**使用这段相同的逻辑。你想实现一个Save按钮,当网络离线时,这个按钮会变成不可用并且显示“Reconnecting...”而不是“Save”。 +现在假设你想要在一个不同的组件里 **也** 使用这段相同的逻辑。你想实现一个 Save 按钮,当网络离线时,这个按钮会变成不可用并且显示“Reconnecting...”而不是“Save”。 -你可以通过复制和粘贴`isOnline` state和Effect到`SaveButton`开始: +你可以通过复制和粘贴 `isOnline` state 和 Effect 到 `SaveButton` 开始: @@ -98,11 +98,11 @@ export default function SaveButton() { 验证一下,如果关闭网络,按钮会变更展示。 -这两个组件都工作正常,但是不幸的是他们之间的逻辑重复了。即使两个组件看上去有不同的**视觉界面**,你也想要重用他们之间的逻辑。 +这两个组件都工作正常,但是不幸的是他们之间的逻辑重复了。即使两个组件看上去有不同的 **视觉界面**,你也想要重用他们之间的逻辑。 -### 从组件中提取出你的自定义Hook {/*extracting-your-own-custom-hook-from-a-component*/} +### 从组件中提取出你的自定义 Hook {/*extracting-your-own-custom-hook-from-a-component*/} -想象一下,与[`useState`](/reference/react/useState)和[`useEffect`](/reference/react/useEffect)类似,有一个内置的`useOnlineStatus`Hook。那么就可以简化这两个组件并且移除他们之间的重复部分: +想象一下,与 [`useState`](/reference/react/useState) 和 [`useEffect`](/reference/react/useEffect) 类似,有一个内置的 `useOnlineStatus` Hook。那么就可以简化这两个组件并且移除他们之间的重复部分: ```js {2,7} function StatusBar() { @@ -125,7 +125,7 @@ function SaveButton() { } ``` -尽管目前没有这样的内置Hook,但是你可以自己写。声明一个`useOnlineStatus`函数,并且把早前组件里的所有重复代码移到里面: +尽管目前没有这样的内置 Hook,但是你可以自己写。声明一个 `useOnlineStatus` 函数,并且把早前组件里的所有重复代码移到里面: ```js {2-16} function useOnlineStatus() { @@ -213,32 +213,32 @@ export function useOnlineStatus() { 现在你的组件里没有那么多重复的逻辑了。**更重要的是,组件中的代码描述了他们想要做什么(使用在线状态!),而不是如何做(通过订阅浏览器事件完成)** -当提取逻辑到自定义Hook中时,你可以隐藏如何处理一些外部系统或者浏览器API的艰难细节。组件中的代码表达的是你的目的而不是实现。 +当提取逻辑到自定义 Hook 中 时,你可以隐藏如何处理一些外部系统或者浏览器 API 的艰难细节。组件中的代码表达的是你的目的而不是实现。 -### Hook的名称必须永远以 `use`开头 {/*hook-names-always-start-with-use*/} +### Hook 的名称必须永远以 `use` 开头 {/*hook-names-always-start-with-use*/} -React 应用是由组件构建的。组件是由内置的或者自定义的Hook构建的。你可能经常使用别人创建的自定义Hook,但是偶尔也可能要自己写! +React 应用是由组件构建的。组件是由内置的或者自定义的 Hook 构建的。你可能经常使用别人创建的自定义 Hook,但是偶尔也可能要自己写! 你必须遵循以下这些命名公约: -1. **React组件名称必须以大写字母开头**, 比如 `StatusBar` 和 `SaveButton`。React 组件还需要返回一些React知道如何展示的内容,比如一段JSX代码。 -2. **Hook的名称必须以 `use`开头,后面跟一个大写字母**, 像 [`useState`](/reference/react/useState) (内置) 或者 `useOnlineStatus` (像文章早前的自定义Hook)。Hook可能会返回任意值。 +1. **React 组件名称必须以大写字母开头**, 比如 `StatusBar` 和 `SaveButton`。React 组件还需要返回一些 React 知道如何展示的内容,比如一段 JSX 代码。 +2. **Hook 的名称必须以 `use` 开头,后面跟一个大写字母**, 像 [`useState`](/reference/react/useState) (内置) 或者 `useOnlineStatus` (像文章早前的自定义 Hook)。Hook 可能会返回任意值。 -这个公约保证了你始终可以查看组件并且知道它的state,Effect以及其他的React特性可能“隐藏”在哪里。例如如果你在组件内部看见`getColor()`的函数调用,你可以确定它内部不可能包含React state,因为它的名称没有以`use`开头。但是像`useOnlineStatus()`这样的函数调用将极有可能包含对内部其他Hook的调用! +这个公约保证了你始终可以查看组件并且知道它的 state,Effect 以及其他的 React 特性可能“隐藏”在哪里。例如如果你在组件内部看见 `getColor()` 的函数调用,你可以确定它内部不可能包含 React state,因为它的名称没有以 `use` 开头。但是像 `useOnlineStatus()` 这样的函数调用将极有可能包含对内部其他 Hook 的调用! -如果为[React配置了](/learn/editor-setup#linting)检查工具,它会强制执行这个命名公约。滑动到上面的sandbox,并将`useOnlineStatus`重命名为`getOnlineStatus`。注意检查工具将不会再允许你在内部调用`useState` 或者 `useEffect`。只有Hook和组件可以调用其他Hook! +如果为 [React 配置了](/learn/editor-setup#linting) 检查工具,它会强制执行这个命名公约。滑动到上面的 sandbox,并将 `useOnlineStatus` 重命名为 `getOnlineStatus`。注意检查工具将不会再允许你在内部调用 `useState` 或者 `useEffect`。只有 Hook 和组件可以调用其他 Hook! -#### 渲染期间调用的所有函数都应该以前缀use开头么? {/*should-all-functions-called-during-rendering-start-with-the-use-prefix*/} +#### 渲染期间调用的所有函数都应该以前缀 use 开头么? {/*should-all-functions-called-during-rendering-start-with-the-use-prefix*/} -不是。没有**调用**Hook的函数不需要**成为**Hook。 +不是。没有 **调用** Hook 的函数不需要 **成为** Hook。 -如果你的函数没有调用任何Hook,请避免使用`use` 前缀。 而是**不带**`use`前缀,把它作为常规函数去写。例如下面的`useSorted` 没有调用Hook,所以叫它 `getSorted`: +如果你的函数没有调用任何 Hook,请避免使用 `use` 前缀。 而是 **不带** `use` 前缀,把它作为常规函数去写。例如下面的 `useSorted` 没有调用 Hook,所以叫它 `getSorted`: ```js // 🔴 避免:没有调用其他Hook的Hook @@ -265,7 +265,7 @@ function List({ items, shouldSort }) { } ``` -如果内部至少使用了一个Hook,你应该给这个函数`use`前缀(从而让它成为一个Hook): +如果内部至少使用了一个 Hook,你应该给这个函数 `use` 前缀(从而让它成为一个 Hook): ```js // ✅ Good:一个使用了其他Hook的Hook @@ -274,7 +274,7 @@ function useAuth() { } ``` -从技术上讲,这不是React强制的。原则上你可以写一个不调用其他Hook的Hook。这常常会令人迷惑且受到限制,所以最好是避免那种方式。但是在极少一些场景下,它可能是有帮助的。例如也许你的函数现在没有使用任何Hook,但是计划未来会添加一些Hook调用。那么使用`use`前缀给它命名就很有意义: +从技术上讲,这不是 React 强制的。原则上你可以写一个不调用其他 Hook 的 Hook。这常常会令人迷惑且受到限制,所以最好是避免那种方式。但是在极少一些场景下,它可能是有帮助的。例如也许你的函数现在没有使用任何 Hook,但是计划未来会添加一些 Hook 调用。那么使用 `use` 前缀给它命名就很有意义: ```js {3-4} // ✅ Good:之后即将可能使用一些其他Hook的一个Hook @@ -285,13 +285,13 @@ function useAuth() { } ``` -接下来组件就不能在条件分支里调用这个函数。当你在里面添加了Hook调用时,这一点将变得很重要。如果你没有计划在内部使用Hook(现在或者之后),就不要让它成为一个Hook。 +接下来组件就不能在条件分支里调用这个函数。当你在里面添加了 Hook 调用时,这一点将变得很重要。如果你没有计划在内部使用 Hook(现在或者之后),就不要让它成为一个 Hook。 -### 自定义Hook共享的是状态逻辑,而不是state本身 {/*custom-hooks-let-you-share-stateful-logic-not-state-itself*/} +### 自定义 Hook 共享的是状态逻辑,而不是 state 本身 {/*custom-hooks-let-you-share-stateful-logic-not-state-itself*/} -之前的例子里,当你开启或关闭网络时,两个组件一起更新了。但是两个组件共享一个`isOnline`state变量的想法是错误的。看这段代码: +之前的例子里,当你开启或关闭网络时,两个组件一起更新了。但是两个组件共享一个 `isOnline` state 变量的想法是错误的。看这段代码: ```js {2,7} function StatusBar() { @@ -325,7 +325,7 @@ function SaveButton() { } ``` -这是两个完全独立的state变量和Effect!他们只是碰巧同时有同样的值,因为你将两个组件与相同的外部值同步(无论网络是否开启)。 +这是两个完全独立的 state 变量和 Effect!他们只是碰巧同时有同样的值,因为你将两个组件与相同的外部值同步(无论网络是否开启)。 为了更好的说明这一点,我们需要一个不同的例子。看下面的 `Form` 组件: @@ -371,11 +371,11 @@ input { margin-left: 10px; } 每个表单域都有一些重复的逻辑: -1. 都有state(`firstName` 和 `lastName`)。 -1. 都有change handler(`handleFirstNameChange` 和 `handleLastNameChange`)。 -1. 都有为输入框指定`value` 和 `onChange`属性的JSX片段。 +1. 都有 state(`firstName` 和 `lastName`)。 +1. 都有 change handler(`handleFirstNameChange` 和 `handleLastNameChange`)。 +1. 都有为输入框指定 `value` 和 `onChange` 属性的 JSX 片段。 -你可以把重复的逻辑提取到自定义Hook`useFormInput`: +你可以把重复的逻辑提取到自定义 Hook `useFormInput`: @@ -428,9 +428,9 @@ input { margin-left: 10px; } -注意它只声明了**一个**叫做 `value` 的state变量。 +注意它只声明了 **一个** 叫做 `value` 的 state 变量。 -但是`Form`组件调用了**两次**`useFormInput`: +但是 `Form` 组件调用了 **两次** `useFormInput`: ```js function Form() { @@ -439,17 +439,17 @@ function Form() { // ... ``` -这就是为什么它工作的时候像声明了两个独立的state变量! +这就是为什么它工作的时候像声明了两个独立的 state 变量! -**自定义Hook只是共享状态逻辑而不是state本身。每个Hook的调用都完全独立于对同一个Hook的其他调用**。 这就是为什么上面两个sandbox完全相同的原因。如果你愿意,可以滚动回去并比较他们。提取自定义Hook之前和之后的行为是一致的。 +**自定义 Hook 只是共享状态逻辑而不是 state 本身。每个 Hook 的调用都完全独立于对同一个 Hook 的其他调用**。 这就是为什么上面两个 sandbox 完全相同的原因。如果你愿意,可以滚动回去并比较他们。提取自定义 Hook 之前和之后的行为是一致的。 -而当你需要在多个组件之间共享state本身时,需要[将变量提升并传递下去](/learn/sharing-state-between-components)。 +而当你需要在多个组件之间共享 state 本身时,需要[将变量提升并传递下去](/learn/sharing-state-between-components)。 -## 在Hook之间传递响应值 {/*passing-reactive-values-between-hooks*/} +## 在 Hook 之间传递响应值 {/*passing-reactive-values-between-hooks*/} -组件每次重新渲染,自定义Hook中的代码也会重新运行。这就是为什么组件和自定义Hook都[需要纯粹](/learn/keeping-components-pure)的原因。我们应该把自定义Hook的代码作为组件主体的一部分。 +组件每次重新渲染,自定义 Hook 中的代码也会重新运行。这就是为什么组件和自定义 Hook 都 [需要纯粹](/learn/keeping-components-pure) 的原因。我们应该把自定义 Hook 的代码作为组件主体的一部分。 -自定义组件总会接收到最新的props和state,因为它会和你的组件一起重新渲染。想知道这意味着什么,看一下这个聊天室的例子。变更Server URL或者聊天室ID: +自定义组件总会接收到最新的 props 和 state,因为它会和你的组件一起重新渲染。想知道这意味着什么,看一下这个聊天室的例子。变更 Server URL 或者聊天室 ID: @@ -599,9 +599,9 @@ button { margin-left: 10px; } -当你修改`serverUrl`或者`roomId`时,Effect会对[你的修改做出“反应”](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values)和重新同步。你可以通过修改Effect依赖引起的聊天室重连的每次console信息来区分。 +当你修改 `serverUrl` 或者 `roomId` 时,Effect 会对 [你的修改做出“反应”](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) 和重新同步。你可以通过修改 Effect 依赖引起的聊天室重连的每次 console 信息来区分。 -现在把Effect代码移动到自定义Hook中: +现在把 Effect 代码移动到自定义 Hook 中: ```js {2-13} export function useChatRoom({ serverUrl, roomId }) { @@ -620,7 +620,7 @@ export function useChatRoom({ serverUrl, roomId }) { } ``` -这让你的`ChatRoom`组件调用自定义Hook,不需要担心内部是如何工作: +这让你的 `ChatRoom` 组件调用自定义 Hook,不需要担心内部是如何工作: ```js {4-7} export default function ChatRoom({ roomId }) { @@ -645,7 +645,7 @@ export default function ChatRoom({ roomId }) { 这看上去简洁多了!(但是它做的是同一件事情。) -注意逻辑**仍然响应** prop和state修改。尝试编辑server URL或者选中的房间: +注意逻辑 **仍然响应** prop 和 state 修改。尝试编辑 server URL 或者选中的房间: @@ -807,7 +807,7 @@ button { margin-left: 10px; } -注意你是如何获取到Hook的返回值: +注意你是如何获取到 Hook 的返回值: ```js {2} export default function ChatRoom({ roomId }) { @@ -820,7 +820,7 @@ export default function ChatRoom({ roomId }) { // ... ``` -并且把它作为输入传递给另外一个Hook: +并且把它作为输入传递给另外一个 Hook: ```js {6} export default function ChatRoom({ roomId }) { @@ -833,17 +833,17 @@ export default function ChatRoom({ roomId }) { // ... ``` -每次`ChatRoom`组件重新渲染,它都会传最新的`roomId`和`serverUrl`到你的Hook中。这就是为什么每当他们的值在重新渲染后不同的时候你的Effect会重连聊天室。(如果你曾经使用过音视频处理软件,像这样的链式Hook也许会让你想起链式可视化或音频effect。就好像`useState` 的输出作为 `useChatRoom`的输入)。 +每次 `ChatRoom` 组件重新渲染,它都会传最新的 `roomId` 和 `serverUrl` 到你的 Hook 中。这就是为什么每当他们的值在重新渲染后不同的时候你的 Effect 会重连聊天室。(如果你曾经使用过音视频处理软件,像这样的链式 Hook 也许会让你想起链式可视化或音频 effect。就好像 `useState` 的输出作为 `useChatRoom` 的输入)。 -### 把事件处理器传到自定义Hook中 {/*passing-event-handlers-to-custom-hooks*/} +### 把事件处理器传到自定义 Hook 中 {/*passing-event-handlers-to-custom-hooks*/} -这个章节描述React稳定版中**还没有发布的实验性API**。 +这个章节描述 React 稳定版中 **还没有发布的实验性 API**。 -当你在更多组件中使用`useChatRoom`组件时,你也许想要让组件自定义它的行为。例如现在Hook中收到信息时做什么的逻辑是硬编码: +当你在更多组件中使用 `useChatRoom` 组件时,你也许想要让组件自定义它的行为。例如现在 Hook 中收到信息时做什么的逻辑是硬编码: ```js {9-11} export function useChatRoom({ serverUrl, roomId }) { @@ -878,7 +878,7 @@ export default function ChatRoom({ roomId }) { // ... ``` -完成这个工作需要修改自定义Hook,把`onReceiveMessage`作为命名选项之一: +完成这个工作需要修改自定义 Hook,把 `onReceiveMessage` 作为命名选项之一: ```js {1,10,13} export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { @@ -897,9 +897,9 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { } ``` -这将会生效,但是当自定义Hook接受事件处理器的时候,你还可以做另一个改进。 +这将会生效,但是当自定义 Hook 接受事件处理器的时候,你还可以做另一个改进。 -增加一个`onReceiveMessage`依赖并不理想,每次只要组件重新渲染,聊天就会重新连接。[将这个事件处理器包装到Effect Event从而将它从依赖中移除](/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 }) { } ``` -现在聊天室不会在每次`ChatRoom` 组件重新渲染时都重新连接。这是一个传递事件处理器给自定义Hook的例子,你可以尝试一下: +现在聊天室不会在每次 `ChatRoom` 组件重新渲染时都重新连接。这是一个传递事件处理器给自定义 Hook 的例子,你可以尝试一下: @@ -1091,13 +1091,13 @@ button { margin-left: 10px; } -注意你不再需要为了使用它而去了解`useChatRoom`是**如何**工作的。你可以把它添加到其他任意组件,传递其他任意选项,而它会以同样的方式工作。这就是自定义Hook的强大之处。 +注意你不再需要为了使用它而去了解 `useChatRoom` 是 **如何** 工作的。你可以把它添加到其他任意组件,传递其他任意选项,而它会以同样的方式工作。这就是自定义 Hook 的强大之处。 -## 什么时候使用自定义Hook {/*when-to-use-custom-hooks*/} +## 什么时候使用自定义 Hook {/*when-to-use-custom-hooks*/} -你不需要把每段重复的代码提取为自定义Hook。一些重复是可以的。例如像早前用来包裹单个`useState`调用的`useFormInput`Hook可能就是没有必要的。 +你不需要把每段重复的代码提取为自定义 Hook。一些重复是可以的。例如像早前用来包裹单个 `useState` 调用的 `useFormInput` Hook 可能就是没有必要的。 -但是每当你写Effect的时候,请考虑一下把它包裹在自定义Hook会不会更清晰。[你不应该经常使用Effect](/learn/you-might-not-need-an-effect),所以如果你正在写Effect就意味着你需要“走出React”来和一些外部系统同步,或者需要做一些React中没有内置API的事。把重复代码包装进自定义Hook可以让你准确表达你的意图和数据在里面是如何流动的。 +但是每当你写 Effect 的时候,请考虑一下把它包裹在自定义 Hook 会不会更清晰。[你不应该经常使用 Effect](/learn/you-might-not-need-an-effect),所以如果你正在写 Effect 就意味着你需要“走出 React”来和一些外部系统同步,或者需要做一些 React 中没有内置 API 的事。把重复代码包装进自定义 Hook 可以让你准确表达你的意图和数据在里面是如何流动的。 例如假设 `ShippingForm` 组件展示两个下拉框:一个展示城市列表,另一个展示选中的城市的区域列表。你可能会像这样开始写代码: @@ -1141,7 +1141,7 @@ function ShippingForm({ country }) { // ... ``` -尽管这部分代码是重复的,但是[把这些Effect各自分开是正确的](/learn/removing-effect-dependencies#is-your-effect-doing-several-unrelated-things)。他们同步两个不同的事情,所以你不应该把他们合并到同一个Effect。取而代之的是,你可以提取他们的通用逻辑到你自己的`useData` Hook中来简化上面的`ShippingForm` 组件: +尽管这部分代码是重复的,但是 [把这些 Effect 各自分开是正确的](/learn/removing-effect-dependencies#is-your-effect-doing-several-unrelated-things)。他们同步两个不同的事情,所以你不应该把他们合并到同一个 Effect。取而代之的是,你可以提取他们的通用逻辑到你自己的 `useData` Hook 中来简化上面的 `ShippingForm` 组件: ```js {2-18} function useData(url) { @@ -1165,7 +1165,7 @@ function useData(url) { } ``` -现在你可以调用`useData`代替`ShippingForm`组件中的Effect: +现在你可以调用 `useData` 代替 `ShippingForm` 组件中的 Effect: ```js {2,4} function ShippingForm({ country }) { @@ -1175,33 +1175,33 @@ function ShippingForm({ country }) { // ... ``` -提取自定义Hook可以让数据流清晰。你可以输入`url`,输出 `data`。通过把你的Effect“隐藏”在`useData`中,也可以防止一些正在处理`ShippingForm`组件的人向里面添加[不必要的依赖](/learn/removing-effect-dependencies)。随着时间的推移,你app中的大部分Effect都会存在于自定义Hook中。 +提取自定义 Hook 可以让数据流清晰。你可以输入 `url`,输出 `data`。通过把你的 Effect “隐藏”在 `useData` 中,也可以防止一些正在处理 `ShippingForm` 组件的人向里面添加 [不必要的依赖](/learn/removing-effect-dependencies)。随着时间的推移,你 app 中的大部分 Effect 都会存在于自定义 Hook 中。 -#### 让你的自定义Hook专注于具体的高级用例 {/*keep-your-custom-hooks-focused-on-concrete-high-level-use-cases*/} +#### 让你的自定义 Hook 专注于具体的高级用例 {/*keep-your-custom-hooks-focused-on-concrete-high-level-use-cases*/} -从选择你的自定义Hook名称开始。如果你难以选择一个清晰的名称,这意味着你的Effect和组件逻辑的剩余部分耦合度太高,还没有准备好被提取出来。 +从选择你的自定义 Hook 名称开始。如果你难以选择一个清晰的名称,这意味着你的 Effect 和组件逻辑的剩余部分耦合度太高,还没有准备好被提取出来。 -理想情况下,你的自定义Hook名称应该清晰到即使一个不经常写代码的人也能很好地猜中你的自定义Hook的功能,输入和返回: +理想情况下,你的自定义 Hook 名称应该清晰到即使一个不经常写代码的人也能很好地猜中你的自定义 Hook 的功能,输入和返回: * ✅ `useData(url)` * ✅ `useImpressionLog(eventName, extraData)` * ✅ `useChatRoom(options)` -当你和外部系统同步的时候,你的自定义Hook名称可能会更加专业,并使用该系统特定的术语。只要这个名称对于熟悉这个系统的人来说清晰,那就是好的: +当你和外部系统同步的时候,你的自定义 Hook 名称可能会更加专业,并使用该系统特定的术语。只要这个名称对于熟悉这个系统的人来说清晰,那就是好的: * ✅ `useMediaQuery(query)` * ✅ `useSocket(url)` * ✅ `useIntersectionObserver(ref, options)` -**保持自定义Hook专注于具体的高级用例**。避免创建和使用作为`useEffect`API本身的替代品和wrapper的自定义“生命周期”Hook: +**保持自定义 Hook 专注于具体的高级用例**。避免创建和使用作为 `useEffect` API 本身的替代品和 wrapper 的自定义“生命周期” Hook: * 🔴 `useMount(fn)` * 🔴 `useEffectOnce(fn)` * 🔴 `useUpdateEffect(fn)` -例如这个 `useMount` Hook试图保证一些代码只在“加载”的时候运行: +例如这个 `useMount` Hook 试图保证一些代码只在“加载”的时候运行: ```js {4-5,14-15} function ChatRoom({ roomId }) { @@ -1225,9 +1225,9 @@ function useMount(fn) { } ``` -**像`useMount`这样的自定义“生命周期” Hook不能很好的适应React模式**。例如示例代码有一个错误(它没有“响应” `roomId`或`serverUrl`的变化),但是代码检查工具并不会向你发出对应的告警,因为代码检查工具只能检测直接的`useEffect`调用。它并不了解你的Hook。 +**像 `useMount` 这样的自定义“生命周期” Hook 不能很好的适应 React 模式**。例如示例代码有一个错误(它没有“响应” `roomId` 或 `serverUrl` 的变化),但是代码检查工具并不会向你发出对应的告警,因为代码检查工具只能检测直接的 `useEffect` 调用。它并不了解你的 Hook。 -如果你正在编写Effect,请从直接使用React API开始: +如果你正在编写 Effect,请从直接使用 React API 开始: ```js function ChatRoom({ roomId }) { @@ -1249,7 +1249,7 @@ function ChatRoom({ roomId }) { } ``` -然后你可以(但不是必须)为不同的高级用例提取自定义Hook: +然后你可以(但不是必须)为不同的高级用例提取自定义 Hook: ```js function ChatRoom({ roomId }) { @@ -1262,13 +1262,13 @@ function ChatRoom({ roomId }) { } ``` -**一个好的自定义Hook通过限制功能使代码调用更具声明性**。例如`useChatRoom(options)`只能连接聊天室,而`useImpressionLog(eventName, extraData)`只能向分析系统发送impression日志。如果你的自定义Hook API 没有约束用例且非常抽象,那么在长期的运行中,比起它解决的问题,可能会引入更多问题。 +**一个好的自定义 Hook 通过限制功能使代码调用更具声明性**。例如 `useChatRoom(options)` 只能连接聊天室,而 `useImpressionLog(eventName, extraData)` 只能向分析系统发送 impression 日志。如果你的自定义 Hook API 没有约束用例且非常抽象,那么在长期的运行中,比起它解决的问题,可能会引入更多问题。 -### 自定义Hook帮助你迁移到更好的模式 {/*custom-hooks-help-you-migrate-to-better-patterns*/} +### 自定义 Hook 帮助你迁移到更好的模式 {/*custom-hooks-help-you-migrate-to-better-patterns*/} -Effect是一个[“应急出口”](/learn/escape-hatches):当你需要“走出React”且对于你的用例没有更好的内置解决方案的时候你可以使用他们。随着时间的推移,React团队的目标是通过给更多特定问题提供特定解决方案来最小化应用中的Effect数量。把你的Effect包裹进自定义Hook会使得这些解决方案可用的时候升级代码更加容易。 +Effect 是一个 [“应急出口”](/learn/escape-hatches):当你需要“走出 React”且对于你的用例没有更好的内置解决方案的时候你可以使用他们。随着时间的推移,React 团队的目标是通过给更多特定问题提供特定解决方案来最小化应用中的 Effect 数量。把你的 Effect 包裹进自定义 Hook 会使得这些解决方案可用的时候升级代码更加容易。 让我们回到这个例子: @@ -1331,9 +1331,9 @@ export function useOnlineStatus() { -在上面的例子中,`useOnlineStatus`借助[`useState`](/reference/react/useState)和[`useEffect`](/reference/react/useEffect) 实现。但这不是最好的解决方案。它有许多没有考虑到的边界用例。例如假设当组件加载的时候,`isOnline`已经是 `true`,但是如果网络已经离线的话这就是错误的。你可以使用浏览器的[`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine)API来检查,但是直接在生成初始HTML的服务器上使用它是不生效的。简而言之这段代码可以改进。 +在上面的例子中,`useOnlineStatus` 借助 [`useState`](/reference/react/useState) 和 [`useEffect`](/reference/react/useEffect) 实现。但这不是最好的解决方案。它有许多没有考虑到的边界用例。例如假设当组件加载的时候,`isOnline` 已经是 `true`,但是如果网络已经离线的话这就是错误的。你可以使用浏览器的 [`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine) API 来检查,但是直接在生成初始 HTML 的服务器上使用它是不生效的。简而言之这段代码可以改进。 -幸运的是,React 18包含了一个叫做[`useSyncExternalStore`]的专用API,它可以解决所有这些问题。这里是如何利用这个新API来重写你的`useOnlineStatus` Hook: +幸运的是,React 18 包含了一个叫做 [`useSyncExternalStore`](/reference/react/useSyncExternalStore) 的专用 API,它可以解决所有这些问题。这里是如何利用这个新 API 来重写你的 `useOnlineStatus` Hook: @@ -1393,7 +1393,7 @@ export function useOnlineStatus() { -注意**不需要修改任何组件**如何来完成这次迁移: +注意 **不需要修改任何组件** 如何来完成这次迁移: ```js {2,7} function StatusBar() { @@ -1407,17 +1407,17 @@ function SaveButton() { } ``` -这是为什么把Effect包裹进自定义Hook是有益的另一个原因: +这是为什么把 Effect 包裹进自定义 Hook 是有益的另一个原因: -1. 你让进出Effect的数据流非常清晰。 -2. 你让组件专注于目标,而不是Effect的实现。 -3. 当React增加新特性时,你可以在不修改你的任何组件的情况下移除这些Effect。 +1. 你让进出 Effect 的数据流非常清晰。 +2. 你让组件专注于目标,而不是 Effect 的实现。 +3. 当 React 增加新特性时,你可以在不修改你的任何组件的情况下移除这些 Effect。 -和[设计系统](https://uxdesign.cc/everything-you-need-to-know-about-design-systems-54b109851969)类似,你可能会发现从应用的组件中提取通用逻辑到自定义Hook是非常有帮助的。这会让你的组件代码专注于目标,避免经常写原始Effect。许多很棒的自定义Hook是由React社区维护的。 +和 [设计系统](https://uxdesign.cc/everything-you-need-to-know-about-design-systems-54b109851969) 类似,你可能会发现从应用的组件中提取通用逻辑到自定义 Hook 是非常有帮助的。这会让你的组件代码专注于目标,避免经常写原始 Effect。许多很棒的自定义 Hook 是由 React 社区维护的。 -#### React会为远程数据获取提供内置的解决方案么? {/*will-react-provide-any-built-in-solution-for-data-fetching*/} +#### React 会为远程数据获取提供内置的解决方案么? {/*will-react-provide-any-built-in-solution-for-data-fetching*/} 我们仍然在规划细节,但是期望未来可以像这样写远程数据获取: @@ -1431,13 +1431,13 @@ function ShippingForm({ country }) { // ... ``` -比起在每个组件手动写原始Effect,如果你在应用中使用像上面的`useData`这样的自定义Hook,迁移到最终推荐方式所需要的更改更少。但是旧的方式仍然可以有效工作,所以如果你喜欢写原始Effect,你可以继续这样做。 +比起在每个组件手动写原始 Effect,如果你在应用中使用像上面的 `useData` 这样的自定义 Hook,迁移到最终推荐方式所需要的更改更少。但是旧的方式仍然可以有效工作,所以如果你喜欢写原始 Effect,你可以继续这样做。 ### 不止一个方法达到这个目的 {/*there-is-more-than-one-way-to-do-it*/} -假设你想要使用浏览器的[`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)API实现一个**从头开始的**渐入动画。你可能会从一个建立动画循环的Effect开始。在动画的每一帧中,你可以修改[ref持有的](/learn/manipulating-the-dom-with-refs)DOM节点的opacity属性直到它为`1`。你的代码可能这样开始: +假设你想要使用浏览器的 [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) API 实现一个 **从头开始的** 渐入动画。你可能会从一个建立动画循环的 Effect 开始。在动画的每一帧中,你可以修改 [ref 持有的](/learn/manipulating-the-dom-with-refs) DOM 节点的 opacity 属性直到它为 `1`。你的代码可能这样开始: @@ -1520,7 +1520,7 @@ html, body { min-height: 300px; } -为了让组件更具有可读性,你可能要将逻辑提取到自定义Hook`useFadeIn`: +为了让组件更具有可读性,你可能要将逻辑提取到自定义 Hook `useFadeIn`: @@ -1611,7 +1611,7 @@ html, body { min-height: 300px; } -你可以让`useFadeIn` 和原来保持一致,但是也可以更进一步重构。例如你可以把创建动画循环的逻辑从`useFadeIn`提取到自定义Hook`useAnimationLoop`: +你可以让 `useFadeIn` 和原来保持一致,但是也可以更进一步重构。例如你可以把创建动画循环的逻辑从 `useFadeIn` 提取到自定义 Hook `useAnimationLoop`: @@ -1715,7 +1715,7 @@ html, body { min-height: 300px; } -但是你**没有必要**这样做。和常规函数一样,最终是由你决定在哪里绘制代码不同部分之间的边界。你也可以采取不一样的方法。把大部分必要的逻辑移入一个[JavaScript类](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes),而不是把逻辑保留在Effect中: +但是你 **没有必要** 这样做。和常规函数一样,最终是由你决定在哪里绘制代码不同部分之间的边界。你也可以采取不一样的方法。把大部分必要的逻辑移入一个 [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes),而不是把逻辑保留在 Effect 中: @@ -1813,9 +1813,9 @@ html, body { min-height: 300px; } -Effect可以将React和外部系统连接起来。Effect之间需要的协调越多(例如链接多个动画),像上面的sandbox一样**完整地**从Effect和Hook中提取逻辑就越有意义。然后你提取的代码**变成**“外部系统”。这会让你的 Effect保持简单化,因为他们只需要向已经移动到React外部的系统发送消息。 +Effect 可以将 React 和外部系统连接起来。Effect 之间需要的协调越多(例如链接多个动画),像上面的 sandbox 一样 **完整地** 从 Effect 和 Hook 中提取逻辑就越有意义。然后你提取的代码 **变成** “外部系统”。这会让你的 Effect 保持简单化,因为他们只需要向已经移动到 React 外部的系统发送消息。 -上面这个例子假设需要使用JavaScript写fade-in逻辑。但是使用纯[CSS 动画](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations)实现这个特定的fade-in会更加简单和高效: +上面这个例子假设需要使用 JavaScript 写 fade-in 逻辑。但是使用纯 [CSS 动画](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations) 实现这个特定的 fade-in 会更加简单和高效: @@ -1870,18 +1870,18 @@ html, body { min-height: 300px; } -某些时候你甚至不需要Hook! +某些时候你甚至不需要 Hook! -- 自定义Hook让你可以在组件间共享逻辑。 -- 自定义Hook命名必须以后面跟着一个大写字母的`use`开头。 -- 自定义Hook共享的只是有状态的逻辑,而不是state 本身。 -- 你可以将响应值从一个Hook传到另一个,并且他们保持最新。 -- 每次你的组件重新渲染时,所有的Hook会重新运行。 -- 自定义Hook的代码应该和组件代码一样保持纯粹。 -- 把自定义Hook收到的事件处理器封装到Effect Event。 -- 不要创建像`useMount`这样的自定义Hook。保持目标具体化。 +- 自定义 Hook 让你可以在组件间共享逻辑。 +- 自定义 Hook 命名必须以后面跟着一个大写字母的 `use` 开头。 +- 自定义 Hook 共享的只是有状态的逻辑,而不是 state 本身。 +- 你可以将响应值从一个 Hook 传到另一个,并且他们保持最新。 +- 每次你的组件重新渲染时,所有的 Hook 会重新运行。 +- 自定义 Hook 的代码应该和组件代码一样保持纯粹。 +- 把自定义 Hook 收到的事件处理器封装到 Effect Event。 +- 不要创建像 `useMount` 这样的自定义 Hook。保持目标具体化。 - 如何以及在哪里选择代码边界取决于你自己。 @@ -1890,7 +1890,7 @@ html, body { min-height: 300px; } #### 提取一个 `useCounter` Hook {/*extract-a-usecounter-hook*/} -这个组件使用了一个state变量和一个Effect来展示每秒递增的一个数字。把逻辑提取到一个`useCounter`的自定义Hook中。你的目标是让`Counter` 组件的实现看上去和这个一样: +这个组件使用了一个 state 变量和一个 Effect 来展示每秒递增的一个数字。把逻辑提取到一个 `useCounter` 的自定义 Hook 中。你的目标是让 `Counter` 组件的实现看上去和这个一样: ```js export default function Counter() { @@ -1899,7 +1899,7 @@ export default function Counter() { } ``` -你需要在 `useCounter.js`中编写你的自定义Hook,并且把它引入到`Counter.js` 文件。 +你需要在 `useCounter.js` 中编写你的自定义 Hook,并且把它引入到 `Counter.js` 文件。 @@ -1919,7 +1919,7 @@ export default function Counter() { ``` ```js useCounter.js -// 在这个文件中编写你的自定义Hook! +// 在这个文件中编写你的自定义 Hook! ``` @@ -1956,13 +1956,13 @@ export function useCounter() { -注意`App.js` 不再需要引入`useState` 或者 `useEffect`。 +注意 `App.js` 不再需要引入 `useState` 或者 `useEffect`。 -#### 让计时器的delay变为可配置 {/*make-the-counter-delay-configurable*/} +#### 让计时器的 delay 变为可配置 {/*make-the-counter-delay-configurable*/} -这个例子中有一个由滑动条控制的state变量`delay`,但是它的值没有被用到。请将`delay`值传给你的自定义Hook `useCounter`,修改`useCounter` Hook,用传过去的`delay`代替硬编码`1000`毫秒。 +这个例子中有一个由滑动条控制的 state 变量 `delay`,但是它的值没有被用到。请将 `delay` 值传给你的自定义 Hook `useCounter`,修改 `useCounter` Hook,用传过去的 `delay` 代替硬编码 `1000` 毫秒。 @@ -2012,7 +2012,7 @@ export function useCounter() { -使用`useCounter(delay)`将`delay`传入你的Hook。然后在Hook内部使用`delay` 而不是硬编码`1000`。你需要向你的Effect依赖项中加入`delay`。这保证了`delay`的变化会重置间隔时间。 +使用 `useCounter(delay)` 将 `delay` 传入你的 Hook。然后在 Hook 内部使用 `delay` 而不是硬编码 `1000`。你需要向你的 Effect 依赖项中加入 `delay`。这保证了 `delay` 的变化会重置间隔时间。 @@ -2062,9 +2062,9 @@ export function useCounter(delay) { -#### 从 `useCounter`中提取 `useInterval` {/*extract-useinterval-out-of-usecounter*/} +#### 从 `useCounter` 中提取 `useInterval` {/*extract-useinterval-out-of-usecounter*/} -现在`useCounter` Hook做两件事。设置一个时间间隔,并且在每个时间间隔的tick内递增一次state变量。将设置时间间隔的逻辑拆分到到一个`useInterval`的独立Hook中。它应该输入两个参数:`onTick` 回调函数和`delay`。修改后`useCounter` 的实现应该如下所示: +现在 `useCounter` Hook 做两件事。设置一个时间间隔,并且在每个时间间隔的 tick 内递增一次 state 变量。将设置时间间隔的逻辑拆分到到一个 `useInterval` 的独立 Hook 中。它应该输入两个参数:`onTick` 回调函数和 `delay`。修改后 `useCounter` 的实现应该如下所示: ```js export function useCounter(delay) { @@ -2076,7 +2076,7 @@ export function useCounter(delay) { } ``` -在 `useInterval.js` 文件中编写`useInterval` 并在 `useCounter.js`文件中导入。 +在 `useInterval.js` 文件中编写 `useInterval` 并在 `useCounter.js` 文件中导入。 @@ -2106,14 +2106,14 @@ export function useCounter(delay) { ``` ```js useInterval.js -// 在这里编写你自己的Hook! +// 在这里编写你自己的 Hook! ``` -`useInterval`内部的逻辑应该是设置和清除计时器。不需要做除此之外的任何事。 +`useInterval` 内部的逻辑应该是设置和清除计时器。不需要做除此之外的任何事。 @@ -2158,11 +2158,11 @@ export function useInterval(onTick, delay) { #### 修复计时器重置 {/*fix-a-resetting-interval*/} -这个例子有**两个**独立的计时器。 +这个例子有 **两个** 独立的计时器。 -`App`组件调用`useCounter`,这个Hook调用`useInterval`来每秒更新一次计数器。但是`App`组件**也**调用`useInterval`来每两秒随机更新一次页面背景色。 +`App` 组件调用 `useCounter`,这个 Hook 调用 `useInterval` 来每秒更新一次计数器。但是 `App` 组件 **也** 调用 `useInterval` 来每两秒随机更新一次页面背景色。 -更新页面背景色的回调函数因为一些原因从未执行。在 `useInterval`内部添加一些log。 +更新页面背景色的回调函数因为一些原因从未执行。在 `useInterval` 内部添加一些 log。 ```js {2,5} useEffect(() => { @@ -2175,13 +2175,13 @@ export function useInterval(onTick, delay) { }, [onTick, delay]); ``` -这些log符合你的预期吗?如果你的一些Effect似乎不必要的重新同步了,你能猜中哪一个依赖导致这个情况发生吗?有其他方式从你的Effect中[移除依赖](/learn/removing-effect-dependencies)吗? +这些 log 符合你的预期吗?如果你的一些 Effect 似乎不必要的重新同步了,你能猜中哪一个依赖导致这个情况发生吗?有其他方式从你的 Effect 中 [移除依赖](/learn/removing-effect-dependencies) 吗? 你修复这个问题以后,应该希望页面背景每两秒更新一次。 -看上去你的`useInterval` Hook接受事件监听器作为参数。你能想到一些包裹事件监听器的方法,这样它不需要成为你的Effect的依赖项吗? +看上去你的 `useInterval` Hook 接受事件监听器作为参数。你能想到一些包裹事件监听器的方法,这样它不需要成为你的 Effect 的依赖项吗? @@ -2250,11 +2250,11 @@ export function useInterval(onTick, delay) { -和[早前这个页面](/learn/reusing-logic-with-custom-hooks#passing-event-handlers-to-custom-hooks)做的一样,在`useInterval`内部,把tick回调函数包裹进一个Effect Event。 +和 [早前这个页面](/learn/reusing-logic-with-custom-hooks#passing-event-handlers-to-custom-hooks) 做的一样,在 `useInterval` 内部,把 tick 回调函数包裹进一个 Effect Event。 -这将让你可以从Effect的依赖项中删掉`onTick`。每次组件重新渲染时,Effect将不会重新同步,所以页面背景颜色更新间隔不会在有机会触发之前每秒重置一次。 +这将让你可以从 Effect 的依赖项中删掉 `onTick`。每次组件重新渲染时,Effect 将不会重新同步,所以页面背景颜色更新间隔不会在有机会触发之前每秒重置一次。 -随着这个修改,两个interval都会像预期的一样工作并且不会互相干预: +随着这个修改,两个 interval 都会像预期的一样工作并且不会互相干预: @@ -2323,19 +2323,19 @@ export function useInterval(callback, delay) { #### 实现一个交错的运动 {/*implement-a-staggering-movement*/} -这个例子中,`usePointerPosition()`Hook追踪当前指针位置。尝试移动光标或你的手指到预览区域上方,看到有一个红点随着你移动。它的位置被保存在变量`pos1`中。 +这个例子中,`usePointerPosition()` Hook 追踪当前指针位置。尝试移动光标或你的手指到预览区域上方,看到有一个红点随着你移动。它的位置被保存在变量 `pos1` 中。 -事实上,有5(!)个不同的红点正在被渲染。你看不见是因为他们都出现在了同一位置。这就是你需要修复的问题。你想要实现的是一个“交错的”运动:每个圆点应该“跟随”它前一个点的路径。例如如果你快速移动光标,第一个点应该立刻跟着它,第二个应该在小小的延时后跟上第一个点,第三个点应该跟着第二个点等。 +事实上,有 5(!) 个不同的红点正在被渲染。你看不见是因为他们都出现在了同一位置。这就是你需要修复的问题。你想要实现的是一个“交错的”运动:每个圆点应该“跟随”它前一个点的路径。例如如果你快速移动光标,第一个点应该立刻跟着它,第二个应该在小小的延时后跟上第一个点,第三个点应该跟着第二个点等。 -你需要实现自定义Hook `useDelayedValue`。它当前的实现是返回提供给它的`value`。而你想从`delay`毫秒之前返回`value`。你可能需要一些state和一个Effect来完成这个任务。 +你需要实现自定义 Hook `useDelayedValue`。它当前的实现是返回提供给它的 `value`。而你想从 `delay` 毫秒之前返回 `value`。你可能需要一些 state 和一个 Effect 来完成这个任务。 -实现`useDelayedValue`后,你应该看见这些点一个接一个运动。 +实现 `useDelayedValue` 后,你应该看见这些点一个接一个运动。 -你需要在自定义Hook内部存储一个state变量`delayedValue`。当`value`变化时,你需要运行一个Effect。这个Effect应该在`delay`毫秒后更新`delayedValue`。你可能发现调用`setTimeout`很有帮助。 +你需要在自定义 Hook 内部存储一个 state 变量 `delayedValue`。当 `value` 变化时,你需要运行一个 Effect。这个 Effect 应该在 `delay` 毫秒后更新 `delayedValue`。你可能发现调用 `setTimeout` 很有帮助。 -这个Effect 需要清除吗?为什么? +这个 Effect 需要清除吗?为什么? @@ -2345,7 +2345,7 @@ export function useInterval(callback, delay) { import { usePointerPosition } from './usePointerPosition.js'; function useDelayedValue(value, delay) { - // TODO: 实现这个Hook + // TODO: 实现这个 Hook return value; } @@ -2408,7 +2408,7 @@ body { min-height: 300px; } -这里是一个生效的版本。你将`delayedValue`保存为一个state变量。当`value`更新的时候,你的Effect会安排一个timeout来更新`delayedValue`。这就是`delayedValue`总是滞后于真实的`value`的原因。 +这里是一个生效的版本。你将 `delayedValue` 保存为一个 state 变量。当 `value` 更新的时候,你的 Effect 会安排一个 timeout 来更新 `delayedValue`。这就是 `delayedValue` 总是滞后于真实的 `value` 的原因。 @@ -2485,7 +2485,7 @@ body { min-height: 300px; } -请注意这个Effect **不**需要清理。如果你在清理函数中调用了`clearTimeout`,那么每当`value`变化时,就会终止已经计划好的timeout。为了保持运动连续,你想要触发所有的timeout。 +请注意这个 Effect **不** 需要清理。如果你在清理函数中调用了 `clearTimeout`,那么每当 `value` 变化时,就会终止已经计划好的 timeout。为了保持运动连续,你想要触发所有的 timeout。 From ded4d61316f353d066eeaa2d33494fa9ac6c996e Mon Sep 17 00:00:00 2001 From: yanyue Date: Sat, 6 May 2023 19:22:14 +0800 Subject: [PATCH 08/14] docs(cn): correct the translation --- .../learn/reusing-logic-with-custom-hooks.md | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index 6a9c23c88e..a31e1f6dc1 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: 使用自定义Hook重用逻辑 +title: 使用自定义 Hook 复用逻辑 --- -React 带有一些内置的 Hook,比如 `useState`, `useContext` 和 `useEffect`。有时候你需要一个用途更加特定的 Hook:比如远程获取数据,追踪用户是否在线,或者连接一个聊天室。在 React 中可能找不到这些 Hook,但是你可以根据自己应用的需求取创建自己的 Hook。 +React 有一些内置 Hook,例如 `useState`, `useContext` 和 `useEffect`。有时你需要一个用途更特殊的 Hook:例如获取数据,记录用户是否在线或者连接聊天室。虽然 React 中可能找不到这些 Hook,但是你可以根据应用需求创建自己的 Hook。 -- 什么是自定义 Hook,以及如何写自己的 Hook +- 自定义 Hook 是什么,以及如何编写 - 如何在组件间重用逻辑 -- 如何命名和构建你的自定义 Hook +- 如何命名和构建自定义 Hook - 提取自定义 Hook 的时机和原因 -## 自定义 Hook:在组件间共享逻辑 {/*custom-hooks-sharing-logic-between-components*/} +## 自定义 Hook:组件间共享逻辑 {/*custom-hooks-sharing-logic-between-components*/} -假设你正在开发一款重度依赖网络的应用(和大多数应用一样)。当用户使用你的应用时,如果网络意外断开,你想要警告用户。你会如何处理这种情况呢?看上去你在组件中需要两个东西: +假设你正在开发一款重度依赖网络的应用(和大多数应用一样)。当用户使用应用时网络意外断开,你需要提醒他。你会怎么处理呢?看上去组件需要两个东西: 1. 一个追踪网络是否在线的 state。 -2. 一个订阅全局 [`在线`](https://developer.mozilla.org/en-US/docs/Web/API/Window/online_event) 和 [`离线`](https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event) 事件以及更新上述 state 的 Effect。 +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) 事件并更新上述 state 的 Effect。 -这将让你的组件与网络状态保持 [同步](/learn/synchronizing-with-effects)。你可以像这样开始: +这将让组件与网络状态保持 [同步](/learn/synchronizing-with-effects)。你也许可以像这样开始: @@ -54,11 +54,11 @@ export default function StatusBar() { -尝试开启和关闭网络,注意 `StatusBar` 应对你的行为是如何更新的。 +试着开启和关闭网络,注意 `StatusBar` 组件应对你的行为是如何更新的。 -现在假设你想要在一个不同的组件里 **也** 使用这段相同的逻辑。你想实现一个 Save 按钮,当网络离线时,这个按钮会变成不可用并且显示“Reconnecting...”而不是“Save”。 +假设现在你想在另一个不同的组件里 **也** 使用同样的逻辑。你希望实现一个 Save 按钮,每当网络断开这个按钮就会不可用并且显示“Reconnecting...”而不是“Save”。 -你可以通过复制和粘贴 `isOnline` state 和 Effect 到 `SaveButton` 开始: +你可以从复制粘贴 `isOnline` state 和 Effect 到 `SaveButton` 开始: @@ -96,13 +96,13 @@ export default function SaveButton() { -验证一下,如果关闭网络,按钮会变更展示。 +如果你关闭网络,可以验证出这个按钮将会变更外观。 -这两个组件都工作正常,但是不幸的是他们之间的逻辑重复了。即使两个组件看上去有不同的 **视觉界面**,你也想要重用他们之间的逻辑。 +这两个组件都能很好地工作,但不幸的是他们的逻辑重复了。他们看上去有不同的 **视觉外观**,但你依然想复用他们的逻辑。 -### 从组件中提取出你的自定义 Hook {/*extracting-your-own-custom-hook-from-a-component*/} +### 从组件中提取自定义 Hook {/*extracting-your-own-custom-hook-from-a-component*/} -想象一下,与 [`useState`](/reference/react/useState) 和 [`useEffect`](/reference/react/useEffect) 类似,有一个内置的 `useOnlineStatus` Hook。那么就可以简化这两个组件并且移除他们之间的重复部分: +假设有一个与 [`useState`](/reference/react/useState) 和 [`useEffect`](/reference/react/useEffect) 相似的内置 Hook `useOnlineStatus`。那么你就可以简化这两个组件并移除他们之间的重复部分: ```js {2,7} function StatusBar() { @@ -125,7 +125,7 @@ function SaveButton() { } ``` -尽管目前没有这样的内置 Hook,但是你可以自己写。声明一个 `useOnlineStatus` 函数,并且把早前组件里的所有重复代码移到里面: +尽管目前还没有这样的内置 Hook,但是你可以自己写。声明一个 `useOnlineStatus` 函数,并把组件里早前写的所有重复代码移入该函数: ```js {2-16} function useOnlineStatus() { @@ -148,7 +148,7 @@ function useOnlineStatus() { } ``` -在函数结尾处,返回 `isOnline`。这可以让组件读取到那个值: +在函数结尾处返回 `isOnline`。这可以让组件读取到该值: @@ -209,50 +209,50 @@ export function useOnlineStatus() { -验证切换网络状态是否更新了两个组件。 +切换网络状态验证一下是否会同时更新两个组件。 -现在你的组件里没有那么多重复的逻辑了。**更重要的是,组件中的代码描述了他们想要做什么(使用在线状态!),而不是如何做(通过订阅浏览器事件完成)** +现在组件里没有那么多的重复逻辑了。**更重要的是,组件内部的代码描述的是想要做什么(使用在线状态!),而不是怎么做(通过订阅浏览器事件完成)** -当提取逻辑到自定义 Hook 中 时,你可以隐藏如何处理一些外部系统或者浏览器 API 的艰难细节。组件中的代码表达的是你的目的而不是实现。 +当提取逻辑到自定义 Hook 时,你可以隐藏如何处理外部系统或者浏览器 API 这些乱七八糟的细节。组件内部的代码表达的是目标而不是具体实现。 ### Hook 的名称必须永远以 `use` 开头 {/*hook-names-always-start-with-use*/} -React 应用是由组件构建的。组件是由内置的或者自定义的 Hook 构建的。你可能经常使用别人创建的自定义 Hook,但是偶尔也可能要自己写! +React 应用是由组件构建的。而组件是由内置或自定义的 Hook 构建。可能你经常使用的是别人写的自定义 Hook,但偶尔可能也要自己写! 你必须遵循以下这些命名公约: -1. **React 组件名称必须以大写字母开头**, 比如 `StatusBar` 和 `SaveButton`。React 组件还需要返回一些 React 知道如何展示的内容,比如一段 JSX 代码。 -2. **Hook 的名称必须以 `use` 开头,后面跟一个大写字母**, 像 [`useState`](/reference/react/useState) (内置) 或者 `useOnlineStatus` (像文章早前的自定义 Hook)。Hook 可能会返回任意值。 +1. **React 组件名称必须以大写字母开头**, 比如 `StatusBar` 和 `SaveButton`。React 组件还需要返回一些 React 能够显示的内容,比如一段 JSX。 +2. **Hook 的名称必须以后跟一个大写字母的 `use` 开头**, 像 [`useState`](/reference/react/useState) (内置) 或者 `useOnlineStatus` (像本文早前的自定义 Hook)。Hook 可以返回任意值。 -这个公约保证了你始终可以查看组件并且知道它的 state,Effect 以及其他的 React 特性可能“隐藏”在哪里。例如如果你在组件内部看见 `getColor()` 的函数调用,你可以确定它内部不可能包含 React state,因为它的名称没有以 `use` 开头。但是像 `useOnlineStatus()` 这样的函数调用将极有可能包含对内部其他 Hook 的调用! +这个公约保证你始终可以一眼识别出组件并且知道它的 state,Effect 以及其他的 React 特性可能“隐藏”在哪里。例如如果你在组件内部看见 `getColor()` 函数调用,你可以确定它内部不可能包含 React state,因为它的名称没有以 `use` 开头。但是像 `useOnlineStatus()` 这样的函数调用就很可能包含对内部其他 Hook 的调用! -如果为 [React 配置了](/learn/editor-setup#linting) 检查工具,它会强制执行这个命名公约。滑动到上面的 sandbox,并将 `useOnlineStatus` 重命名为 `getOnlineStatus`。注意检查工具将不会再允许你在内部调用 `useState` 或者 `useEffect`。只有 Hook 和组件可以调用其他 Hook! +如果你为 [React 配置了](/learn/editor-setup#linting) 代码检查工具,它会强制执行这个命名公约。现在滑动到上面的 sandbox,并将 `useOnlineStatus` 重命名为 `getOnlineStatus`。注意此时代码检查工具将不会再允许你其内部调用 `useState` 或者 `useEffect`。只有 Hook 和组件可以调用其他 Hook! -#### 渲染期间调用的所有函数都应该以前缀 use 开头么? {/*should-all-functions-called-during-rendering-start-with-the-use-prefix*/} +#### 渲染期间调用的所有函数都应该以 use 前缀开头么? {/*should-all-functions-called-during-rendering-start-with-the-use-prefix*/} -不是。没有 **调用** Hook 的函数不需要 **成为** Hook。 +不。没有 **调用** Hook 的函数不需要 **变成** Hook。 -如果你的函数没有调用任何 Hook,请避免使用 `use` 前缀。 而是 **不带** `use` 前缀,把它作为常规函数去写。例如下面的 `useSorted` 没有调用 Hook,所以叫它 `getSorted`: +如果函数没有调用任何 Hook,请避免使用 `use` 前缀。 而是 **不带** `use` 前缀把它当成常规函数去写。例如下面的 `useSorted` 没有调用 Hook,所以叫它 `getSorted`: ```js -// 🔴 避免:没有调用其他Hook的Hook +// 🔴 Avoid: 没有调用其他Hook的Hook function useSorted(items) { return items.slice().sort(); } -// ✅ Good:没有使用Hook的常规函数 +// ✅ Good: 没有使用Hook的常规函数 function getSorted(items) { return items.slice().sort(); } ``` -这保证了你的代码可以在包括条件语句在内的任何地方调用这个常规函数: +这保证你的代码可以在包含条件语句在内的任何地方调用这个常规函数: ```js function List({ items, shouldSort }) { @@ -265,19 +265,19 @@ function List({ items, shouldSort }) { } ``` -如果内部至少使用了一个 Hook,你应该给这个函数 `use` 前缀(从而让它成为一个 Hook): +如果内部至少使用了一个 Hook,你就应该给这个函数加 `use` 前缀(让它成为一个 Hook): ```js -// ✅ Good:一个使用了其他Hook的Hook +// ✅ Good: 一个使用了其他Hook的Hook function useAuth() { return useContext(Auth); } ``` -从技术上讲,这不是 React 强制的。原则上你可以写一个不调用其他 Hook 的 Hook。这常常会令人迷惑且受到限制,所以最好是避免那种方式。但是在极少一些场景下,它可能是有帮助的。例如也许你的函数现在没有使用任何 Hook,但是计划未来会添加一些 Hook 调用。那么使用 `use` 前缀给它命名就很有意义: +技术上 React 对此并不强制要求。原则上你可以写出不调用其他 Hook 的 Hook。但这常常会难以理解且受限,所以最好避免这种方式。但是它在极少数场景下可能是有益的。例如函数目前也许并没有使用任何 Hook,但是你计划未来在该函数内部添加一些 Hook 调用。那么使用 `use` 前缀命名就很有意义: ```js {3-4} -// ✅ Good:之后即将可能使用一些其他Hook的一个Hook +// ✅ Good: 之后可能使用其他Hook的Hook function useAuth() { // TODO: 当认证功能实现以后,替换这一行: // 返回 useContext(Auth); @@ -285,13 +285,13 @@ function useAuth() { } ``` -接下来组件就不能在条件分支里调用这个函数。当你在里面添加了 Hook 调用时,这一点将变得很重要。如果你没有计划在内部使用 Hook(现在或者之后),就不要让它成为一个 Hook。 +接下来组件就不能在条件语句里调用这个函数。当你在内部实际添加了 Hook 调用时,这一点将变得很重要。如果你没有计划在内部使用 Hook(现在或者之后),请不要让它变成 Hook。 -### 自定义 Hook 共享的是状态逻辑,而不是 state 本身 {/*custom-hooks-let-you-share-stateful-logic-not-state-itself*/} +### 自定义 Hook 共享有状态逻辑,而不是 state 本身 {/*custom-hooks-let-you-share-stateful-logic-not-state-itself*/} -之前的例子里,当你开启或关闭网络时,两个组件一起更新了。但是两个组件共享一个 `isOnline` state 变量的想法是错误的。看这段代码: +之前的例子里,当你开启或关闭网络时,两个组件一起更新了。但是两个组件共享 state 变量 `isOnline` 这种想法是错的。看这段代码: ```js {2,7} function StatusBar() { @@ -305,7 +305,7 @@ function SaveButton() { } ``` -它的工作方式和你提取重复部分之前一样: +它的工作方式和你之前提取的重复代码一模一样: ```js {2-5,10-13} function StatusBar() { @@ -325,9 +325,9 @@ function SaveButton() { } ``` -这是两个完全独立的 state 变量和 Effect!他们只是碰巧同时有同样的值,因为你将两个组件与相同的外部值同步(无论网络是否开启)。 +这是完全独立的两个 state 变量和 Effect!只是碰巧同一时间值一样,因为你使用了相同的外部值同步两个组件(无论网络是否开启)。 -为了更好的说明这一点,我们需要一个不同的例子。看下面的 `Form` 组件: +为了更好的说明这一点,我们需要一个不同的示例。看下面的 `Form` 组件: @@ -369,13 +369,13 @@ input { margin-left: 10px; } -每个表单域都有一些重复的逻辑: +每个表单域都有一部分重复的逻辑: -1. 都有 state(`firstName` 和 `lastName`)。 -1. 都有 change handler(`handleFirstNameChange` 和 `handleLastNameChange`)。 -1. 都有为输入框指定 `value` 和 `onChange` 属性的 JSX 片段。 +1. 都有一个 state(`firstName` 和 `lastName`)。 +1. 都有 change 事件的处理函数(`handleFirstNameChange` 和 `handleLastNameChange`)。 +1. 都有为输入框指定 `value` 和 `onChange` 属性的 JSX。 -你可以把重复的逻辑提取到自定义 Hook `useFormInput`: +你可以提取重复的逻辑到自定义 Hook `useFormInput`: @@ -428,9 +428,9 @@ input { margin-left: 10px; } -注意它只声明了 **一个** 叫做 `value` 的 state 变量。 +注意它只声明了 **一个** state 变量,叫做 `value`。 -但是 `Form` 组件调用了 **两次** `useFormInput`: +但 `Form` 组件调用了 **两次** `useFormInput`: ```js function Form() { @@ -439,18 +439,18 @@ function Form() { // ... ``` -这就是为什么它工作的时候像声明了两个独立的 state 变量! +这就是为什么它工作的时候像声明了两个单独的 state 变量! -**自定义 Hook 只是共享状态逻辑而不是 state 本身。每个 Hook 的调用都完全独立于对同一个 Hook 的其他调用**。 这就是为什么上面两个 sandbox 完全相同的原因。如果你愿意,可以滚动回去并比较他们。提取自定义 Hook 之前和之后的行为是一致的。 +**自定义 Hook 只是共享有状态逻辑而不是 state 本身。对 Hook 的每个调用完全独立于对同一个 Hook 的其他调用**。 这就是上面两个 sandbox 结果完全相同的原因。如果愿意,你可以划上去进行比较。提取自定义 Hook 前后组件的行为是一致的。 -而当你需要在多个组件之间共享 state 本身时,需要[将变量提升并传递下去](/learn/sharing-state-between-components)。 +当你需要在多个组件之间共享 state 本身时,需要[将变量提升并传递下去](/learn/sharing-state-between-components)。 ## 在 Hook 之间传递响应值 {/*passing-reactive-values-between-hooks*/} -组件每次重新渲染,自定义 Hook 中的代码也会重新运行。这就是为什么组件和自定义 Hook 都 [需要纯粹](/learn/keeping-components-pure) 的原因。我们应该把自定义 Hook 的代码作为组件主体的一部分。 - -自定义组件总会接收到最新的 props 和 state,因为它会和你的组件一起重新渲染。想知道这意味着什么,看一下这个聊天室的例子。变更 Server URL 或者聊天室 ID: +每当组件重新渲染,自定义 Hook 中的代码就会重新运行。这就是组件和自定义 Hook 都 [需要纯粹](/learn/keeping-components-pure) 的原因。我们应该把自定义 Hook 的代码看作组件主体的一部分。 +由于自定义 Hook 会随着组件一起重新渲染,所以组件可以一直接收到最新的 props 和 state。想知道这意味着什么吗,那就看这个聊天室的示例。修改 Server URL 或者聊天室 ID: + ```js App.js From a9382b57f51a99aaf3e4e9b1f8d4af5753017c3e Mon Sep 17 00:00:00 2001 From: yueyueyan Date: Sun, 7 May 2023 15:10:47 +0800 Subject: [PATCH 09/14] docs(cn) correct all translation of eusing-logic-with-custom-hooks.md --- .../learn/reusing-logic-with-custom-hooks.md | 166 +++++++++--------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index a31e1f6dc1..e9a1c38385 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -449,8 +449,8 @@ function Form() { 每当组件重新渲染,自定义 Hook 中的代码就会重新运行。这就是组件和自定义 Hook 都 [需要纯粹](/learn/keeping-components-pure) 的原因。我们应该把自定义 Hook 的代码看作组件主体的一部分。 -由于自定义 Hook 会随着组件一起重新渲染,所以组件可以一直接收到最新的 props 和 state。想知道这意味着什么吗,那就看这个聊天室的示例。修改 Server URL 或者聊天室 ID: - +由于自定义 Hook 会随着组件一起重新渲染,所以组件可以永远接收到最新的 props 和 state。想知道这意味着什么,那就看看这个聊天室的示例。修改 Server URL 或者聊天室 ID: + ```js App.js @@ -599,9 +599,9 @@ button { margin-left: 10px; } -当你修改 `serverUrl` 或者 `roomId` 时,Effect 会对 [你的修改做出“反应”](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) 和重新同步。你可以通过修改 Effect 依赖引起的聊天室重连的每次 console 信息来区分。 +当你修改 `serverUrl` 或者 `roomId` 时,Effect 会对 [你的修改做出“响应”](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) 并重新同步。你可以通过每次修改 Effect 依赖项时聊天室重连的控制台消息来区分。 -现在把 Effect 代码移动到自定义 Hook 中: +现在将 Effect 代码移入自定义 Hook: ```js {2-13} export function useChatRoom({ serverUrl, roomId }) { @@ -620,7 +620,7 @@ export function useChatRoom({ serverUrl, roomId }) { } ``` -这让你的 `ChatRoom` 组件调用自定义 Hook,不需要担心内部是如何工作: +这让 `ChatRoom` 组件调用自定义 Hook,而不需要担心内部怎么工作: ```js {4-7} export default function ChatRoom({ roomId }) { @@ -643,9 +643,9 @@ export default function ChatRoom({ roomId }) { } ``` -这看上去简洁多了!(但是它做的是同一件事情。) +这看上去简洁多了!(但是它做的是同一件事。) -注意逻辑 **仍然响应** prop 和 state 修改。尝试编辑 server URL 或者选中的房间: +注意逻辑 **仍然响应** prop 和 state 的变化。尝试编辑 server URL 或选中的房间: @@ -807,7 +807,7 @@ button { margin-left: 10px; } -注意你是如何获取到 Hook 的返回值: +注意你如何获取 Hook 的返回值: ```js {2} export default function ChatRoom({ roomId }) { @@ -820,7 +820,7 @@ export default function ChatRoom({ roomId }) { // ... ``` -并且把它作为输入传递给另外一个 Hook: +并把它作为输入传给另一个 Hook: ```js {6} export default function ChatRoom({ roomId }) { @@ -833,17 +833,17 @@ export default function ChatRoom({ roomId }) { // ... ``` -每次 `ChatRoom` 组件重新渲染,它都会传最新的 `roomId` 和 `serverUrl` 到你的 Hook 中。这就是为什么每当他们的值在重新渲染后不同的时候你的 Effect 会重连聊天室。(如果你曾经使用过音视频处理软件,像这样的链式 Hook 也许会让你想起链式可视化或音频 effect。就好像 `useState` 的输出作为 `useChatRoom` 的输入)。 +每次 `ChatRoom` 组件重新渲染,它就会传最新的 `roomId` 和 `serverUrl` 到你的 Hook。这就是每当重新渲染后他们的值不一样时你的 Effect 会重连聊天室的原因。(如果你曾经使用过音视频处理软件,像这样的 Hook 链也许会让你想起音视频效果链。好似 `useState` 的输出作为 `useChatRoom` 的输入)。 -### 把事件处理器传到自定义 Hook 中 {/*passing-event-handlers-to-custom-hooks*/} +### 把事件处理函数传到自定义 Hook 中 {/*passing-event-handlers-to-custom-hooks*/} -这个章节描述 React 稳定版中 **还没有发布的实验性 API**。 +这个章节描述了 React 稳定版 **还未发布的一个实验性 API**。 -当你在更多组件中使用 `useChatRoom` 组件时,你也许想要让组件自定义它的行为。例如现在 Hook 中收到信息时做什么的逻辑是硬编码: +当你在更多组件中使用 `useChatRoom` 时,你可能希望组件能定制它的行为。例如现在 Hook 内部收到消息的处理逻辑是硬编码: ```js {9-11} export function useChatRoom({ serverUrl, roomId }) { @@ -862,7 +862,7 @@ export function useChatRoom({ serverUrl, roomId }) { } ``` -假设你想要把这个逻辑移回到组件中: +假设你想把这个逻辑移回到组件中: ```js {7-9} export default function ChatRoom({ roomId }) { @@ -878,7 +878,7 @@ export default function ChatRoom({ roomId }) { // ... ``` -完成这个工作需要修改自定义 Hook,把 `onReceiveMessage` 作为命名选项之一: +完成这个工作需要修改自定义 Hook,把 `onReceiveMessage` 作为其命名选项之一: ```js {1,10,13} export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { @@ -897,9 +897,9 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { } ``` -这将会生效,但是当自定义 Hook 接受事件处理器的时候,你还可以做另一个改进。 +这个修改有效果,但是当自定义 Hook 接受事件处理函数时,你还可以进一步改进。 -增加一个 `onReceiveMessage` 依赖并不理想,每次只要组件重新渲染,聊天就会重新连接。 [将这个事件处理器包装到 Effect Event 从而将它从依赖中移除](/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 }) { } ``` -现在聊天室不会在每次 `ChatRoom` 组件重新渲染时都重新连接。这是一个传递事件处理器给自定义 Hook 的例子,你可以尝试一下: +现在每次 `ChatRoom` 组件重新渲染时聊天室都不会重连。这是一个将事件处理函数传给自定义 Hook 的完整且有效的 demo,你可以尝试一下: @@ -1095,16 +1095,16 @@ button { margin-left: 10px; } ## 什么时候使用自定义 Hook {/*when-to-use-custom-hooks*/} -你不需要把每段重复的代码提取为自定义 Hook。一些重复是可以的。例如像早前用来包裹单个 `useState` 调用的 `useFormInput` Hook 可能就是没有必要的。 +你没必要对每段重复的代码都提取自定义 Hook。一些重复是好的。例如像早前提取的包裹单个 `useState` 调用的 `useFormInput` Hook 就是没有必要的。 -但是每当你写 Effect 的时候,请考虑一下把它包裹在自定义 Hook 会不会更清晰。[你不应该经常使用 Effect](/learn/you-might-not-need-an-effect),所以如果你正在写 Effect 就意味着你需要“走出 React”来和一些外部系统同步,或者需要做一些 React 中没有内置 API 的事。把重复代码包装进自定义 Hook 可以让你准确表达你的意图和数据在里面是如何流动的。 +但是每当你写 Effect 时,考虑一下把它包裹在自定义 Hook 是否更清晰。[你不应该经常使用 Effect](/learn/you-might-not-need-an-effect),所以如果你正在写 Effect 就意味着你需要“走出 React”和某些外部系统同步,或者需要做一些 React 中没有对应内置 API 的事。把 Effect 包裹进自定义 Hook 可以准确表达你的目标以及数据在里面是如何流动的。 -例如假设 `ShippingForm` 组件展示两个下拉框:一个展示城市列表,另一个展示选中的城市的区域列表。你可能会像这样开始写代码: +例如,假设 `ShippingForm` 组件展示两个下拉菜单:一个显示城市列表,另一个显示选中城市的区域列表。你可能一开始会像这样写代码: ```js {3-16,20-35} function ShippingForm({ country }) { const [cities, setCities] = useState(null); - // 这个Effect拉取一个国家的城市数据 + // 这个 Effect 拉取一个国家的城市数据 useEffect(() => { let ignore = false; fetch(`/api/cities?country=${country}`) @@ -1121,7 +1121,7 @@ function ShippingForm({ country }) { const [city, setCity] = useState(null); const [areas, setAreas] = useState(null); - // 这个Effect拉取选中城市的区域列表 + // 这个 Effect 拉取选中城市的区域列表 useEffect(() => { if (city) { let ignore = false; @@ -1141,7 +1141,7 @@ function ShippingForm({ country }) { // ... ``` -尽管这部分代码是重复的,但是 [把这些 Effect 各自分开是正确的](/learn/removing-effect-dependencies#is-your-effect-doing-several-unrelated-things)。他们同步两个不同的事情,所以你不应该把他们合并到同一个 Effect。取而代之的是,你可以提取他们的通用逻辑到你自己的 `useData` Hook 中来简化上面的 `ShippingForm` 组件: +尽管这部分代码是重复的,但是 [把这些 Effect 各自分开是正确的](/learn/removing-effect-dependencies#is-your-effect-doing-several-unrelated-things)。他们同步两件不同的事情,所以不应该把他们合并到同一个 Effect。而是提取其中的通用逻辑到你自己的 `useData` Hook 来简化上面的 `ShippingForm` 组件: ```js {2-18} function useData(url) { @@ -1165,7 +1165,7 @@ function useData(url) { } ``` -现在你可以调用 `useData` 代替 `ShippingForm` 组件中的 Effect: +现在你可以在 `ShippingForm` 组件中调用 `useData` 替换两个 Effect: ```js {2,4} function ShippingForm({ country }) { @@ -1175,21 +1175,21 @@ function ShippingForm({ country }) { // ... ``` -提取自定义 Hook 可以让数据流清晰。你可以输入 `url`,输出 `data`。通过把你的 Effect “隐藏”在 `useData` 中,也可以防止一些正在处理 `ShippingForm` 组件的人向里面添加 [不必要的依赖](/learn/removing-effect-dependencies)。随着时间的推移,你 app 中的大部分 Effect 都会存在于自定义 Hook 中。 +提取自定义 Hook 让数据流清晰。输入 `url`,就会输出 `data`。通过把 Effect “隐藏”在 `useData` 内部,你也可以防止一些正在处理 `ShippingForm` 组件的人向里面添加 [不必要的依赖](/learn/removing-effect-dependencies)。随着时间的推移,应用中大部分 Effect 都会存在于自定义 Hook 内部。 #### 让你的自定义 Hook 专注于具体的高级用例 {/*keep-your-custom-hooks-focused-on-concrete-high-level-use-cases*/} -从选择你的自定义 Hook 名称开始。如果你难以选择一个清晰的名称,这意味着你的 Effect 和组件逻辑的剩余部分耦合度太高,还没有准备好被提取出来。 +从选择自定义 Hook 名称开始。如果你难以选择一个清晰的名称,这可能意味着你的 Effect 和组件逻辑剩余的部分耦合度太高,还没有做好被提取的准备。 -理想情况下,你的自定义 Hook 名称应该清晰到即使一个不经常写代码的人也能很好地猜中你的自定义 Hook 的功能,输入和返回: +理想情况下,你的自定义 Hook 名称应该清晰到即使一个不经常写代码的人也能很好地猜中自定义 Hook 的功能,输入和返回: * ✅ `useData(url)` * ✅ `useImpressionLog(eventName, extraData)` * ✅ `useChatRoom(options)` -当你和外部系统同步的时候,你的自定义 Hook 名称可能会更加专业,并使用该系统特定的术语。只要这个名称对于熟悉这个系统的人来说清晰,那就是好的: +当你和外部系统同步的时候,你的自定义 Hook 名称可能会更加专业,并使用该系统特定的术语。只要对熟悉这个系统的人来说名称清晰就可以: * ✅ `useMediaQuery(query)` * ✅ `useSocket(url)` @@ -1201,13 +1201,13 @@ function ShippingForm({ country }) { * 🔴 `useEffectOnce(fn)` * 🔴 `useUpdateEffect(fn)` -例如这个 `useMount` Hook 试图保证一些代码只在“加载”的时候运行: +例如这个 `useMount` Hook 试图保证一些代码只在“加载”时运行: ```js {4-5,14-15} function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - // 🔴 避免:使用自定义 "lifecycle" Hook + // 🔴 Avoid: 使用自定义“生命周期” Hook useMount(() => { const connection = createConnection({ roomId, serverUrl }); connection.connect(); @@ -1217,7 +1217,7 @@ function ChatRoom({ roomId }) { // ... } -// 🔴 避免:创建自定义 "lifecycle" Hook +// 🔴 Avoid: 创建自定义“生命周期” Hook function useMount(fn) { useEffect(() => { fn(); @@ -1225,7 +1225,7 @@ function useMount(fn) { } ``` -**像 `useMount` 这样的自定义“生命周期” Hook 不能很好的适应 React 模式**。例如示例代码有一个错误(它没有“响应” `roomId` 或 `serverUrl` 的变化),但是代码检查工具并不会向你发出对应的告警,因为代码检查工具只能检测直接的 `useEffect` 调用。它并不了解你的 Hook。 +**像 `useMount` 这样的自定义“生命周期” Hook 不是很适合 React 范式**。例如示例代码有一个错误(它没有对 `roomId` 或 `serverUrl` 的变化做出“响应” ),但是代码检查工具并不会向你发出对应的警告,因为它只能检测 `useEffect` 的直接调用。并不了解你的 Hook。 如果你正在编写 Effect,请从直接使用 React API 开始: @@ -1249,7 +1249,7 @@ function ChatRoom({ roomId }) { } ``` -然后你可以(但不是必须)为不同的高级用例提取自定义 Hook: +然后你可以(但不是必须的)为不同的高级用例提取自定义 Hook: ```js function ChatRoom({ roomId }) { @@ -1262,15 +1262,15 @@ function ChatRoom({ roomId }) { } ``` -**一个好的自定义 Hook 通过限制功能使代码调用更具声明性**。例如 `useChatRoom(options)` 只能连接聊天室,而 `useImpressionLog(eventName, extraData)` 只能向分析系统发送 impression 日志。如果你的自定义 Hook API 没有约束用例且非常抽象,那么在长期的运行中,比起它解决的问题,可能会引入更多问题。 +**好的自定义 Hook 通过限制功能使代码调用更具声明性**。例如 `useChatRoom(options)` 只能连接聊天室,而 `useImpressionLog(eventName, extraData)` 只能向分析系统发送展示日志。如果你的自定义 Hook API 没有约束用例且非常抽象,那么在长期的运行中,它引入的问题可能比解决的问题更多。 ### 自定义 Hook 帮助你迁移到更好的模式 {/*custom-hooks-help-you-migrate-to-better-patterns*/} -Effect 是一个 [“应急出口”](/learn/escape-hatches):当你需要“走出 React”且对于你的用例没有更好的内置解决方案的时候你可以使用他们。随着时间的推移,React 团队的目标是通过给更多特定问题提供特定解决方案来最小化应用中的 Effect 数量。把你的 Effect 包裹进自定义 Hook 会使得这些解决方案可用的时候升级代码更加容易。 +Effect 是一个 [“逃脱方案”](/learn/escape-hatches):当需要“走出 React”且用例没有更好的内置解决方案时你可以使用他们。随着时间的推移,React 团队的目标是通过给更具体的问题提供更具体的解决方案来最小化应用中的 Effect 数量。把你的 Effect 包裹进自定义 Hook,当这些解决方案可用时升级代码会更加容易。 -让我们回到这个例子: +让我们回到这个示例: @@ -1331,9 +1331,9 @@ export function useOnlineStatus() { -在上面的例子中,`useOnlineStatus` 借助 [`useState`](/reference/react/useState) 和 [`useEffect`](/reference/react/useEffect) 实现。但这不是最好的解决方案。它有许多没有考虑到的边界用例。例如假设当组件加载的时候,`isOnline` 已经是 `true`,但是如果网络已经离线的话这就是错误的。你可以使用浏览器的 [`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine) API 来检查,但是直接在生成初始 HTML 的服务器上使用它是不生效的。简而言之这段代码可以改进。 +在上述示例中,`useOnlineStatus` 借助一组 [`useState`](/reference/react/useState) 和 [`useEffect`](/reference/react/useEffect) 实现。但这不是最好的解决方案。它有许多边界用例没有考虑到。例如假设当组件加载时,`isOnline` 已经为 `true`,但是如果网络已经离线的话这就是错误的。你可以使用浏览器的 [`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine) API 来检查,但是在生成初始 HTML 的服务端直接使用它是没用的。简而言之这段代码可以改进。 -幸运的是,React 18 包含了一个叫做 [`useSyncExternalStore`](/reference/react/useSyncExternalStore) 的专用 API,它可以解决所有这些问题。这里是如何利用这个新 API 来重写你的 `useOnlineStatus` Hook: +幸运的是,React 18 包含了一个叫做 [`useSyncExternalStore`](/reference/react/useSyncExternalStore) 的专用 API,它可以解决你所有这些问题。这里展示了如何利用这个新 API 来重写你的 `useOnlineStatus` Hook: @@ -1393,7 +1393,7 @@ export function useOnlineStatus() { -注意 **不需要修改任何组件** 如何来完成这次迁移: +注意 **你不需要修改任何组件** 就能完成这次迁移: ```js {2,7} function StatusBar() { @@ -1407,19 +1407,19 @@ function SaveButton() { } ``` -这是为什么把 Effect 包裹进自定义 Hook 是有益的另一个原因: +这是把 Effect 包裹进自定义 Hook 有益的另一个原因: 1. 你让进出 Effect 的数据流非常清晰。 -2. 你让组件专注于目标,而不是 Effect 的实现。 -3. 当 React 增加新特性时,你可以在不修改你的任何组件的情况下移除这些 Effect。 +2. 你让组件专注于目标,而不是 Effect 的准确实现。 +3. 当 React 增加新特性时,你可以在不修改任何组件的情况下移除这些 Effect。 -和 [设计系统](https://uxdesign.cc/everything-you-need-to-know-about-design-systems-54b109851969) 类似,你可能会发现从应用的组件中提取通用逻辑到自定义 Hook 是非常有帮助的。这会让你的组件代码专注于目标,避免经常写原始 Effect。许多很棒的自定义 Hook 是由 React 社区维护的。 +和 [设计系统](https://uxdesign.cc/everything-you-need-to-know-about-design-systems-54b109851969) 相似,你可能会发现从应用的组件中提取通用逻辑到自定义 Hook 是非常有帮助的。这会让你的组件代码专注于目标,并且避免经常写原始 Effect。许多很棒的自定义 Hook 是由 React 社区维护的。 -#### React 会为远程数据获取提供内置的解决方案么? {/*will-react-provide-any-built-in-solution-for-data-fetching*/} +#### React 会为数据获取提供内置解决方案么? {/*will-react-provide-any-built-in-solution-for-data-fetching*/} -我们仍然在规划细节,但是期望未来可以像这样写远程数据获取: +我们仍然在规划细节,但是期望未来可以像这样写数据获取: ```js {1,4,6} import { use } from 'react'; // 还不可用! @@ -1431,13 +1431,13 @@ function ShippingForm({ country }) { // ... ``` -比起在每个组件手动写原始 Effect,如果你在应用中使用像上面的 `useData` 这样的自定义 Hook,迁移到最终推荐方式所需要的更改更少。但是旧的方式仍然可以有效工作,所以如果你喜欢写原始 Effect,你可以继续这样做。 +比起在每个组件手动写原始 Effect,在应用中使用像上面 `useData` 这样的自定义 Hook,之后迁移到最终推荐方式你所需要的修改更少。但是旧的方式仍然可以有效工作,所以如果你喜欢写原始 Effect,可以继续这样做。 -### 不止一个方法达到这个目的 {/*there-is-more-than-one-way-to-do-it*/} +### 不止一个方法可以做到 {/*there-is-more-than-one-way-to-do-it*/} -假设你想要使用浏览器的 [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) API 实现一个 **从头开始的** 渐入动画。你可能会从一个建立动画循环的 Effect 开始。在动画的每一帧中,你可以修改 [ref 持有的](/learn/manipulating-the-dom-with-refs) DOM 节点的 opacity 属性直到它为 `1`。你的代码可能这样开始: +假设你想要使用浏览器的 [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) API 实现一个 **从头开始的** fade-in 动画。你也许会从一个设置动画循环的 Effect 开始。在动画的每一帧中,你可以修改 [ref 持有的](/learn/manipulating-the-dom-with-refs) DOM 节点的 opacity 属性直到 `1`。你的代码一开始可能是这样: @@ -1611,7 +1611,7 @@ html, body { min-height: 300px; } -你可以让 `useFadeIn` 和原来保持一致,但是也可以更进一步重构。例如你可以把创建动画循环的逻辑从 `useFadeIn` 提取到自定义 Hook `useAnimationLoop`: +你可以让 `useFadeIn` 和原来保持一致,但是也可以进一步重构。例如你可以把设置动画循环的逻辑从 `useFadeIn` 提取到自定义 Hook `useAnimationLoop`: @@ -1715,7 +1715,7 @@ html, body { min-height: 300px; } -但是你 **没有必要** 这样做。和常规函数一样,最终是由你决定在哪里绘制代码不同部分之间的边界。你也可以采取不一样的方法。把大部分必要的逻辑移入一个 [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes),而不是把逻辑保留在 Effect 中: +但是 **没有必要** 这样做。和常规函数一样,最终是由你决定在哪里绘制代码不同部分之间的边界。你也可以采取不一样的方法。把大部分必要的逻辑移入一个 [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes),而不是把逻辑保留在 Effect 中: @@ -1813,9 +1813,9 @@ html, body { min-height: 300px; } -Effect 可以将 React 和外部系统连接起来。Effect 之间需要的协调越多(例如链接多个动画),像上面的 sandbox 一样 **完整地** 从 Effect 和 Hook 中提取逻辑就越有意义。然后你提取的代码 **变成** “外部系统”。这会让你的 Effect 保持简单化,因为他们只需要向已经移动到 React 外部的系统发送消息。 +Effect 可以连接 React 和外部系统。Effect 之间的配合越多(例如链接多个动画),像上面的 sandbox 一样 **完整地** 从 Effect 和 Hook 中提取逻辑就越有意义。然后你提取的代码 **变成** “外部系统”。这会让你的 Effect 保持简洁,因为他们只需要向已经被你移动到 React 外部的系统发送消息。 -上面这个例子假设需要使用 JavaScript 写 fade-in 逻辑。但是使用纯 [CSS 动画](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations) 实现这个特定的 fade-in 会更加简单和高效: +上面这个示例假设需要使用 JavaScript 写 fade-in 逻辑。但使用纯 [CSS 动画](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations) 实现这个特定的 fade-in 动画会更加简单和高效: @@ -1875,22 +1875,22 @@ html, body { min-height: 300px; } - 自定义 Hook 让你可以在组件间共享逻辑。 -- 自定义 Hook 命名必须以后面跟着一个大写字母的 `use` 开头。 -- 自定义 Hook 共享的只是有状态的逻辑,而不是 state 本身。 -- 你可以将响应值从一个 Hook 传到另一个,并且他们保持最新。 -- 每次你的组件重新渲染时,所有的 Hook 会重新运行。 +- 自定义 Hook 命名必须以后跟一个大写字母的 `use` 开头。 +- 自定义 Hook 共享的只是有状态逻辑,不是 state 本身。 +- 你可以将响应值从一个 Hook 传到另一个,并且他们会保持最新。 +- 每次组件重新渲染时,所有的 Hook 会重新运行。 - 自定义 Hook 的代码应该和组件代码一样保持纯粹。 -- 把自定义 Hook 收到的事件处理器封装到 Effect Event。 +- 把自定义 Hook 收到的事件处理函数包裹到 Effect Event。 - 不要创建像 `useMount` 这样的自定义 Hook。保持目标具体化。 -- 如何以及在哪里选择代码边界取决于你自己。 +- 如何以及在哪里选择代码边界取决于你。 -#### 提取一个 `useCounter` Hook {/*extract-a-usecounter-hook*/} +#### 提取 `useCounter` Hook {/*extract-a-usecounter-hook*/} -这个组件使用了一个 state 变量和一个 Effect 来展示每秒递增的一个数字。把逻辑提取到一个 `useCounter` 的自定义 Hook 中。你的目标是让 `Counter` 组件的实现看上去和这个一样: +这个组件使用了一个 state 变量和一个 Effect 来展示每秒递增的一个数字。把这个逻辑提取到一个 `useCounter` 的自定义 Hook 中。你的目标是让 `Counter` 组件的实现看上去和这个一样: ```js export default function Counter() { @@ -1960,9 +1960,9 @@ export function useCounter() { -#### 让计时器的 delay 变为可配置 {/*make-the-counter-delay-configurable*/} +#### 让计时器的 delay 变为可配置项 {/*make-the-counter-delay-configurable*/} -这个例子中有一个由滑动条控制的 state 变量 `delay`,但是它的值没有被用到。请将 `delay` 值传给你的自定义 Hook `useCounter`,修改 `useCounter` Hook,用传过去的 `delay` 代替硬编码 `1000` 毫秒。 +这个示例中有一个由滑动条控制的 state 变量 `delay`,但它的值没有被使用。请将 `delay` 值传给自定义 Hook `useCounter`,修改 `useCounter` Hook,用传过去的 `delay` 代替硬编码 `1000` 毫秒。 @@ -2012,7 +2012,7 @@ export function useCounter() { -使用 `useCounter(delay)` 将 `delay` 传入你的 Hook。然后在 Hook 内部使用 `delay` 而不是硬编码 `1000`。你需要向你的 Effect 依赖项中加入 `delay`。这保证了 `delay` 的变化会重置间隔时间。 +使用 `useCounter(delay)` 将 `delay` 传入 Hook。然后在 Hook 内部使用 `delay` 替换硬编码值 `1000`。你需要在 Effect 依赖项中加入 `delay`。这保证了 `delay` 的变化会重置 interval。 @@ -2064,7 +2064,7 @@ export function useCounter(delay) { #### 从 `useCounter` 中提取 `useInterval` {/*extract-useinterval-out-of-usecounter*/} -现在 `useCounter` Hook 做两件事。设置一个时间间隔,并且在每个时间间隔的 tick 内递增一次 state 变量。将设置时间间隔的逻辑拆分到到一个 `useInterval` 的独立 Hook 中。它应该输入两个参数:`onTick` 回调函数和 `delay`。修改后 `useCounter` 的实现应该如下所示: +现在 `useCounter` Hook 做两件事。设置一个 interval,并且在每个interval tick 内递增一次 state 变量。将设置 interval 的逻辑拆分到一个独立 Hook `useInterval`。它应该有两个参数:`onTick` 回调函数和 `delay`。本次修改后 `useCounter` 的实现应该如下所示: ```js export function useCounter(delay) { @@ -2113,7 +2113,7 @@ export function useCounter(delay) { -`useInterval` 内部的逻辑应该是设置和清除计时器。不需要做除此之外的任何事。 +`useInterval` 内部的逻辑应该是设置和清理计时器。除此之外不需要做任何事。 @@ -2152,17 +2152,17 @@ export function useInterval(onTick, delay) { -请注意这个解决方案有一些你将会下一个挑战中解决的问题。 +注意这个解决方案有一些问题,你将在下一个挑战中解决他们。 #### 修复计时器重置 {/*fix-a-resetting-interval*/} -这个例子有 **两个** 独立的计时器。 +这个示例有 **两个** 独立的计时器。 -`App` 组件调用 `useCounter`,这个 Hook 调用 `useInterval` 来每秒更新一次计数器。但是 `App` 组件 **也** 调用 `useInterval` 来每两秒随机更新一次页面背景色。 +`App` 组件调用 `useCounter`,这个 Hook 调用 `useInterval` 来每秒更新一次计数器。但是 `App` 组件 **也** 调用 `useInterval` 每两秒随机更新一次页面背景色。 -更新页面背景色的回调函数因为一些原因从未执行。在 `useInterval` 内部添加一些 log。 +更新页面背景色的回调函数因为一些原因从未执行过。在 `useInterval` 内部添加一些日志。 ```js {2,5} useEffect(() => { @@ -2175,13 +2175,13 @@ export function useInterval(onTick, delay) { }, [onTick, delay]); ``` -这些 log 符合你的预期吗?如果你的一些 Effect 似乎不必要的重新同步了,你能猜中哪一个依赖导致这个情况发生吗?有其他方式从你的 Effect 中 [移除依赖](/learn/removing-effect-dependencies) 吗? +这些日志符合你的预期吗?如果一些不必要的 Effect 似乎重新同步了,你能猜出哪一个依赖项导致了这个情况吗?有其他方式从 Effect 中 [移除依赖](/learn/removing-effect-dependencies) 吗? -你修复这个问题以后,应该希望页面背景每两秒更新一次。 +这个问题修复以后,你预期的应该是页面背景每两秒更新一次。 -看上去你的 `useInterval` Hook 接受事件监听器作为参数。你能想到一些包裹事件监听器的方法,这样它不需要成为你的 Effect 的依赖项吗? +看上去你的 `useInterval` Hook 接受事件监听器作为参数。你能想到一些包裹事件监听器的方法,让它不需要成为 Effect 的依赖项吗? @@ -2250,11 +2250,11 @@ export function useInterval(onTick, delay) { -和 [早前这个页面](/learn/reusing-logic-with-custom-hooks#passing-event-handlers-to-custom-hooks) 做的一样,在 `useInterval` 内部,把 tick 回调函数包裹进一个 Effect Event。 +和 [早前这个页面](/learn/reusing-logic-with-custom-hooks#passing-event-handlers-to-custom-hooks) 做的一样,在 `useInterval` 内部把 tick 回调函数包裹进一个 Effect Event。 -这将让你可以从 Effect 的依赖项中删掉 `onTick`。每次组件重新渲染时,Effect 将不会重新同步,所以页面背景颜色更新间隔不会在有机会触发之前每秒重置一次。 +这将让你可以从 Effect 的依赖项中删掉 `onTick`。每次组件重新渲染时,Effect 将不会重新同步,所以页面背景颜色变化 interval 有机会触发之前不会每秒重置一次。 -随着这个修改,两个 interval 都会像预期的一样工作并且不会互相干预: +随着这个修改,两个 interval 都会像预期一样工作并且不会互相干扰: @@ -2321,13 +2321,13 @@ export function useInterval(callback, delay) { -#### 实现一个交错的运动 {/*implement-a-staggering-movement*/} +#### 实现交错运动 {/*implement-a-staggering-movement*/} -这个例子中,`usePointerPosition()` Hook 追踪当前指针位置。尝试移动光标或你的手指到预览区域上方,看到有一个红点随着你移动。它的位置被保存在变量 `pos1` 中。 +这个示例中,`usePointerPosition()` Hook 追踪当前指针位置。尝试移动光标或你的手指到预览区域上方,可以看到有一个红点随着你移动。它的位置被保存在变量 `pos1` 中。 -事实上,有 5(!) 个不同的红点正在被渲染。你看不见是因为他们都出现在了同一位置。这就是你需要修复的问题。你想要实现的是一个“交错的”运动:每个圆点应该“跟随”它前一个点的路径。例如如果你快速移动光标,第一个点应该立刻跟着它,第二个应该在小小的延时后跟上第一个点,第三个点应该跟着第二个点等。 +事实上,有 5(!) 个正在被渲染的不同红点。你看不见是因为他们现在都显示在同一位置。这就是你需要修复的问题。你想要实现的是一个“交错”运动:每个圆点应该“跟随”它前一个点的路径。例如如果你快速移动光标,第一个点应该立刻跟着它,第二个应该在小小的延时后跟上第一个点,第三个点应该跟着第二个点等等。 -你需要实现自定义 Hook `useDelayedValue`。它当前的实现是返回提供给它的 `value`。而你想从 `delay` 毫秒之前返回 `value`。你可能需要一些 state 和一个 Effect 来完成这个任务。 +你需要实现自定义 Hook `useDelayedValue`。它当前实现返回的是提供给它的 `value`。而你想从 `delay` 毫秒之前返回 `value`。你可能需要一些 state 和一个 Effect 来完成这个任务。 实现 `useDelayedValue` 后,你应该看见这些点一个接一个运动。 @@ -2335,7 +2335,7 @@ export function useInterval(callback, delay) { 你需要在自定义 Hook 内部存储一个 state 变量 `delayedValue`。当 `value` 变化时,你需要运行一个 Effect。这个 Effect 应该在 `delay` 毫秒后更新 `delayedValue`。你可能发现调用 `setTimeout` 很有帮助。 -这个 Effect 需要清除吗?为什么? +这个 Effect 需要清理吗?为什么? @@ -2408,7 +2408,7 @@ body { min-height: 300px; } -这里是一个生效的版本。你将 `delayedValue` 保存为一个 state 变量。当 `value` 更新的时候,你的 Effect 会安排一个 timeout 来更新 `delayedValue`。这就是 `delayedValue` 总是滞后于真实的 `value` 的原因。 +这里是一个生效的版本。你将 `delayedValue` 保存为一个 state 变量。当 `value` 更新时,Effect 会安排一个 timeout 来更新 `delayedValue`。这就是 `delayedValue` 总是“滞后于”实际 `value` 的原因。 @@ -2485,7 +2485,7 @@ body { min-height: 300px; } -请注意这个 Effect **不** 需要清理。如果你在清理函数中调用了 `clearTimeout`,那么每当 `value` 变化时,就会终止已经计划好的 timeout。为了保持运动连续,你想要触发所有的 timeout。 +注意这个 Effect **不** 需要清理。如果你在清理函数中调用了 `clearTimeout`,那么每次 `value` 变化时,就会终止已经计划好的 timeout。为了保持运动连续,你需要触发所有 timeout。 From 9dc98e8c95ff8baa9ede3c42669109fa026b3cd7 Mon Sep 17 00:00:00 2001 From: yanyue Date: Sun, 25 Jun 2023 16:46:38 +0800 Subject: [PATCH 10/14] fix from review suggestion --- src/content/learn/reusing-logic-with-custom-hooks.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index e9a1c38385..c0b0a674fa 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -4,7 +4,7 @@ title: 使用自定义 Hook 复用逻辑 -React 有一些内置 Hook,例如 `useState`, `useContext` 和 `useEffect`。有时你需要一个用途更特殊的 Hook:例如获取数据,记录用户是否在线或者连接聊天室。虽然 React 中可能找不到这些 Hook,但是你可以根据应用需求创建自己的 Hook。 +React 有一些内置 Hook,例如 `useState`,`useContext` 和 `useEffect`。有时你需要一个用途更特殊的 Hook:例如获取数据,记录用户是否在线或者连接聊天室。虽然 React 中可能找不到这些 Hook,但是你可以根据应用需求创建自己的 Hook。 @@ -56,7 +56,7 @@ export default function StatusBar() { 试着开启和关闭网络,注意 `StatusBar` 组件应对你的行为是如何更新的。 -假设现在你想在另一个不同的组件里 **也** 使用同样的逻辑。你希望实现一个 Save 按钮,每当网络断开这个按钮就会不可用并且显示“Reconnecting...”而不是“Save”。 +假设现在你想在另一个不同的组件里 **也** 使用同样的逻辑。你希望实现一个保存按钮,每当网络断开这个按钮就会不可用并且显示“Reconnecting...”而不是“Save progress”。 你可以从复制粘贴 `isOnline` state 和 Effect 到 `SaveButton` 开始: @@ -211,7 +211,7 @@ export function useOnlineStatus() { 切换网络状态验证一下是否会同时更新两个组件。 -现在组件里没有那么多的重复逻辑了。**更重要的是,组件内部的代码描述的是想要做什么(使用在线状态!),而不是怎么做(通过订阅浏览器事件完成)** +现在组件里没有那么多的重复逻辑了。**更重要的是,组件内部的代码描述的是想要做什么(使用在线状态!),而不是怎么做(通过订阅浏览器事件完成)**。 当提取逻辑到自定义 Hook 时,你可以隐藏如何处理外部系统或者浏览器 API 这些乱七八糟的细节。组件内部的代码表达的是目标而不是具体实现。 @@ -447,7 +447,7 @@ function Form() { ## 在 Hook 之间传递响应值 {/*passing-reactive-values-between-hooks*/} -每当组件重新渲染,自定义 Hook 中的代码就会重新运行。这就是组件和自定义 Hook 都 [需要纯粹](/learn/keeping-components-pure) 的原因。我们应该把自定义 Hook 的代码看作组件主体的一部分。 +每当组件重新渲染,自定义 Hook 中的代码就会重新运行。这就是组件和自定义 Hook 都 [需要是纯函数](/learn/keeping-components-pure) 的原因。我们应该把自定义 Hook 的代码看作组件主体的一部分。 由于自定义 Hook 会随着组件一起重新渲染,所以组件可以永远接收到最新的 props 和 state。想知道这意味着什么,那就看看这个聊天室的示例。修改 Server URL 或者聊天室 ID: From 1b917ae119488371622cd490fb15030102591929 Mon Sep 17 00:00:00 2001 From: yueyueyan Date: Sun, 25 Jun 2023 21:58:33 +0800 Subject: [PATCH 11/14] replace link of mdn --- src/content/learn/reusing-logic-with-custom-hooks.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index c0b0a674fa..51a57010fb 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -22,7 +22,7 @@ React 有一些内置 Hook,例如 `useState`,`useContext` 和 `useEffect`。 假设你正在开发一款重度依赖网络的应用(和大多数应用一样)。当用户使用应用时网络意外断开,你需要提醒他。你会怎么处理呢?看上去组件需要两个东西: 1. 一个追踪网络是否在线的 state。 -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) 事件并更新上述 state 的 Effect。 +2. 一个订阅全局 [`online`](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/online_event) 和 [`offline`](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/offline_event) 事件并更新上述 state 的 Effect。 这将让组件与网络状态保持 [同步](/learn/synchronizing-with-effects)。你也许可以像这样开始: @@ -643,9 +643,9 @@ export default function ChatRoom({ roomId }) { } ``` -这看上去简洁多了!(但是它做的是同一件事。) +这看上去简洁多了(但是它做的是同一件事)! -注意逻辑 **仍然响应** prop 和 state 的变化。尝试编辑 server URL 或选中的房间: +注意逻辑 **仍然响应** props 和 state 的变化。尝试编辑 server URL 或选中的房间: @@ -1437,7 +1437,7 @@ function ShippingForm({ country }) { ### 不止一个方法可以做到 {/*there-is-more-than-one-way-to-do-it*/} -假设你想要使用浏览器的 [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) API 实现一个 **从头开始的** fade-in 动画。你也许会从一个设置动画循环的 Effect 开始。在动画的每一帧中,你可以修改 [ref 持有的](/learn/manipulating-the-dom-with-refs) DOM 节点的 opacity 属性直到 `1`。你的代码一开始可能是这样: +假设你想要使用浏览器的 [`requestAnimationFrame`](https://developer.mozilla.org/zh-CN/docs/Web/API/window/requestAnimationFrame) API 实现一个 **从头开始的** fade-in 动画。你也许会从一个设置动画循环的 Effect 开始。在动画的每一帧中,你可以修改 [ref 持有的](/learn/manipulating-the-dom-with-refs) DOM 节点的 opacity 属性直到 `1`。你的代码一开始可能是这样: @@ -1715,7 +1715,7 @@ html, body { min-height: 300px; } -但是 **没有必要** 这样做。和常规函数一样,最终是由你决定在哪里绘制代码不同部分之间的边界。你也可以采取不一样的方法。把大部分必要的逻辑移入一个 [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes),而不是把逻辑保留在 Effect 中: +但是 **没有必要** 这样做。和常规函数一样,最终是由你决定在哪里绘制代码不同部分之间的边界。你也可以采取不一样的方法。把大部分必要的逻辑移入一个 [JavaScript 类](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes),而不是把逻辑保留在 Effect 中: @@ -1815,7 +1815,7 @@ html, body { min-height: 300px; } Effect 可以连接 React 和外部系统。Effect 之间的配合越多(例如链接多个动画),像上面的 sandbox 一样 **完整地** 从 Effect 和 Hook 中提取逻辑就越有意义。然后你提取的代码 **变成** “外部系统”。这会让你的 Effect 保持简洁,因为他们只需要向已经被你移动到 React 外部的系统发送消息。 -上面这个示例假设需要使用 JavaScript 写 fade-in 逻辑。但使用纯 [CSS 动画](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations) 实现这个特定的 fade-in 动画会更加简单和高效: +上面这个示例假设需要使用 JavaScript 写 fade-in 逻辑。但使用纯 [CSS 动画](https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Animations/Using_CSS_animations) 实现这个特定的 fade-in 动画会更加简单和高效: From 554b48ec1e70256a065fe1ba7a65a6b554db9778 Mon Sep 17 00:00:00 2001 From: yueyueyan Date: Sun, 25 Jun 2023 22:31:52 +0800 Subject: [PATCH 12/14] fix from review suggestion --- src/content/learn/reusing-logic-with-custom-hooks.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index 51a57010fb..ba24dfc9cb 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -221,8 +221,8 @@ React 应用是由组件构建的。而组件是由内置或自定义的 Hook 你必须遵循以下这些命名公约: -1. **React 组件名称必须以大写字母开头**, 比如 `StatusBar` 和 `SaveButton`。React 组件还需要返回一些 React 能够显示的内容,比如一段 JSX。 -2. **Hook 的名称必须以后跟一个大写字母的 `use` 开头**, 像 [`useState`](/reference/react/useState) (内置) 或者 `useOnlineStatus` (像本文早前的自定义 Hook)。Hook 可以返回任意值。 +1. **React 组件名称必须以大写字母开头**,比如 `StatusBar` 和 `SaveButton`。React 组件还需要返回一些 React 能够显示的内容,比如一段 JSX。 +2. **Hook 的名称必须以后跟一个大写字母的 `use` 开头**,像 [`useState`](/reference/react/useState) (内置) 或者 `useOnlineStatus` (像本文早前的自定义 Hook)。Hook 可以返回任意值。 这个公约保证你始终可以一眼识别出组件并且知道它的 state,Effect 以及其他的 React 特性可能“隐藏”在哪里。例如如果你在组件内部看见 `getColor()` 函数调用,你可以确定它内部不可能包含 React state,因为它的名称没有以 `use` 开头。但是像 `useOnlineStatus()` 这样的函数调用就很可能包含对内部其他 Hook 的调用! @@ -441,9 +441,9 @@ function Form() { 这就是为什么它工作的时候像声明了两个单独的 state 变量! -**自定义 Hook 只是共享有状态逻辑而不是 state 本身。对 Hook 的每个调用完全独立于对同一个 Hook 的其他调用**。 这就是上面两个 sandbox 结果完全相同的原因。如果愿意,你可以划上去进行比较。提取自定义 Hook 前后组件的行为是一致的。 +**自定义 Hook 只是共享有状态逻辑而不是 state 本身。对 Hook 的每个调用完全独立于对同一个 Hook 的其他调用**。这就是上面两个 sandbox 结果完全相同的原因。如果愿意,你可以划上去进行比较。提取自定义 Hook 前后组件的行为是一致的。 -当你需要在多个组件之间共享 state 本身时,需要[将变量提升并传递下去](/learn/sharing-state-between-components)。 +当你需要在多个组件之间共享 state 本身时,需要 [将变量提升并传递下去](/learn/sharing-state-between-components)。 ## 在 Hook 之间传递响应值 {/*passing-reactive-values-between-hooks*/} From 1f938c96110561b473abb6c939f0b717e4e443ff Mon Sep 17 00:00:00 2001 From: yueyueyan Date: Thu, 29 Jun 2023 00:11:17 +0800 Subject: [PATCH 13/14] =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=A1=A8=E8=BE=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../learn/reusing-logic-with-custom-hooks.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index ba24dfc9cb..3d8f7e7797 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -4,15 +4,15 @@ title: 使用自定义 Hook 复用逻辑 -React 有一些内置 Hook,例如 `useState`,`useContext` 和 `useEffect`。有时你需要一个用途更特殊的 Hook:例如获取数据,记录用户是否在线或者连接聊天室。虽然 React 中可能找不到这些 Hook,但是你可以根据应用需求创建自己的 Hook。 +React 有一些内置 Hook,例如 `useState`,`useContext` 和 `useEffect`。有时你需要一个用途更特殊的 Hook:例如获取数据,记录用户是否在线或者连接聊天室。虽然 React 中可能没有这些 Hook,但是你可以根据应用需求创建自己的 Hook。 -- 自定义 Hook 是什么,以及如何编写 +- 什么是自定义 Hook,以及如何编写 - 如何在组件间重用逻辑 -- 如何命名和构建自定义 Hook +- 如何给自定义 Hook 命名以及如何构建 - 提取自定义 Hook 的时机和原因 @@ -24,7 +24,7 @@ React 有一些内置 Hook,例如 `useState`,`useContext` 和 `useEffect`。 1. 一个追踪网络是否在线的 state。 2. 一个订阅全局 [`online`](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/online_event) 和 [`offline`](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/offline_event) 事件并更新上述 state 的 Effect。 -这将让组件与网络状态保持 [同步](/learn/synchronizing-with-effects)。你也许可以像这样开始: +这会让组件与网络状态保持 [同步](/learn/synchronizing-with-effects)。你也许可以像这样开始: @@ -54,11 +54,11 @@ export default function StatusBar() { -试着开启和关闭网络,注意 `StatusBar` 组件应对你的行为是如何更新的。 +试着开启和关闭网络,注意观察 `StatusBar` 组件应对你的行为是如何更新的。 假设现在你想在另一个不同的组件里 **也** 使用同样的逻辑。你希望实现一个保存按钮,每当网络断开这个按钮就会不可用并且显示“Reconnecting...”而不是“Save progress”。 -你可以从复制粘贴 `isOnline` state 和 Effect 到 `SaveButton` 开始: +你可以从复制粘贴 `isOnline` state 和 Effect 到 `SaveButton` 组件开始: @@ -96,13 +96,13 @@ export default function SaveButton() { -如果你关闭网络,可以验证出这个按钮将会变更外观。 +如果你关闭网络,可以发现这个按钮的外观变了。 这两个组件都能很好地工作,但不幸的是他们的逻辑重复了。他们看上去有不同的 **视觉外观**,但你依然想复用他们的逻辑。 ### 从组件中提取自定义 Hook {/*extracting-your-own-custom-hook-from-a-component*/} -假设有一个与 [`useState`](/reference/react/useState) 和 [`useEffect`](/reference/react/useEffect) 相似的内置 Hook `useOnlineStatus`。那么你就可以简化这两个组件并移除他们之间的重复部分: +假设有一个内置 Hook `useOnlineStatus`,它与 [`useState`](/reference/react/useState) 和 [`useEffect`](/reference/react/useEffect) 相似。那么你就可以简化这两个组件并移除他们之间的重复部分: ```js {2,7} function StatusBar() { @@ -217,18 +217,18 @@ export function useOnlineStatus() { ### Hook 的名称必须永远以 `use` 开头 {/*hook-names-always-start-with-use*/} -React 应用是由组件构建的。而组件是由内置或自定义的 Hook 构建。可能你经常使用的是别人写的自定义 Hook,但偶尔可能也要自己写! +React 应用是由组件构成,而组件由内置或自定义 Hook 构成。可能你经常使用别人写的自定义 Hook,但偶尔也要自己写! 你必须遵循以下这些命名公约: 1. **React 组件名称必须以大写字母开头**,比如 `StatusBar` 和 `SaveButton`。React 组件还需要返回一些 React 能够显示的内容,比如一段 JSX。 2. **Hook 的名称必须以后跟一个大写字母的 `use` 开头**,像 [`useState`](/reference/react/useState) (内置) 或者 `useOnlineStatus` (像本文早前的自定义 Hook)。Hook 可以返回任意值。 -这个公约保证你始终可以一眼识别出组件并且知道它的 state,Effect 以及其他的 React 特性可能“隐藏”在哪里。例如如果你在组件内部看见 `getColor()` 函数调用,你可以确定它内部不可能包含 React state,因为它的名称没有以 `use` 开头。但是像 `useOnlineStatus()` 这样的函数调用就很可能包含对内部其他 Hook 的调用! +这个公约保证你始终能一眼识别出组件并且知道它的 state,Effect 以及其他的 React 特性可能“隐藏”在哪里。例如如果你在组件内部看见 `getColor()` 函数调用,就可以确定它里面不可能包含 React state,因为它的名称没有以 `use` 开头。但是像 `useOnlineStatus()` 这样的函数调用就很可能包含对内部其他 Hook 的调用! -如果你为 [React 配置了](/learn/editor-setup#linting) 代码检查工具,它会强制执行这个命名公约。现在滑动到上面的 sandbox,并将 `useOnlineStatus` 重命名为 `getOnlineStatus`。注意此时代码检查工具将不会再允许你其内部调用 `useState` 或者 `useEffect`。只有 Hook 和组件可以调用其他 Hook! +如果你为 [React 配置了](/learn/editor-setup#linting) 代码检查工具,它会强制执行这个命名公约。现在滑动到上面的 sandbox,并将 `useOnlineStatus` 重命名为 `getOnlineStatus`。注意此时代码检查工具将不会再允许你在其内部调用 `useState` 或者 `useEffect`。只有 Hook 和组件可以调用其他 Hook! @@ -265,7 +265,7 @@ function List({ items, shouldSort }) { } ``` -如果内部至少使用了一个 Hook,你就应该给这个函数加 `use` 前缀(让它成为一个 Hook): +哪怕内部只使用了一个 Hook,你也应该给这个函数加 `use` 前缀(让它成为一个 Hook): ```js // ✅ Good: 一个使用了其他Hook的Hook @@ -285,7 +285,7 @@ function useAuth() { } ``` -接下来组件就不能在条件语句里调用这个函数。当你在内部实际添加了 Hook 调用时,这一点将变得很重要。如果你没有计划在内部使用 Hook(现在或者之后),请不要让它变成 Hook。 +接下来组件就不能在条件语句里调用这个函数。当你在内部实际添加了 Hook 调用时,这一点将变得很重要。如果你(现在或者之后)没有计划在内部使用 Hook,请不要让它变成 Hook。 From 3858672c724efb6e8574a29e81e55f66a43fcbbe Mon Sep 17 00:00:00 2001 From: yanyue Date: Thu, 29 Jun 2023 14:19:17 +0800 Subject: [PATCH 14/14] fix from review suggestion --- .../learn/reusing-logic-with-custom-hooks.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index 3d8f7e7797..85aca2cbac 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -222,7 +222,7 @@ React 应用是由组件构成,而组件由内置或自定义 Hook 构成。 你必须遵循以下这些命名公约: 1. **React 组件名称必须以大写字母开头**,比如 `StatusBar` 和 `SaveButton`。React 组件还需要返回一些 React 能够显示的内容,比如一段 JSX。 -2. **Hook 的名称必须以后跟一个大写字母的 `use` 开头**,像 [`useState`](/reference/react/useState) (内置) 或者 `useOnlineStatus` (像本文早前的自定义 Hook)。Hook 可以返回任意值。 +2. **Hook 的名称必须以后跟一个大写字母的 `use` 开头**,像 [`useState`](/reference/react/useState)(内置) 或者 `useOnlineStatus`(像本文早前的自定义 Hook)。Hook 可以返回任意值。 这个公约保证你始终能一眼识别出组件并且知道它的 state,Effect 以及其他的 React 特性可能“隐藏”在哪里。例如如果你在组件内部看见 `getColor()` 函数调用,就可以确定它里面不可能包含 React state,因为它的名称没有以 `use` 开头。但是像 `useOnlineStatus()` 这样的函数调用就很可能包含对内部其他 Hook 的调用! @@ -289,7 +289,7 @@ function useAuth() { -### 自定义 Hook 共享有状态逻辑,而不是 state 本身 {/*custom-hooks-let-you-share-stateful-logic-not-state-itself*/} +### 自定义 Hook 共享的是状态逻辑,而不是状态本身 {/*custom-hooks-let-you-share-stateful-logic-not-state-itself*/} 之前的例子里,当你开启或关闭网络时,两个组件一起更新了。但是两个组件共享 state 变量 `isOnline` 这种想法是错的。看这段代码: @@ -441,7 +441,7 @@ function Form() { 这就是为什么它工作的时候像声明了两个单独的 state 变量! -**自定义 Hook 只是共享有状态逻辑而不是 state 本身。对 Hook 的每个调用完全独立于对同一个 Hook 的其他调用**。这就是上面两个 sandbox 结果完全相同的原因。如果愿意,你可以划上去进行比较。提取自定义 Hook 前后组件的行为是一致的。 +**自定义 Hook 共享的只是状态逻辑而不是状态本身。对 Hook 的每个调用完全独立于对同一个 Hook 的其他调用**。这就是上面两个 sandbox 结果完全相同的原因。如果愿意,你可以划上去进行比较。提取自定义 Hook 前后组件的行为是一致的。 当你需要在多个组件之间共享 state 本身时,需要 [将变量提升并传递下去](/learn/sharing-state-between-components)。 @@ -449,7 +449,7 @@ function Form() { 每当组件重新渲染,自定义 Hook 中的代码就会重新运行。这就是组件和自定义 Hook 都 [需要是纯函数](/learn/keeping-components-pure) 的原因。我们应该把自定义 Hook 的代码看作组件主体的一部分。 -由于自定义 Hook 会随着组件一起重新渲染,所以组件可以永远接收到最新的 props 和 state。想知道这意味着什么,那就看看这个聊天室的示例。修改 Server URL 或者聊天室 ID: +由于自定义 Hook 会随着组件一起重新渲染,所以组件可以一直接收到最新的 props 和 state。想知道这意味着什么,那就看看这个聊天室的示例。修改 ServeUrl 或者 roomID: @@ -1437,7 +1437,7 @@ function ShippingForm({ country }) { ### 不止一个方法可以做到 {/*there-is-more-than-one-way-to-do-it*/} -假设你想要使用浏览器的 [`requestAnimationFrame`](https://developer.mozilla.org/zh-CN/docs/Web/API/window/requestAnimationFrame) API 实现一个 **从头开始的** fade-in 动画。你也许会从一个设置动画循环的 Effect 开始。在动画的每一帧中,你可以修改 [ref 持有的](/learn/manipulating-the-dom-with-refs) DOM 节点的 opacity 属性直到 `1`。你的代码一开始可能是这样: +假设你想要使用浏览器的 [`requestAnimationFrame`](https://developer.mozilla.org/zh-CN/docs/Web/API/window/requestAnimationFrame) API **从头开始** 实现一个 fade-in 动画。你也许会从一个设置动画循环的 Effect 开始。在动画的每一帧中,你可以修改 [ref 持有的](/learn/manipulating-the-dom-with-refs) DOM 节点的 opacity 属性直到 `1`。你的代码一开始可能是这样: @@ -1876,7 +1876,7 @@ html, body { min-height: 300px; } - 自定义 Hook 让你可以在组件间共享逻辑。 - 自定义 Hook 命名必须以后跟一个大写字母的 `use` 开头。 -- 自定义 Hook 共享的只是有状态逻辑,不是 state 本身。 +- 自定义 Hook 共享的只是状态逻辑,不是状态本身。 - 你可以将响应值从一个 Hook 传到另一个,并且他们会保持最新。 - 每次组件重新渲染时,所有的 Hook 会重新运行。 - 自定义 Hook 的代码应该和组件代码一样保持纯粹。 @@ -2064,7 +2064,7 @@ export function useCounter(delay) { #### 从 `useCounter` 中提取 `useInterval` {/*extract-useinterval-out-of-usecounter*/} -现在 `useCounter` Hook 做两件事。设置一个 interval,并且在每个interval tick 内递增一次 state 变量。将设置 interval 的逻辑拆分到一个独立 Hook `useInterval`。它应该有两个参数:`onTick` 回调函数和 `delay`。本次修改后 `useCounter` 的实现应该如下所示: +现在 `useCounter` Hook 做两件事。设置一个 interval,并且在每个 interval tick 内递增一次 state 变量。将设置 interval 的逻辑拆分到一个独立 Hook `useInterval`。它应该有两个参数:`onTick` 回调函数和 `delay`。本次修改后 `useCounter` 的实现应该如下所示: ```js export function useCounter(delay) { @@ -2325,9 +2325,9 @@ export function useInterval(callback, delay) { 这个示例中,`usePointerPosition()` Hook 追踪当前指针位置。尝试移动光标或你的手指到预览区域上方,可以看到有一个红点随着你移动。它的位置被保存在变量 `pos1` 中。 -事实上,有 5(!) 个正在被渲染的不同红点。你看不见是因为他们现在都显示在同一位置。这就是你需要修复的问题。你想要实现的是一个“交错”运动:每个圆点应该“跟随”它前一个点的路径。例如如果你快速移动光标,第一个点应该立刻跟着它,第二个应该在小小的延时后跟上第一个点,第三个点应该跟着第二个点等等。 +事实上,有 5(!)个正在被渲染的不同红点。你看不见是因为他们现在都显示在同一位置。这就是你需要修复的问题。你想要实现的是一个“交错”运动:每个圆点应该“跟随”它前一个点的路径。例如如果你快速移动光标,第一个点应该立刻跟着它,第二个应该在小小的延时后跟上第一个点,第三个点应该跟着第二个点等等。 -你需要实现自定义 Hook `useDelayedValue`。它当前实现返回的是提供给它的 `value`。而你想从 `delay` 毫秒之前返回 `value`。你可能需要一些 state 和一个 Effect 来完成这个任务。 +你需要实现自定义 Hook `useDelayedValue`。它当前的实现返回的是提供给它的 `value`。而你想从 `delay` 毫秒之前返回 `value`。你可能需要一些 state 和一个 Effect 来完成这个任务。 实现 `useDelayedValue` 后,你应该看见这些点一个接一个运动。