diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index 679a9bac2..26d43eabc 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -1,30 +1,31 @@ --- -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 伴隨一些像 useState 、 useContext 和 useEffect 的內建 Hook 。有時候,你會希望有個 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 ,與如何自行編寫 +- 如何在 component 間重複使用邏輯 +- 如何命名與建構客製化的 Hook +- 提取客製化 Hook 的時機與原因 + -## Custom Hooks: Sharing logic between components {/*custom-hooks-sharing-logic-between-components*/} +## 客製化 Hooks :在 Component 間共享邏輯 {/*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: +想像正在開發一個大量依賴網路的應用程式(像是大部分的應用程式),你想在使用者無法使用應用程式時,警告他們網路連線意外中斷,你會怎麼做呢?也許需要在 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. Effect 訂閱全域的 [`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 -This will keep your component [synchronized](/learn/synchronizing-with-effects) with the network status. You might start with something like this: +這會讓 component 保持[同步](/learn/synchronizing-with-effects)網路狀態,你也許會像這樣開始: @@ -54,11 +55,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. +現在,想像你*也*想在不同的 component 中使用相同的邏輯。你想完成一個 Save 按鈕,它會在網路關閉時無法使用,並且顯示「 Reconnecting... 」,而非「 Save 」。 -To start, you can copy and paste the `isOnline` state and the Effect into `SaveButton`: +首先,你可以將 `isOnline` 、 Effect 複製及貼至 `SaveButton` 內部: @@ -96,13 +97,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. +這兩個 component 會正常運作,但不幸的是它們的邏輯重複;即使它們有不同的*視覺外觀*,但你會想重複使用它們的邏輯。 -### Extracting your own custom Hook from a component {/*extracting-your-own-custom-hook-from-a-component*/} +### 從 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 ,兩者都可以簡化這些 component 的程式,並可從中移除重複的部分: ```js {2,7} function StatusBar() { @@ -125,7 +126,8 @@ 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` ,將全部重複的程式從 component 內移動到其內部: ```js {2-16} function useOnlineStatus() { @@ -148,7 +150,7 @@ function useOnlineStatus() { } ``` -At the end of the function, return `isOnline`. This lets your components read that value: +在函數的最後回傳 `isOnline` ,讓 component 可讀取到這個值: @@ -209,89 +211,89 @@ export function useOnlineStatus() { -Verify that switching the network on and off updates both components. +驗證切換網路開關時,是否更新兩個元件。 -Now your components don't have as much repetitive logic. **More importantly, the code inside them describes *what they want to do* (use the online status!) rather than *how to do it* (by subscribing to the browser events).** +現在 component 內不再有重複的邏輯,**更重要的是,它們內部的程式描述*它們要做的事情*(使用線上的狀態!),而非*它們如何做*(透過訂閱瀏覽器事件)。** -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 的粗糙細節; component 內的程式表達你的意圖,而非實作方式。 -### 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 應用程式由 component 所構成; component 由 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 component 名稱的開頭必須是大寫**,像是 `StatusBar` 和 `SavaButton` 。 React component 也需要回傳一些東西,讓 React 知道如何顯示,像是一段 JSX 。 +2. **Hook 名稱的必須起始於 `use` ,接續是大寫**,像是 [useState](/reference/react/useState) (內建)或 `useOnlineStatus` (客製化,像這頁之前的); 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! +這些慣例確保你可以總是看到 component 就知道它的 state 、 Effect 和其他可能「隱藏」的 React 功能;例如,如果你看到 component 內部呼叫一個 `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! +如果你的 linter 是[為 React 配置的](/learn/editor-setup#linting),它會強制執行該命名慣例。往上滑到上方的沙盒,將 `useOnlineStatus` 重新命名為 `getOnlineStatus` ,注意 linter 不會允許你在內部呼叫 `useState` 或 `useEffect` ,只有 Hook 與 component 可以呼叫其他 Hook ! -#### Should all functions called during rendering start with the use prefix? {/*should-all-functions-called-during-rendering-start-with-the-use-prefix*/} +#### 所有 Render 期間被呼叫的函數都應使用前綴 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 +// ✅ 好的:一個不使用 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 (因此讓它成為 Hook),你應該加上前綴 `use` : ```js -// ✅ Good: A Hook that uses other Hooks +// ✅ 好的:一個使用其他 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 +// ✅ 好的:一個稍後很可能會使用其他 Hook 的 Hook function useAuth() { - // TODO: Replace with this line when authentication is implemented: - // return useContext(Auth); + // 該做的: 在實作驗證時更新這一行 + // 回傳 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. +如此一來, component 就不可能有條件地被呼叫,這在內部實際加入呼叫 Hooks 時變得重要;如果沒有預計在內部(現在或稍後)使用 Hook ,不要將它變成 Hook 。 -### 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: +稍早的案例中,切換網路的開關時會同時更新兩個 component ,但認為它們共享單一 `isOnline` state 變數是錯誤的。看這段程式: ```js {2,7} function StatusBar() { @@ -305,7 +307,7 @@ function SaveButton() { } ``` -It works the same way as before you extracted the duplication: +它使用與之前提取重複部分相同的方式: ```js {2-5,10-13} function StatusBar() { @@ -325,9 +327,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` component : @@ -369,13 +371,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 ( `firsrName` 與 `lastName` ) +1. 有改變的處理器( `handleFirstNameChange` 與 `handleLastNameChange` ) +1. 有部分的 JSX 為 input 指定 `value` 與 `onChange` 屬性 -You can extract the repetitive logic into this `useFormInput` custom Hook: +你可以將重複的邏輯提取到 `useFormInput` 的客製化 Hook 中: @@ -428,9 +430,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` component 呼叫*兩次* `useFormInput` : ```js function Form() { @@ -439,17 +441,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 呼叫**,這是為什麼上方兩個沙盒是完全相等的。如果你願意,往上滑並比較它們,提取客製化 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. +當你需要在複數 component 間共享 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 內部的程式會在每次 component re-render 期間重新執行,這是為什麼像是 component 或客製化的 Hook [需要保持單純](/learn/keeping-components-pure),將客製化 Hook 的程式當成 component 的主要部分! -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: +因為客製化 Hook 會與 component 共同 re-render ,它們會總是接收到最新的 props 與 state 。要知道其意涵,想像下方的聊天室範例,改變伺服器的網址或聊天室: @@ -599,9 +601,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),並且重新同步。你可以透過 console 的訊息得知,每當 Effect 的 dependency 改變時,聊天室會重新連線。 -Now move the Effect's code into a custom Hook: +現在將 Effect 的程式移到客製化 Hook 中: ```js {2-13} export function useChatRoom({ serverUrl, roomId }) { @@ -620,7 +622,7 @@ export function useChatRoom({ serverUrl, roomId }) { } ``` -This lets your `ChatRoom` component call your custom Hook without worrying about how it works inside: +這讓 `ChatRoom` component 呼叫客製化 Hook 時不需擔心內部的運作: ```js {4-7} export default function ChatRoom({ roomId }) { @@ -643,9 +645,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: +注意邏輯*仍回應* props 與 state 的變化;嘗試編輯伺服器的網址或選擇的房間: @@ -724,7 +726,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 +809,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 +822,7 @@ export default function ChatRoom({ roomId }) { // ... ``` -and pass it as an input to another Hook: +以及將它當成 input 向另一個 Hook 傳遞: ```js {6} export default function ChatRoom({ roomId }) { @@ -833,17 +835,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` component re-render 時,它傳遞最新的 `roomId` 與 `serverUrl` 到 Hook 內,這是為什麼在 re-render 後,無論它們的值是否改變, Effect 都會重現連線至聊天室。(如果你曾經使用聲音或影片處理軟體,連鎖 Hook 會讓你想起串連視覺與聲音效果,就像 `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. +描述這部分的**實驗性 API 還未釋出**於 React 的穩定版本中。 -As you start using `useChatRoom` in more components, you might want to let components customize its behavior. For example, currently, the logic for what to do when a message arrives is hardcoded inside the Hook: +當你在更多 component 內開始使用 `useChatRoom` 時,可能會想讓 component 客製化它的行為;例如,現在寫死在 Hook 內的邏輯是在收到訊息時要執行的: ```js {9-11} export function useChatRoom({ serverUrl, roomId }) { @@ -862,7 +864,7 @@ export function useChatRoom({ serverUrl, roomId }) { } ``` -Let's say you want to move this logic back to your component: +你想要將該邏輯移回 component 中: ```js {7-9} export default function ChatRoom({ roomId }) { @@ -878,7 +880,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 +895,13 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { onReceiveMessage(msg); }); return () => connection.disconnect(); - }, [roomId, serverUrl, onReceiveMessage]); // ✅ All dependencies declared + }, [roomId, serverUrl, onReceiveMessage]); // ✅ 宣告所有的 dependency } ``` -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` 加入 dependency 並不理想,因為它會導致聊天室在每次 component re-render 時重新連線,[將該事件處理器包裝到 Effect 事件內,並從 dependency 中移除](/learn/removing-effect-dependencies#wrapping-an-event-handler-from-the-props): ```js {1,4,5,15,18} import { useEffect, useEffectEvent } from 'react'; @@ -919,11 +921,11 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { onMessage(msg); }); return () => connection.disconnect(); - }, [roomId, serverUrl]); // ✅ All dependencies declared + }, [roomId, serverUrl]); // ✅ 宣告所有的 dependency } ``` -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` component re-render 時重新連線。以下示範事件處理器傳入客製化 Hook 後,可以操作的完整動作: @@ -1008,7 +1010,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 +1093,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` 而了解它*如何*執行,你可以將它加入到其他任何 component 、傳入任何選項,它會以相同的方式執行;這就是客製化 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 ,有些重複是可以的;例如,像之前提取一個 `useFormInput` Hook ,用以包裝一個 `useState` 呼叫可能會不太必要。 -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` component:一個顯示城市的列表、另一個顯示所選城市的區域列表。你可能會從一些像這樣的程式開始: ```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 +1123,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 +1143,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 ;反之,你可以透過提取上方 `ShippingForm` component 內的共同邏輯到 `useData` Hook 中: ```js {2-18} function useData(url) { @@ -1165,7 +1167,7 @@ function useData(url) { } ``` -Now you can replace both Effects in the `ShippingForm` components with calls to `useData`: +現在,你可以透過呼叫 `useData` 更新 `ShippingForm` component 內的兩個 Effect : ```js {2,4} function ShippingForm({ country }) { @@ -1175,39 +1177,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` ;透過在 `useData` 內「隱藏」 Effect ,你也預防有些處理 `ShippingForm` component 的人加入[不必要的 dependency ](/learn/removing-effect-dependencies)。隨著時間推進,大部分應用程式內的 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 和 component 邏輯的剩餘部分過於耦合,它還沒準備好要被提取。 -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 聚焦於具體的高層級使用情境**,避免建立與使用客製化的「生命週期」 Hook ,作為 `useEffect` API 本身的替代方案與便利的包裝器: * 🔴 `useMount(fn)` * 🔴 `useEffectOnce(fn)` * 🔴 `useUpdateEffect(fn)` -For example, this `useMount` Hook tries to ensure some code only runs "on mount": +例如,該 `useMount` Hook 嘗試確保一些程式只在「 on mount 」時執行: ```js {4-5,14-15} function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); - // 🔴 Avoid: using custom "lifecycle" Hooks + // 🔴 避免:使用客製化的「生命週期」 Hook useMount(() => { const connection = createConnection({ roomId, serverUrl }); connection.connect(); @@ -1217,23 +1219,23 @@ function ChatRoom({ roomId }) { // ... } -// 🔴 Avoid: creating custom "lifecycle" Hooks +// 🔴 避免:建立客製化的「生命週期」 Hook function useMount(fn) { useEffect(() => { fn(); - }, []); // 🔴 React Hook useEffect has a missing dependency: 'fn' + }, []); // 🔴 React Hook useEffect 失去一個 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. +**如同 `useMount` 的客製化「生命週期」 Hook 無法符合 React 的範例**;例如,此程式範例有一個錯誤(它沒有「回應」 `roomId` 或 `serverUrl` 的改變),但 linter 沒有警告,因為 linter 只會直接確認 `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 + // ✅ 好的:兩個原本的 Effect 因為目的而分開 useEffect(() => { const connection = createConnection({ serverUrl, roomId }); @@ -1249,28 +1251,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 + // ✅ 好的:客製化 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(option)` 只會連線到聊天室, `useImpressionLog(eventName, extraData)` 則只會為分析傳送曝光紀錄;如果客製化 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 +1333,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) 實作,但這不是最好的解決辦法,沒有考慮到一些危險的情況;例如,它假設 component mount 時, `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`](/reference/react/useSyncExternalStore) 的專用 API ,它可以為你處理這類型的問題。以下是 `useOnlineStatus` Hook 如何重新改寫以使用此新 API : @@ -1384,8 +1386,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 +1395,7 @@ export function useOnlineStatus() { -Notice how **you didn't need to change any of the components** to make this migration: +注意你如何**不需要改變任何 component** 將它們轉移: ```js {2,7} function StatusBar() { @@ -1407,22 +1409,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. 你讓 component 聚焦在意圖,而非準確的 Effect 執行步驟 +3. 當 React 增加新功能時,你可以不需要改變任何 component 就移除這些 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)相似,你可能會發現它有助於從應用程式的 component 提取共同片段到客製化 Hook 中,它會讓 component 的程式聚焦在意圖上,讓你避免頻繁編寫原本的 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 +1433,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. +如果你在應用程式中使用像是上方 `useData` 的客製化 Hook ,它會比在每個 component 中手動編寫原生的 Effect ,還需要更多的改變以轉移到最後推薦的方法。但舊方法仍可以持續運作,因此你就快樂地編寫原生的 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 節點透明度,直到它變成: `1` 。你的程式可能會先像這樣: @@ -1459,7 +1461,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 +1522,7 @@ html, body { min-height: 300px; } -To make the component more readable, you might extract the logic into a `useFadeIn` custom Hook: +為了讓 component 更容易閱讀,你可能會將邏輯提取到 `useFadeIn` 的客製化 Hook 中: @@ -1569,7 +1571,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 +1613,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` 的外面,提取到客製化 `useAnimationLoop` 的 Hook 內: @@ -1715,7 +1717,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 的 [class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) 內,而非將邏輯保留在 Effect 中: @@ -1782,7 +1784,7 @@ export class FadeInAnimation { if (progress === 1) { this.stop(); } else { - // We still have more frames to paint + // 我們仍需要繪製更多的影格 this.frameId = requestAnimationFrame(() => this.onFrame()); } } @@ -1813,9 +1815,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 之間需要越多的協調(例如串連複數的動畫),像上方的沙盒將邏輯*完全*提取到 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 中,但這種特定的淡入動畫使用簡單的 [CSS 動畫](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations)會更簡潔且更有效率: @@ -1870,28 +1872,28 @@ 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 讓你在 component 間共享邏輯 +- 客製化 Hook 必須使用 `use` 命名,後面需接上大寫字母 +- 客製化 Hook 只會共享有狀態的邏輯,而非 state 本身 +- 你可以將回應的值從一個 Hook 傳給另一個,它們會保持最新 +- 所有的 Hook 會在 component re-render 時重新執行 +- 客製化 Hook 的程式需要保持單純,像是 component 的程式 +- 將接收客製化 Hook 的事件處理器包裝到 Effect 事件內 +- 不要建立像是 `useMount` 的客製化 Hook ,保持它們的特殊目的 +- 你可以決定如何選擇程式的邊界與地方 -#### 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: +#### 提取 `useCounter` Hook {/*extract-a-usecounter-hook*/} +這 component 使用 state 變數與 Effect 顯示每秒增加的數字。將該邏輯提取至名為 `useCounter` 的客製化 Hook 中,你的目標是讓 `Counter` component 完成後像這樣: + ```js export default function Counter() { const count = useCounter(); @@ -1899,8 +1901,8 @@ 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` 檔案內。 + ```js @@ -1919,14 +1921,15 @@ export default function Counter() { ``` ```js useCounter.js -// Write your custom Hook in this file! +// 在這個檔案中編寫你的客製化 Hook ! + ``` -Your code should look like this: +你的程式碼應該如此: @@ -1956,13 +1959,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*/} +#### 使計數器的延遲是可設定的 {/*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. +在此案例中,滑桿控制 `delay` state 變數,該值卻不被使用。將 `delay` 的值傳入客製化的 `useCounter` Hook 中,並改變 `useCounter` Hook 以使用傳入的 `deplay` ,而非寫死的 `1000` 毫秒。 @@ -2012,7 +2015,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` ;你會需要將 `delay` 加入到 Effect 的 dependency 中,這會確保 `delay` 改變時會重新設定時間間隔。 @@ -2062,9 +2065,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 執行兩件事情,它設定時間間隔,也會在每隔一段時間增加 state 變數。將設定時間間隔的邏輯拆開,並放入名為 `useInterval` 的單獨 Hook 內,它應該需要兩個引數: `onTick` 的 callback 與 `delay` 。改變後,你的 `useCounter` 應該會如同: ```js export function useCounter(delay) { @@ -2076,7 +2079,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 +2109,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,36 +2155,36 @@ 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*/} - -In this example, there are *two* separate intervals. +#### 修改重新設定的時間間隔 {/*fix-a-resetting-interval*/} -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`: +`App` component 呼叫 `useCounter` ,它呼叫 `useInterval` 每秒更新計數器;但 `App` component *也*呼叫 `useInterval` 每兩秒隨機更新頁面的背景顏色。 +基於一些理由,更新頁面背景的 callback 並為執行。在 `useInterval` 中加入一些邏輯: + ```js {2,5} useEffect(() => { - console.log('✅ Setting up an interval with delay ', delay) + console.log('✅ 使用 delay 設定間隔 ', delay) const id = setInterval(onTick, delay); return () => { - console.log('❌ Clearing an interval with delay ', delay) + console.log('❌ 使用 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? +Log 符合你所期望發生的事情嗎?如果有些 Effect 似乎非必要地重新同步,你可以猜測是哪個 dependency 引起的嗎?有存在一些從 Effect 中[移除 dependency ](/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 的 dependency 嗎? @@ -2250,11 +2253,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) +在 `useInterval` 中,將 tick callback 包裝到 Effect 事件中,就你[稍早在此頁](/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. +這會允許你從 Effect 的 dependency 中忽略 `onTick` , Effect 不會在每次 component re-render 時同步更新,因此每次改變頁面的背景顏色前,不會有機會觸發每秒的重新設定。 -With this change, both intervals work as expected and don't interfere with each other: +透過這個改變,兩個時間間隔會如預期般執行,且不會互相干擾: @@ -2321,21 +2324,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. +事實上,有五個(!)不同的紅點會被 render 。你不會看見它們,因為現在它們全部都出現在相同的位置,這是你需要修改的,你需要完成「驚人的」動作取代它:每個點應該「跟隨」上一個點的路徑。例如,如果快速移動你的游標,第一個點應該馬上跟上;第二個點應該稍緩跟隨第一個點;第三個點應該跟著第二個點,以此類推。 -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. +你需要完成 `useDeleyedValue` 客製化 Hook ,目前的實作會回傳提供給它的 `value` ;而你想要將值回傳至 `delay` 的毫秒前。你可能需要使用一些 state 與 Effect 完成它。 -After you implement `useDelayedValue`, you should see the dots move following one another. +在你完成 `useDelayValue` 後,你應該看到點跟著另一個點移動。 -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`. +你會需要將 `delayedValue` 儲存為客製化 Hook 內的 state 變數。當 `value` 改變時,你會想要執行 Effect ,該 Effect 應該在 `delay` 後更新 `delayedValue` 。你可能發現呼叫 `setTimeout` 是有用的。 -Does this Effect need cleanup? Why or why not? +這個 Effect 需要清除嗎?為什麼與為什麼不? @@ -2408,7 +2411,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 安排暫停以更新 `delayedValue` 。這是為什麼 `delayedValue` 總是「落後」實際的 `value` 。 @@ -2485,7 +2488,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` 改變時,它都會重新設定已經規劃好的暫停。為了保持持續的動作,你需要觸發每次的暫停。 diff --git a/src/content/learn/updating-arrays-in-state.md b/src/content/learn/updating-arrays-in-state.md index 45c6b70dc..dd228f586 100644 --- a/src/content/learn/updating-arrays-in-state.md +++ b/src/content/learn/updating-arrays-in-state.md @@ -1,52 +1,52 @@ --- -title: Updating Arrays in State +title: 更新 State 內的 Array --- -Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array. +Javascript 內的 array 是可改變的,但當你要將它們儲存在 state 中時,應該將其視為不可改變的,就像 object 。當想更新儲存在 state 中的 array 時,你需要建立一個新的(或建立一個現有 array 的副本),並為新 array 設定 state 。 -- How to add, remove, or change items in an array in React state -- How to update an object inside of an array -- How to make array copying less repetitive with Immer +- 如何增加、移除或改變 React state 內的 array 項目 +- 如何更新 array 內的 object +- 如何使用 Immer 減少重複的 array 複製 -## Updating arrays without mutation {/*updating-arrays-without-mutation*/} +## 更新 Array 而不 Mutation {/*updating-arrays-without-mutation*/} -In JavaScript, arrays are just another kind of object. [Like with objects](/learn/updating-objects-in-state), **you should treat arrays in React state as read-only.** This means that you shouldn't reassign items inside an array like `arr[0] = 'bird'`, and you also shouldn't use methods that mutate the array, such as `push()` and `pop()`. +在 Javascript 中, array 只是另一種型態的 object ,[就像 object ](/learn/updating-objects-in-state),**你應該將 React state 中的 array 視為只能讀取**。這代表你不應該在 array 內重新指定項目,像是 `arr[0] = 'bird'` ;也不應該使用會改變 array 的方法,像是 `push()` 及 `pop()` 。 -Instead, every time you want to update an array, you'll want to pass a *new* array to your state setting function. To do that, you can create a new array from the original array in your state by calling its non-mutating methods like `filter()` and `map()`. Then you can set your state to the resulting new array. +反之,每次你想更新 array 時,會需要傳入一個*新的* array 到 state 的 setting 函數中。為此,你可以呼叫不會改變的方法,如 `filter()` 和 `map()` ,從 state 內的原始 array 中建立一個新 array ,再將其結果設定為 state 的新 array 。 -Here is a reference table of common array operations. When dealing with arrays inside React state, you will need to avoid the methods in the left column, and instead prefer the methods in the right column: +以下是常用的 array 運算參考表格。在 React state 內部處理 array 時,你會需要避免左邊欄位的方法,並替換成右邊欄位的方法: -| | avoid (mutates the array) | prefer (returns a new array) | +| | 避免(改變 array ) | 建議(回傳新 array ) | | --------- | ----------------------------------- | ------------------------------------------------------------------- | -| adding | `push`, `unshift` | `concat`, `[...arr]` spread syntax ([example](#adding-to-an-array)) | -| removing | `pop`, `shift`, `splice` | `filter`, `slice` ([example](#removing-from-an-array)) | -| replacing | `splice`, `arr[i] = ...` assignment | `map` ([example](#replacing-items-in-an-array)) | -| sorting | `reverse`, `sort` | copy the array first ([example](#making-other-changes-to-an-array)) | +| 增加 | `push`, `unshift` | `concat`, `[...arr]` spread 語法 ([example](#adding-to-an-array)) | +| 移除 | `pop`, `shift`, `splice` | `filter`, `slice` ([example](#removing-from-an-array)) | +| 更新 | `splice`, `arr[i] = ...` 賦值 | `map` ([example](#replacing-items-in-an-array)) | +| 排列 | `reverse`, `sort` | 先複製 array ([example](#making-other-changes-to-an-array)) | -Alternatively, you can [use Immer](#write-concise-update-logic-with-immer) which lets you use methods from both columns. +此外,你可以[使用 Immer](#write-concise-update-logic-with-immer),可讓你使用兩個欄位的方法。 -Unfortunately, [`slice`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) and [`splice`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) are named similarly but are very different: +不幸地,[`slice`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) 和 [`splice`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) 的名稱很像,但卻非常不一樣: -* `slice` lets you copy an array or a part of it. -* `splice` **mutates** the array (to insert or delete items). +* `slice` 讓你可以複製一個或局部的 array。 +* `splice` **改變** array (插入或刪除項目)。 -In React, you will be using `slice` (no `p`!) a lot more often because you don't want to mutate objects or arrays in state. [Updating Objects](/learn/updating-objects-in-state) explains what mutation is and why it's not recommended for state. +在 React 中會更常使用 `slice` (沒有 `p` !),因為你不希望在 state 中改變 object 或 array 。[更新 object ](/learn/updating-objects-in-state)解釋什麼是 mutation ,與為什麼不推薦用在 state 上。 -### Adding to an array {/*adding-to-an-array*/} +### 加入一個 Array {/*adding-to-an-array*/} -`push()` will mutate an array, which you don't want: +`push` 會改變 array ,這是你不想要的: @@ -88,7 +88,7 @@ button { margin-left: 5px; } -Instead, create a *new* array which contains the existing items *and* a new item at the end. There are multiple ways to do this, but the easiest one is to use the `...` [array spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_array_literals) syntax: +取而代之,建立一個包含目前項目、*與*新項目在後方的*新* array 。有許多方法可以完成,但最簡單的方法是使用 `...` [ array spread ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_array_literals)語法: ```js setArtists( // Replace the state @@ -99,7 +99,7 @@ setArtists( // Replace the state ); ``` -Now it works correctly: +現在它可以正常運作: @@ -141,7 +141,7 @@ button { margin-left: 5px; } -The array spread syntax also lets you prepend an item by placing it *before* the original `...artists`: +Array spread 語法讓你可以在原始的 `...artists` 前置入項目: ```js setArtists([ @@ -150,11 +150,11 @@ setArtists([ ]); ``` -In this way, spread can do the job of both `push()` by adding to the end of an array and `unshift()` by adding to the beginning of an array. Try it in the sandbox above! +在此方法中, spread 可以完成工作,透過 `push()` 加入在 array 的後方,與 `unshift()` 加入在 array 的開頭兩種。在上方的沙盒中試試看! -### Removing from an array {/*removing-from-an-array*/} +### 從 Array 中移除 {/*removing-from-an-array*/} -The easiest way to remove an item from an array is to *filter it out*. In other words, you will produce a new array that will not contain that item. To do this, use the `filter` method, for example: +從 array 中移除項目的最簡單方式是*將它篩選掉*;換句話說,你會產生一個新 array 且不包含它們。為此,使用 `filter` 方法,例如: @@ -198,7 +198,7 @@ export default function List() { -Click the "Delete" button a few times, and look at its click handler. +按下「 Delete」按鈕幾秒後,再看它的 click 處理器。 ```js setArtists( @@ -206,13 +206,14 @@ setArtists( ); ``` -Here, `artists.filter(a => a.id !== artist.id)` means "create an array that consists of those `artists` whose IDs are different from `artist.id`". In other words, each artist's "Delete" button will filter _that_ artist out of the array, and then request a re-render with the resulting array. Note that `filter` does not modify the original array. +這裡,`artists.filter(a => a.id !== artist.id)` 意思是「建立一個包含那些 `artists` ID 的 array ,與 `artist.id` 是不同的陣列」。換句話說,每個藝術家的「 Delete 」按鈕會把*那些*藝術家篩選到 array 外面,並使用該結果的 array 請求一次 re-render 。注意 `filter` 沒有修改原始的 array 。 -### Transforming an array {/*transforming-an-array*/} -If you want to change some or all items of the array, you can use `map()` to create a **new** array. The function you will pass to `map` can decide what to do with each item, based on its data or its index (or both). +### 轉換一個 Array {/*transforming-an-array*/} -In this example, an array holds coordinates of two circles and a square. When you press the button, it moves only the circles down by 50 pixels. It does this by producing a new array of data using `map()`: +如果你想改變 array 內的某些或全部的項目,你可以使用 `map()` 建立一個**新** array 。你傳入 `map` 的函數可以根據其資料或索引(或兩者),決定要對每個項目做的事情。 + +在此案例中, array 擁有兩個圓形及一個正方形的座標;按下按鈕時,只有圓形會往下移動 50 像素。這是使用 `map()` 產生一個新 array 資料所完成的: @@ -278,11 +279,11 @@ body { height: 300px; } -### Replacing items in an array {/*replacing-items-in-an-array*/} +### 更新 Array 中的項目 {/*replacing-items-in-an-array*/} -It is particularly common to want to replace one or more items in an array. Assignments like `arr[0] = 'bird'` are mutating the original array, so instead you'll want to use `map` for this as well. +這特別常見於想在 array 內更新一個或多個項目。像是 `arr[0] = 'bird'` 的賦值會改變原始 array ,因此使用 `map` 操作會更好。 -To replace an item, create a new array with `map`. Inside your `map` call, you will receive the item index as the second argument. Use it to decide whether to return the original item (the first argument) or something else: +為了更新項目,使用 `map` 建立新 array 。在內部呼叫 `map` ,你會收到項目索引作為第二個引數,使用它決定是否回傳原始 array (第一個引數),或其他: @@ -332,11 +333,11 @@ button { margin: 5px; } -### Inserting into an array {/*inserting-into-an-array*/} +### 在 Array 中插入 {/*inserting-into-an-array*/} -Sometimes, you may want to insert an item at a particular position that's neither at the beginning nor at the end. To do this, you can use the `...` array spread syntax together with the `slice()` method. The `slice()` method lets you cut a "slice" of the array. To insert an item, you will create an array that spreads the slice _before_ the insertion point, then the new item, and then the rest of the original array. +有時候,你也許會想將一個項目插入到特定的位置,而且不是在最後或最前面。為此,你可以一起使用 `...` array spread 語法與 `slice()` 方法完成。 `slice()` 方法讓你可以取下一個 array 的「切片」。為了插入項目,你會建立一個 array ,並在插入點*之前*展開該切片,接著是新項目,再來才是原始 array 的剩餘部分。 -In this example, the Insert button always inserts at the index `1`: +在此案例中,一個 Insert 按鈕總是會插入到索引 `1` : @@ -396,13 +397,13 @@ button { margin-left: 5px; } -### Making other changes to an array {/*making-other-changes-to-an-array*/} +### 對 Array 進行其他改變 {/*making-other-changes-to-an-array*/} -There are some things you can't do with the spread syntax and non-mutating methods like `map()` and `filter()` alone. For example, you may want to reverse or sort an array. The JavaScript `reverse()` and `sort()` methods are mutating the original array, so you can't use them directly. +有些東西無法使用 spread 語法或不改變的方法操作,例如 `map()` 及單獨的 `filter()` ;例如,你會想倒序或排序一個 array 。 Javascript 的 `reverse()` 及 `sort()` 方法會改變原始 array ,你無法直接使用它們。 -**However, you can copy the array first, and then make changes to it.** +**然而,你可以先複製 array 再改變它們**。 -For example: +例如: @@ -442,9 +443,9 @@ export default function List() { -Here, you use the `[...list]` spread syntax to create a copy of the original array first. Now that you have a copy, you can use mutating methods like `nextList.reverse()` or `nextList.sort()`, or even assign individual items with `nextList[0] = "something"`. +首先,你使用 spread 語法 `[...list]` 建立一個原始 array 的副本。目前你有副本,可以使用像是 `nextList.reverse()` 或 `nextList.sort()` 的改變方法,或甚至透過 `nextList[0] = "something"` 指定個別的項目。 -However, **even if you copy an array, you can't mutate existing items _inside_ of it directly.** This is because copying is shallow--the new array will contain the same items as the original one. So if you modify an object inside the copied array, you are mutating the existing state. For example, code like this is a problem. +然而,**儘管複製 array ,你還是無法直接改變其*內部*的現有項目**,這是因為複製是淺的——新 array 會包含原始的相同項目。因此,如果你要在複製的 array 內修改一個 object ,會需要改變目前的 state ;例如,類似的程式碼會是問題。 ```js const nextList = [...list]; @@ -452,15 +453,15 @@ nextList[0].seen = true; // Problem: mutates list[0] setList(nextList); ``` -Although `nextList` and `list` are two different arrays, **`nextList[0]` and `list[0]` point to the same object.** So by changing `nextList[0].seen`, you are also changing `list[0].seen`. This is a state mutation, which you should avoid! You can solve this issue in a similar way to [updating nested JavaScript objects](/learn/updating-objects-in-state#updating-a-nested-object)--by copying individual items you want to change instead of mutating them. Here's how. +雖然 `nextList` 和 `list` 是兩個不同的 array ,**但 `nextList[0]` 和 `list[0]` 會指向相同的 object** ,因此,改變 `nextList[0].seen` 也是改變 `list[0].seen` 。這是一個你該避免的 state mutation !你可以使用和[更新巢狀 Javadcript object ](/learn/updating-objects-in-state#updating-a-nested-object)相似的方式解決該問題——複製你想改變的個別項目,而非改變它們,以下是方法。 -## Updating objects inside arrays {/*updating-objects-inside-arrays*/} +## 在 Array 中更新 Object {/*updating-objects-inside-arrays*/} -Objects are not _really_ located "inside" arrays. They might appear to be "inside" in code, but each object in an array is a separate value, to which the array "points". This is why you need to be careful when changing nested fields like `list[0]`. Another person's artwork list may point to the same element of the array! +Object 不是*真的*在 array 的「內部」,它們可能出現在程式「內部」,但每個在 array 中的 object ,是 array 所指的一個分散的值。這是為什麼當你改變巢狀區域像 `list[0]` 時需要小心,另一個人物的藝術品清單也指向相同的元素 array ! -**When updating nested state, you need to create copies from the point where you want to update, and all the way up to the top level.** Let's see how this works. +**更新巢狀的 state 時,你需要從想更新地方建立一個副本,接著向上延伸到最上層**。讓我們看看如何操作。 -In this example, two separate artwork lists have the same initial state. They are supposed to be isolated, but because of a mutation, their state is accidentally shared, and checking a box in one list affects the other list: +此案例中,兩件藝術品清單擁有相同的初始 state ,它們應該是獨立的;但由於 mutation,它們意外共享 state ,且確認其中一個勾選框會影響到另一個清單: @@ -540,34 +541,34 @@ function ItemList({ artworks, onToggle }) { -The problem is in code like this: +問題出於這段程式: ```js const myNextList = [...myList]; const artwork = myNextList.find(a => a.id === artworkId); -artwork.seen = nextSeen; // Problem: mutates an existing item +artwork.seen = nextSeen; // 問題:改變既有的項目 setMyList(myNextList); ``` -Although the `myNextList` array itself is new, the *items themselves* are the same as in the original `myList` array. So changing `artwork.seen` changes the *original* artwork item. That artwork item is also in `yourList`, which causes the bug. Bugs like this can be difficult to think about, but thankfully they disappear if you avoid mutating state. +雖然 `myNextList` array 本身是新的,但*項目它們本身*和原始的 `myList` array 是相同的,因此改變 `artwork.seen` 即改變*原始的* artwork 項目。該 artwork 項目也在 `yourList` 中,這會引發錯誤,這種錯誤可能難以想像,但好在只要避免改變 state 就不會出現。 -**You can use `map` to substitute an old item with its updated version without mutation.** +**你可以使用 `map` 將舊項目替換成更新的版本,且沒有 mutation**。 ```js setMyList(myList.map(artwork => { if (artwork.id === artworkId) { - // Create a *new* object with changes + // 使用改變建立一個*新* object return { ...artwork, seen: nextSeen }; } else { - // No changes + // 沒有改變 return artwork; } })); ``` -Here, `...` is the object spread syntax used to [create a copy of an object.](/learn/updating-objects-in-state#copying-objects-with-the-spread-syntax) +這裡的 `...` 是 object spread 語法,用來[建立一個 object 的副本](/learn/updating-objects-in-state#copying-objects-with-the-spread-syntax) 。 -With this approach, none of the existing state items are being mutated, and the bug is fixed: +使用此方法,沒有任何現有的 state 項目會被改變,且會修正錯誤: @@ -590,10 +591,10 @@ export default function BucketList() { function handleToggleMyList(artworkId, nextSeen) { setMyList(myList.map(artwork => { if (artwork.id === artworkId) { - // Create a *new* object with changes + // 使用改變建立一個*新* object return { ...artwork, seen: nextSeen }; } else { - // No changes + // 沒有改變 return artwork; } })); @@ -602,10 +603,10 @@ export default function BucketList() { function handleToggleYourList(artworkId, nextSeen) { setYourList(yourList.map(artwork => { if (artwork.id === artworkId) { - // Create a *new* object with changes + // 使用改變建立一個*新* object return { ...artwork, seen: nextSeen }; } else { - // No changes + // 沒有改變 return artwork; } })); @@ -653,16 +654,16 @@ function ItemList({ artworks, onToggle }) { -In general, **you should only mutate objects that you have just created.** If you were inserting a *new* artwork, you could mutate it, but if you're dealing with something that's already in state, you need to make a copy. - -### Write concise update logic with Immer {/*write-concise-update-logic-with-immer*/} +通常**你應該只在剛建立 object 時改變它**,如果你插入一個*新的*藝術品,你可以改變它;但如果是處理一些已經在 state 內的東西,你需要建立一個副本。 -Updating nested arrays without mutation can get a little bit repetitive. [Just as with objects](/learn/updating-objects-in-state#write-concise-update-logic-with-immer): +### 使用 Immer 編寫簡潔的更新邏輯 {/*write-concise-update-logic-with-immer*/} -- Generally, you shouldn't need to update state more than a couple of levels deep. If your state objects are very deep, you might want to [restructure them differently](/learn/choosing-the-state-structure#avoid-deeply-nested-state) so that they are flat. -- If you don't want to change your state structure, you might prefer to use [Immer](https://github.com/immerjs/use-immer), which lets you write using the convenient but mutating syntax and takes care of producing the copies for you. +更新巢狀的 array 而沒有 mutation 可能會有一點重複,[就像使用 object ](/learn/updating-objects-in-state#write-concise-update-logic-with-immer): -Here is the Art Bucket List example rewritten with Immer: +- 通常,你不應該需要更新比兩層還深的 state ,如果 state 中的 object 非常的深層,你可能需要[重新解構它們](/learn/choosing-the-state-structure#avoid-deeply-nested-state),將它們變的平坦。 +- 如果你不想改變你的 state 結構,你可以選擇使用 [Immer](https://github.com/immerjs/use-immer) ,可以更方便地編寫,但使用變異語法,並會為你建立一個副本 + +以下是使用 Immer 重新編寫 Art Bucket List 的範例: @@ -763,7 +764,7 @@ function ItemList({ artworks, onToggle }) { -Note how with Immer, **mutation like `artwork.seen = nextSeen` is now okay:** +注意如何使用 Immer ,**目前像是 `artwork.seen = nextSeen` 的 mutation 是沒問題的:** ```js updateMyTodos(draft => { @@ -772,17 +773,17 @@ updateMyTodos(draft => { }); ``` -This is because you're not mutating the _original_ state, but you're mutating a special `draft` object provided by Immer. Similarly, you can apply mutating methods like `push()` and `pop()` to the content of the `draft`. +這是因為你沒有改變*原始的* state ,但改變 Immer 特別提供的 `draft` object ;同樣地,你可以應用改變方法在 `draft` 內,像是 `push()` 或 `pop()` 。 -Behind the scenes, Immer always constructs the next state from scratch according to the changes that you've done to the `draft`. This keeps your event handlers very concise without ever mutating state. +在幕後, Immer 總是根據你在 `draft` 所做的改變,從頭建構下一個 state 。這讓你的事件處理器非常簡潔,且不用改變 state 。 -- You can put arrays into state, but you can't change them. -- Instead of mutating an array, create a *new* version of it, and update the state to it. -- You can use the `[...arr, newItem]` array spread syntax to create arrays with new items. -- You can use `filter()` and `map()` to create new arrays with filtered or transformed items. -- You can use Immer to keep your code concise. +- 你可以將 array 放入 state 中,但不可以改變它們 +- 取代改變陣列,為它建立一個*新*版本,並將 state 更新在新版本上 +- 你可以使用 `[...arr, newItem]` array spread 語法建立 array 的新項目 +- 你可以使用 `filter()` 和 `map()` ,透過被篩選和被轉換的項目建立新陣列 +- 你可以使用 Immer 保持程式簡潔 @@ -790,9 +791,9 @@ Behind the scenes, Immer always constructs the next state from scratch according -#### Update an item in the shopping cart {/*update-an-item-in-the-shopping-cart*/} +#### 更新購物車內的項目 {/*update-an-item-in-the-shopping-cart*/} -Fill in the `handleIncreaseClick` logic so that pressing "+" increases the corresponding number: +補上 `handleIncreaseClick` 的邏輯,讓按下「 + 」時增加對應的數字: @@ -850,7 +851,7 @@ button { margin: 5px; } -You can use the `map` function to create a new array, and then use the `...` object spread syntax to create a copy of the changed object for the new array: +你可以使用 `map` 函數建立一個新 array ,接著使用 `...` object spread 語法,為新 array 建立一個被改變 object 的副本: @@ -917,9 +918,9 @@ button { margin: 5px; } -#### Remove an item from the shopping cart {/*remove-an-item-from-the-shopping-cart*/} +#### 移除購物車中的項目 {/*remove-an-item-from-the-shopping-cart*/} -This shopping cart has a working "+" button, but the "–" button doesn't do anything. You need to add an event handler to it so that pressing it decreases the `count` of the corresponding product. If you press "–" when the count is 1, the product should automatically get removed from the cart. Make sure it never shows 0. +此購物車有運作的「 + 」按鈕,但「 - 」按鈕不會做任何動作。你需要為它加入事件處理器,讓按下它減少對應的產品 `count` ;如果按下「 - 」時數量是 1 ,產品應該從購物車中自動移除。確認它不會永遠顯示 0 。 @@ -989,7 +990,7 @@ button { margin: 5px; } -You can first use `map` to produce a new array, and then `filter` to remove products with a `count` set to `0`: +首先,你可以使用 `map` 建立一個新 array ,接著使用 `filter` 篩選,移除 `count` 設定為 `0` 的產品: @@ -1078,9 +1079,9 @@ button { margin: 5px; } -#### Fix the mutations using non-mutative methods {/*fix-the-mutations-using-non-mutative-methods*/} +#### 使用非改變的方式修改 Mutation {/*fix-the-mutations-using-non-mutative-methods*/} -In this example, all of the event handlers in `App.js` use mutation. As a result, editing and deleting todos doesn't work. Rewrite `handleAddTodo`, `handleChangeTodo`, and `handleDeleteTodo` to use the non-mutative methods: +在此範例中,所有在 `App.js` 內的事件處理器使用 mutation ,因此無法執行編輯與刪除待辦。重新編寫 `handleAddTodo` 、 `handleChangeTodo` 與 `handleDeleteTodo` 以此用非改變的方式: @@ -1243,7 +1244,7 @@ ul, li { margin: 0; padding: 0; } -In `handleAddTodo`, you can use the array spread syntax. In `handleChangeTodo`, you can create a new array with `map`. In `handleDeleteTodo`, you can create a new array with `filter`. Now the list works correctly: +在 `handleAddTodo` 中,你可以使用 array spread 語法;在 `handleChangeTodo` 中,可以使用 `map` 建立新 array ;在 `handleDeleteTodo` 中,可以使用 `filter` 建立新 array 。現在清單可正常運作: @@ -1411,9 +1412,9 @@ ul, li { margin: 0; padding: 0; } -#### Fix the mutations using Immer {/*fix-the-mutations-using-immer*/} +#### 使用 Immer 修改 Mutation {/*fix-the-mutations-using-immer*/} -This is the same example as in the previous challenge. This time, fix the mutations by using Immer. For your convenience, `useImmer` is already imported, so you need to change the `todos` state variable to use it. +這是與上個挑戰相同的範例,這是使用 Immer 修改 mutation 。方便起見,已經匯入 `useImmer` ,因此你需要改變 `todos` 的 state 變數以使用它。 @@ -1595,7 +1596,7 @@ ul, li { margin: 0; padding: 0; } -With Immer, you can write code in the mutative fashion, as long as you're only mutating parts of the `draft` that Immer gives you. Here, all mutations are performed on the `draft` so the code works: +只要透過 Immer ,你就可以使用改變的方式編寫程式碼,並只會改變 Immer 提供的 `draft` 部分。全部的 mutation 會被呈現在 `draft` 中,因此程式會執行: @@ -1781,9 +1782,9 @@ ul, li { margin: 0; padding: 0; } -You can also mix and match the mutative and non-mutative approaches with Immer. +你也可以混合與搭配改變的、非改變的方式使用 Immer 。 -For example, in this version `handleAddTodo` is implemented by mutating the Immer `draft`, while `handleChangeTodo` and `handleDeleteTodo` use the non-mutative `map` and `filter` methods: +例如,在此版本中的 `handleAddTodo` 是由改變 Immer `draft` 完成的; `handleChangeTodo` 與 `handleDeleteTodo` 則使用非改變的 `map` 與 `filter` 方式: @@ -1966,7 +1967,7 @@ ul, li { margin: 0; padding: 0; } -With Immer, you can pick the style that feels the most natural for each separate case. +透過 Immer ,你可以為每個個別的案例挑選最自然的方式。 diff --git a/src/content/learn/updating-objects-in-state.md b/src/content/learn/updating-objects-in-state.md index 9289f2454..b6969223f 100644 --- a/src/content/learn/updating-objects-in-state.md +++ b/src/content/learn/updating-objects-in-state.md @@ -1,57 +1,57 @@ --- -title: Updating Objects in State +title: 更新 State 內的 Object --- -State can hold any kind of JavaScript value, including objects. But you shouldn't change objects that you hold in the React state directly. Instead, when you want to update an object, you need to create a new one (or make a copy of an existing one), and then set the state to use that copy. +State 可以儲存任何 Javascript 的值,包含 object ,但你不應該直接在 React 的 state 中修改 object ;取而代之,當想要更新一個 object 時,你需要建立一個新的(或是複製既有的),接著使用副本設定 state 。 -- How to correctly update an object in React state -- How to update a nested object without mutating it -- What immutability is, and how not to break it -- How to make object copying less repetitive with Immer +- 如何在 React state 中正確地更新 object +- 如何更新一個巢狀的 object 且不改變它 +- 什麼是 immutability 與如何避免破壞它 +- 如何使用 Immer 減少 object 的重複複製 -## What's a mutation? {/*whats-a-mutation*/} +## 什麼是 Mutation? {/*whats-a-mutation*/} -You can store any kind of JavaScript value in state. +你可以在 state 中儲存任何一種 Javascript 的值。 ```js const [x, setX] = useState(0); ``` -So far you've been working with numbers, strings, and booleans. These kinds of JavaScript values are "immutable", meaning unchangeable or "read-only". You can trigger a re-render to _replace_ a value: +至今你已經使用數字、字串以及布林值,這些 Javascript 的值是「不可改變的」,代表無法改變或「只能讀取」。你可以觸發一個 re-render 以*更新*值: ```js setX(5); ``` -The `x` state changed from `0` to `5`, but the _number `0` itself_ did not change. It's not possible to make any changes to the built-in primitive values like numbers, strings, and booleans in JavaScript. +`x` 的 state 從 `0` 被改成 `5` ,但未改變*數字 `0` 本身*;改變任何內建的 primitive value ,像是 Javascript 中的數字、字串與布林值是不可能的。 -Now consider an object in state: +現在想像 state 中的 object : ```js const [position, setPosition] = useState({ x: 0, y: 0 }); ``` -Technically, it is possible to change the contents of _the object itself_. **This is called a mutation:** +在技術上可以改變 *object 本身*的內容,**稱之為 mutation**: ```js position.x = 5; ``` -However, although objects in React state are technically mutable, you should treat them **as if** they were immutable--like numbers, booleans, and strings. Instead of mutating them, you should always replace them. +然而,雖然技術上在 React state 中的 object 是可改變的,但你仍需將它們**當成**是不可改變的——像數字、布林值與字串;比起改變它們,你應該總是更新它們。 -## Treat state as read-only {/*treat-state-as-read-only*/} +## 將 State 視為只能讀取 {/*treat-state-as-read-only*/} -In other words, you should **treat any JavaScript object that you put into state as read-only.** +換句話說,你應該**把任何放到 state 內的 Javascript object 當成只能讀取**。 -This example holds an object in state to represent the current pointer position. The red dot is supposed to move when you touch or move the cursor over the preview area. But the dot stays in the initial position: +此案例是在 state 內儲存一個 object ,以顯示目前游標的位置;紅點應該會在你觸摸或超過預覽區域時移動,但點停留在開始的位置: @@ -94,7 +94,7 @@ body { margin: 0; padding: 0; height: 250px; } -The problem is with this bit of code. +問題出於這段程式碼。 ```js onPointerMove={e => { @@ -103,9 +103,9 @@ onPointerMove={e => { }} ``` -This code modifies the object assigned to `position` from [the previous render.](/learn/state-as-a-snapshot#rendering-takes-a-snapshot-in-time) But without using the state setting function, React has no idea that object has changed. So React does not do anything in response. It's like trying to change the order after you've already eaten the meal. While mutating state can work in some cases, we don't recommend it. You should treat the state value you have access to in a render as read-only. +該程式修改 object 在[上一次 render ](/learn/state-as-a-snapshot#rendering-takes-a-snapshot-in-time)被分配到的 `position` ,但沒有使用 state setting 函數, React 不知道 object 已經被改變,因此 React 沒有任何回應,就像已經吃飽飯後還嘗試修改點餐內容。儘管改變的 state 可以在有些情況下運作,但我們不建議,你應該將在 render 中存取的 state 值視為只能讀取。 -To actually [trigger a re-render](/learn/state-as-a-snapshot#setting-state-triggers-renders) in this case, **create a *new* object and pass it to the state setting function:** +為了在此情境中實際[觸發一個 re-render](/learn/state-as-a-snapshot#setting-state-triggers-renders),**建立一個*新的* object 並將它傳給一個 state setting 函數**: ```js onPointerMove={e => { @@ -116,12 +116,12 @@ onPointerMove={e => { }} ``` -With `setPosition`, you're telling React: +使用 `setPosition` ,你告訴 React : -* Replace `position` with this new object -* And render this component again +* 使用此新物件更新 `position` +* 並再次 render 該 component -Notice how the red dot now follows your pointer when you touch or hover over the preview area: +當你的游標碰到或放過預覽區域時,注意紅點現在如何跟著它: @@ -168,16 +168,16 @@ body { margin: 0; padding: 0; height: 250px; } -#### Local mutation is fine {/*local-mutation-is-fine*/} +#### Local mutation 是可以的 {/*local-mutation-is-fine*/} -Code like this is a problem because it modifies an *existing* object in state: +類似這樣的程式碼會有問題,因為它修改 state 內的*既有* object : ```js position.x = e.clientX; position.y = e.clientY; ``` -But code like this is **absolutely fine** because you're mutating a fresh object you have *just created*: +但像這樣的程式是**完全沒有問題的**,因為你正在改變一個*剛建立*的新 object : ```js const nextPosition = {}; @@ -186,7 +186,7 @@ nextPosition.y = e.clientY; setPosition(nextPosition); ``` -In fact, it is completely equivalent to writing this: +事實上,它完全等同於這樣編寫: ```js setPosition({ @@ -195,15 +195,15 @@ setPosition({ }); ``` -Mutation is only a problem when you change *existing* objects that are already in state. Mutating an object you've just created is okay because *no other code references it yet.* Changing it isn't going to accidentally impact something that depends on it. This is called a "local mutation". You can even do local mutation [while rendering.](/learn/keeping-components-pure#local-mutation-your-components-little-secret) Very convenient and completely okay! +Mutation 只在改變*既有的* object 時才會是個問題。改變一個剛建立的是可以的,因為*還未有其他程式碼參考它*,改變它不會意外影響到某些依賴它的東西。這稱為「 local mutation 」,你甚至可以[在 render 期間](/learn/keeping-components-pure#local-mutation-your-components-little-secret)執行局部的改變,非常方便,而且完全是可以的! -## Copying objects with the spread syntax {/*copying-objects-with-the-spread-syntax*/} +## 使用 Spread 語法複製 Object {/*copying-objects-with-the-spread-syntax*/} -In the previous example, the `position` object is always created fresh from the current cursor position. But often, you will want to include *existing* data as a part of the new object you're creating. For example, you may want to update *only one* field in a form, but keep the previous values for all other fields. +在前一個案例中, `position` object 總是由目前的游標位置重新建立,但你經常會希望新建立的 object 包含*既有的*資料;例如,或許你只想在表單內*只*更新*一個*欄位,但仍保留其他所有欄位先前的值。 -These input fields don't work because the `onChange` handlers mutate the state: +以下的 input 欄位無法運作,因為 `onChange` 處理器改變 state : @@ -269,13 +269,14 @@ input { margin-left: 5px; margin-bottom: 5px; } -For example, this line mutates the state from a past render: +例如,這一行改變上次 render 的 state : ```js person.firstName = e.target.value; ``` -The reliable way to get the behavior you're looking for is to create a new object and pass it to `setPerson`. But here, you want to also **copy the existing data into it** because only one of the fields has changed: +取得預想動作最可靠的方式是建立一個新 object ,並將它傳給 `setPerson` ,但在此,你還想**將既有的資料複製到其中**,因為只有一個欄位被改變: + ```js setPerson({ @@ -285,18 +286,18 @@ setPerson({ }); ``` -You can use the `...` [object spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_object_literals) syntax so that you don't need to copy every property separately. +你可以使用[ object spread ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_object_literals)語法的 `...` ,如此一來,你就不需要個別複製每個 property。 ```js setPerson({ - ...person, // Copy the old fields - firstName: e.target.value // But override this one + ...person, // 複製舊的欄位 + firstName: e.target.value // 但覆寫它 }); ``` -Now the form works! +現在表單運作了! -Notice how you didn't declare a separate state variable for each input field. For large forms, keeping all data grouped in an object is very convenient--as long as you update it correctly! +留意你如何沒有為每個 input 欄位分別宣告 state 變數。對大型表單而言,將所有資料組織在同一個 object 是很方便的——只要你正確地更新! @@ -371,13 +372,13 @@ input { margin-left: 5px; margin-bottom: 5px; } -Note that the `...` spread syntax is "shallow"--it only copies things one level deep. This makes it fast, but it also means that if you want to update a nested property, you'll have to use it more than once. +注意 `...` spread 語法是「淺的」——它只複製一層深的東西;這使它快速,卻也代表如果你想更新一個巢狀的 property ,你會需要多次使用。 -#### Using a single event handler for multiple fields {/*using-a-single-event-handler-for-multiple-fields*/} +#### 為複數欄位使用單一事件處理器 {/*using-a-single-event-handler-for-multiple-fields*/} -You can also use the `[` and `]` braces inside your object definition to specify a property with dynamic name. Here is the same example, but with a single event handler instead of three different ones: +你也可以在 object 定義中使用中括號 `[` 和 `]` ,為一個 property 指定動態名稱。以下是一些範例,但使用單一事件處理器取代三個: @@ -441,13 +442,13 @@ input { margin-left: 5px; margin-bottom: 5px; } -Here, `e.target.name` refers to the `name` property given to the `` DOM element. +這裡, `e.target.name` 參考賦予 `` DOM 元素的 `name` property 。 -## Updating a nested object {/*updating-a-nested-object*/} +## 更新一個巢狀的 Object {/*updating-a-nested-object*/} -Consider a nested object structure like this: +想像一個像這樣的巢狀 object 結構: ```js const [person, setPerson] = useState({ @@ -460,13 +461,13 @@ const [person, setPerson] = useState({ }); ``` -If you wanted to update `person.artwork.city`, it's clear how to do it with mutation: +如果你想更新 `person.artwork.city` ,要怎麼改變它是很清楚的: ```js person.artwork.city = 'New Delhi'; ``` -But in React, you treat state as immutable! In order to change `city`, you would first need to produce the new `artwork` object (pre-populated with data from the previous one), and then produce the new `person` object which points at the new `artwork`: +但在 React 中,你將 state 當成是不可變的!為了改變 `city` ,首先你需要建立新的 `artwork` object (使用先前的資料預先填入),接著再建立可以指向新 `artwork` 的新 `person` object : ```js const nextArtwork = { ...person.artwork, city: 'New Delhi' }; @@ -474,19 +475,19 @@ const nextPerson = { ...person, artwork: nextArtwork }; setPerson(nextPerson); ``` -Or, written as a single function call: +或者,寫成單一函數呼叫: ```js setPerson({ - ...person, // Copy other fields - artwork: { // but replace the artwork - ...person.artwork, // with the same one - city: 'New Delhi' // but in New Delhi! + ...person, // 複製其他欄位 + artwork: { // 但更新藝術品 + ...person.artwork, // 使用相同的 + city: 'New Delhi' // 但是在 New Delhi! } }); ``` -This gets a bit wordy, but it works fine for many cases: +這會有點囉唆,但它可在許多情況下運作: @@ -596,9 +597,9 @@ img { width: 200px; height: 200px; } -#### Objects are not really nested {/*objects-are-not-really-nested*/} +#### Objects 並非真的是巢狀的 {/*objects-are-not-really-nested*/} -An object like this appears "nested" in code: +一個 object 會像這樣「套疊」出現在程式中: ```js let obj = { @@ -611,7 +612,7 @@ let obj = { }; ``` -However, "nesting" is an inaccurate way to think about how objects behave. When the code executes, there is no such thing as a "nested" object. You are really looking at two different objects: +然而,「套疊」是不精確思考 object 行為的方法。當程式執行時,並沒有所謂「套疊的」 object ,你實際上看到的是兩個不同的 object : ```js let obj1 = { @@ -626,7 +627,7 @@ let obj2 = { }; ``` -The `obj1` object is not "inside" `obj2`. For example, `obj3` could "point" at `obj1` too: +`ob1` object 並非在 `obj2` 「內部」;例如, `obj3` 也可以「指向」 `obj1` : ```js let obj1 = { @@ -646,13 +647,13 @@ let obj3 = { }; ``` -If you were to mutate `obj3.artwork.city`, it would affect both `obj2.artwork.city` and `obj1.city`. This is because `obj3.artwork`, `obj2.artwork`, and `obj1` are the same object. This is difficult to see when you think of objects as "nested". Instead, they are separate objects "pointing" at each other with properties. +如果你要改變 `obj3.artwork.city` ,它會影響 `obj.artwork.city` 及 `obj1.city` 兩者,這是因為 `obj3.artwork` 、 `obj2.artwork` 、 `obj1` 是相同的物件。當你將物件視為「套疊」時,很難觀察看到這一點;取而代之,它們是分散的 object ,並透過 property 「指向」彼此。 -### Write concise update logic with Immer {/*write-concise-update-logic-with-immer*/} +### 使用 Immer 編寫簡潔的更新邏輯 {/*write-concise-update-logic-with-immer*/} -If your state is deeply nested, you might want to consider [flattening it.](/learn/choosing-the-state-structure#avoid-deeply-nested-state) But, if you don't want to change your state structure, you might prefer a shortcut to nested spreads. [Immer](https://github.com/immerjs/use-immer) is a popular library that lets you write using the convenient but mutating syntax and takes care of producing the copies for you. With Immer, the code you write looks like you are "breaking the rules" and mutating an object: +如果你的 state 套疊得很深,你可能會考慮[將它攤平](/learn/choosing-the-state-structure#avoid-deeply-nested-state);但假如不想改變 state 的結構,你也許會偏好一個展開套疊的捷徑。 [Immer](https://github.com/immerjs/use-immer) 是一個知名的函數庫,讓你方便編寫卻又可以使用改變的語法,且為你產生一份副本;使用 Immer ,你所編寫的程式看起來像「打破規則」與改變一個 object : ```js updatePerson(draft => { @@ -660,22 +661,22 @@ updatePerson(draft => { }); ``` -But unlike a regular mutation, it doesn't overwrite the past state! +但不像普通的改變,它不會覆蓋過去的 state ! -#### How does Immer work? {/*how-does-immer-work*/} +#### Immer 如何執行? {/*how-does-immer-work*/} -The `draft` provided by Immer is a special type of object, called a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), that "records" what you do with it. This is why you can mutate it freely as much as you like! Under the hood, Immer figures out which parts of the `draft` have been changed, and produces a completely new object that contains your edits. +Immer 提供的 `draft` 是 obkect 的特別型態,稱為 [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) ,「紀錄」任何你做的事情。這是為什麼你可以依照喜好自由地改變它!其原理是 Immer 指出哪些部分的 `draft` 已經改變,藉此產生一個新 object ,且包含你的編輯。 -To try Immer: +嘗試使用 Immer : -1. Run `npm install use-immer` to add Immer as a dependency -2. Then replace `import { useState } from 'react'` with `import { useImmer } from 'use-immer'` +1. 執行 `npm install use-immer` 將 Immer 加入 dependency +2. 將 `import { useState } from 'react'` 更新成 `import { useImmer } from 'use-immer'` -Here is the above example converted to Immer: +這是將上述案例轉換成 Immer : @@ -788,33 +789,34 @@ img { width: 200px; height: 200px; } -Notice how much more concise the event handlers have become. You can mix and match `useState` and `useImmer` in a single component as much as you like. Immer is a great way to keep the update handlers concise, especially if there's nesting in your state, and copying objects leads to repetitive code. +注意事件處理器如何變得更加簡潔。你可以依照喜好在單一 component 內混合及搭配 `useState` 和 `useImmer` 。 Immer 是保持更新處理器簡潔的好方法,特別是如果在 state 中有套疊、以及複製 object 導致程式碼重複的時候。 -#### Why is mutating state not recommended in React? {/*why-is-mutating-state-not-recommended-in-react*/} +#### 為什麼在 React 中不推薦改變的 state ? {/*why-is-mutating-state-not-recommended-in-react*/} -There are a few reasons: +有幾個原因: -* **Debugging:** If you use `console.log` and don't mutate state, your past logs won't get clobbered by the more recent state changes. So you can clearly see how state has changed between renders. -* **Optimizations:** Common React [optimization strategies](/reference/react/memo) rely on skipping work if previous props or state are the same as the next ones. If you never mutate state, it is very fast to check whether there were any changes. If `prevObj === obj`, you can be sure that nothing could have changed inside of it. -* **New Features:** The new React features we're building rely on state being [treated like a snapshot.](/learn/state-as-a-snapshot) If you're mutating past versions of state, that may prevent you from using the new features. -* **Requirement Changes:** Some application features, like implementing Undo/Redo, showing a history of changes, or letting the user reset a form to earlier values, are easier to do when nothing is mutated. This is because you can keep past copies of state in memory, and reuse them when appropriate. If you start with a mutative approach, features like this can be difficult to add later on. -* **Simpler Implementation:** Because React does not rely on mutation, it does not need to do anything special with your objects. It does not need to hijack their properties, always wrap them into Proxies, or do other work at initialization as many "reactive" solutions do. This is also why React lets you put any object into state--no matter how large--without additional performance or correctness pitfalls. +* **除錯:** 如果使用 `console.log` 且不改變 state ,過去的紀錄不會被更多後來的 state 改變所破壞,因此你可以清楚地看見 state 在每次 render 如何改變。 +* **最佳化:** 如果上一個 props 或 state 與下一個相同,通常 React 的[最佳化策略](/reference/react/memo)仰賴省略執行。如果從未變異 state ,確認是否有任何修改會非常快速。如果 `prevObj === obj` ,可以確定內部沒有東西改變。 +* **新功能:** 我們打造的 React 新功能仰賴於 state [被視為快照](/learn/state-as-a-snapshot);如果改變過去的 state 版本,可能會阻止你使用新功能 +* **請求變更:** 有些應用程式的功能像是取消/重做、顯示歷史修改、或讓使用者重新設定稍早之前的值,在沒有東西被改變時很容易,這是因為可以在記憶體中保留過去的 state ,並在適當時機重新使用它們;如果一開始就使用一個改變的方式,後續難以加上這些功能 +* **更簡單的實作:** 因為 React 不仰賴改變,它不需要對你的 object 做任何特別的事情;不需要劫持它們的 property 、總是把它們包進 proxy 內、或其他在初始化時執行許多「反應的」解決辦法。這也是為什麼 React 讓你可以將任何 object 放入 state 中——不管多大——沒有多餘的效能與正確性的陷阱。 -In practice, you can often "get away" with mutating state in React, but we strongly advise you not to do that so that you can use new React features developed with this approach in mind. Future contributors and perhaps even your future self will thank you! +在實務中,你可以經常在 React 中改變 state 而不會出錯,但我們強烈建議你不要這樣做,以便你可以使用 React 新開發的功能。未來的貢獻者、甚至未來的你都會非常感謝你! -* Treat all state in React as immutable. -* When you store objects in state, mutating them will not trigger renders and will change the state in previous render "snapshots". -* Instead of mutating an object, create a *new* version of it, and trigger a re-render by setting state to it. -* You can use the `{...obj, something: 'newValue'}` object spread syntax to create copies of objects. -* Spread syntax is shallow: it only copies one level deep. -* To update a nested object, you need to create copies all the way up from the place you're updating. -* To reduce repetitive copying code, use Immer. +* 將 React 中的所有 state 視為不可改變的 +* 當你在 state 中儲存 object 時,改變它們不會觸發 render ,且會改變 state 在上次 render 的「快照」 +* 比起改變一個 object ,為它建立一個*新*版本,並藉由為它設定 state 觸發 re-render +* 你可以使用 `{...obj, something: 'newValue'}` 的 object spread 語法建立一個 object 的副本 +* Spread 語法是淺的:它只會複製一層 +* 為了更新巢狀的 object ,你需要在從更新的地方一路向上建立副本 +* 使用 Immer 減少重複複製的程式, + @@ -822,11 +824,11 @@ In practice, you can often "get away" with mutating state in React, but we stron -#### Fix incorrect state updates {/*fix-incorrect-state-updates*/} +#### 修改不正確的 State 更新 {/*fix-incorrect-state-updates*/} -This form has a few bugs. Click the button that increases the score a few times. Notice that it does not increase. Then edit the first name, and notice that the score has suddenly "caught up" with your changes. Finally, edit the last name, and notice that the score has disappeared completely. +該表單存在一些錯誤。點擊幾次按鈕增加分數,留意它未增加;接著編輯名字,注意分數突然因為你的改變而追趕上來;最後編輯姓氏時,注意分數完全消失。 -Your task is to fix all of these bugs. As you fix them, explain why each of them happens. +你的工作是修改全部的錯誤,當你修改它們時,解釋它們為何發生。 @@ -894,7 +896,7 @@ input { margin-left: 5px; margin-bottom: 5px; } -Here is a version with both bugs fixed: +這是解決兩個錯誤的方式: @@ -964,23 +966,23 @@ input { margin-left: 5px; margin-bottom: 5px; } -The problem with `handlePlusClick` was that it mutated the `player` object. As a result, React did not know that there's a reason to re-render, and did not update the score on the screen. This is why, when you edited the first name, the state got updated, triggering a re-render which _also_ updated the score on the screen. +`handlePlusClick` 的問題是改變 `player` object ,因此, React 不知道這是 re-render 的理由,而未將畫面上的分數更新。這是為什麼當編輯名字時, state 有更新,觸發 re-render *同時*更新畫面上的分數。 -The problem with `handleLastNameChange` was that it did not copy the existing `...player` fields into the new object. This is why the score got lost after you edited the last name. +`hanleLastNameChange` 的問題是未複製既有的 `...player` 欄位到新的 object 中,這是為什麼在編輯姓氏後分數消失。 -#### Find and fix the mutation {/*find-and-fix-the-mutation*/} +#### 找出並修改 Mutation {/*find-and-fix-the-mutation*/} -There is a draggable box on a static background. You can change the box's color using the select input. +有個可拖曳的盒子在靜止的背景中,你可以使用選單改變盒子的顏色。 -But there is a bug. If you move the box first, and then change its color, the background (which isn't supposed to move!) will "jump" to the box position. But this should not happen: the `Background`'s `position` prop is set to `initialPosition`, which is `{ x: 0, y: 0 }`. Why is the background moving after the color change? +但這裡存在錯誤,假如你先移動盒子,接著更換它的眼色,背景(未預期它會移動的!)會「跳」到盒子的位置,但這不應該發生: `Background` 的 `position` prop 被設定成 `initialPosition` ,其為 `{ x: 0, y:0 }` 。為什麼背景會在更換顏色時移動呢? -Find the bug and fix it. +找出錯誤並且解決它。 -If something unexpected changes, there is a mutation. Find the mutation in `App.js` and fix it. +如果有些東西意外改變,那就存在 mutation 。找出在 `App.js` 內的 mutation 並解決它。 @@ -1130,9 +1132,9 @@ select { margin-bottom: 10px; } -The problem was in the mutation inside `handleMove`. It mutated `shape.position`, but that's the same object that `initialPosition` points at. This is why both the shape and the background move. (It's a mutation, so the change doesn't reflect on the screen until an unrelated update--the color change--triggers a re-render.) +問題出在 `handleMove` 內部的 mutation ,它改變 `shape.position` ,但又與 `initialPosition` 指著相同的 object ,這是為什麼兩個形狀與背景都會移動。(這是 mutation ,因此改變不會反映在畫面上,直到有非相關的更新——顏色改變——觸發 re-render 。) -The fix is to remove the mutation from `handleMove`, and use the spread syntax to copy the shape. Note that `+=` is a mutation, so you need to rewrite it to use a regular `+` operation. +解決辦法是從 `handleMove` 中移除 mutation ,並使用 spread 語法複製形狀。注意 `+=` 是 mutation ,因此你需要使用一個普通運算的 `+` 重寫它。 @@ -1285,9 +1287,9 @@ select { margin-bottom: 10px; } -#### Update an object with Immer {/*update-an-object-with-immer*/} +#### 使用 Immer 更新 Object {/*update-an-object-with-immer*/} -This is the same buggy example as in the previous challenge. This time, fix the mutation by using Immer. For your convenience, `useImmer` is already imported, so you need to change the `shape` state variable to use it. +這是與上一個挑戰相同的錯誤範例,這次使用 Immer 解決 mutation 。方便起見,已經匯入 `useImmer` ,因此你需要改變 `shape` state 變數以使用它。 @@ -1454,7 +1456,7 @@ select { margin-bottom: 10px; } -This is the solution rewritten with Immer. Notice how the event handlers are written in a mutating fashion, but the bug does not occur. This is because under the hood, Immer never mutates the existing objects. +這是使用 Immer 重寫的解法。留意事件處理器由改變的方式編寫,但錯誤並未發生,這是因為 Immer 未改變既有 object 的原理。