# React Hook (ES6 UP)
###### tags: `React`
Hook 是 React 16.8 中增加的新功能。它讓你不必寫 class 就能使用 state 以及其他 React 的功能。
原文:
https://zh-hant.reactjs.org/docs/hooks-state.html
## useState
### hook
```javascript=
import React, { useState } from 'react';
function Example() {
// 宣告一個新的 state 變數,我們稱作為「count」。 const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
```
### Class
```javascript=
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
```
State 從 { count: 0 } 開始,當使用者點擊按鈕時,我們藉由呼叫 this.setState() 增加 state.count。在這ㄧ整頁,我們會使用這個 class 中的片段來說明。
### 詳解
```javascript=
import React, { useState } from 'react'; 2:
function Example() {
const [count, setCount] = useState(0); 5:
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>10: Click me
</button>
</div>
);
}
```
* 第一行:我們從 React 引入 useState Hook。它讓我們可以在 function component 保留 local state。
* 第四行:在 Example component 裡,我們呼叫 useState Hook 宣告了一個新的 state 變數。並回傳了一對由我們命名的值。我們將代表點擊數的變數命名為 count。我們將起始值設為 0 並傳入 useState 當作唯一參數。第二個回傳的值是個可以更新 count 的 function,所以我們命名為 setCount。
* 第九行:當使用者點擊,我們就呼叫 setCount 並傳入新的值。然後 React 就會 re-render Example component,並傳入新的 count 值。
---
## useEffect
如果你熟悉 React class 的生命週期方法,你可以把 useEffect 視為 componentDidMount,componentDidUpdate 和 componentWillUnmount 的組合。
### 無需清除的 Effect
### class
這是一個 React class component 的計數器,它在 React 對 DOM 進行變更後立即更新網頁標題:
```javascript=
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() { document.title = `You clicked ${this.state.count} times`; } componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; }
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
```
注意我們如何必須在 class 中複製這兩個生命週期方法之間的程式碼。
這是因為在許多情況下,我們希望執行相同的 side effect,無論 component 是剛被 mount 還是已經被更新。概念上,我們希望它在每次 render 之後發生 — 但是 React class component 沒有這樣的方法。我們可以提取一個單獨的方法,但我們仍然需要在兩個地方呼叫它。
### hook
現在來看看我們可以如何使用 useEffect Hook 做同樣的事情。
```javascript=
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => { document.title = `You clicked ${count} times`; });
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
```
**useEffect 有什麼作用? **透過使用這個 Hook,你告訴 React 你的 component 需要在 render 後做一些事情。React 將記住你傳遞的 function(我們將其稱為「effect」),並在執行 DOM 更新之後呼叫它。 在這個 effect 中,我們設定了網頁的標題,但我們也可以執行資料提取或呼叫其他命令式 API。
**為什麼在 component 內部呼叫 useEffect?** 在 component 中放置 useEffect 讓我們可以直接從 effect 中存取 count state 變數(或任何 props)。我們不需要特殊的 API 來讀取它 — 它已經在 function 範圍內了。 Hook 擁抱 JavaScript closure,並避免在 JavaScript 已經提供解決方案的情況下引入 React 特定的 API。
**每次 render 後都會執行 useEffect 嗎?** 是的!預設情況下,它在第一個 render 和隨後每一個更新之後執行。(我們稍後會談到如何自定義。)你可能會發現把 effect 想成發生在「render 之後」更為容易,而不是考慮「mount」和「更新」。 React 保證 DOM 在執行 effect 時已被更新。
### 詳解
```javascript=
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
}
```
我們宣告 count state 變數,然後告訴 React 我們需要使用一個 effect。我們將一個 function 傳入給 useEffect Hook。我們傳入的這個 function 就是我們的 effect。在 effect 內部,我們使用瀏覽器 API document.title 設定了網頁標題。我們可以讀取 effect 中最新的 count,因為它在我們 function 的範圍內。當 React render 我們的 component 時,它會記住我們使用的 effect,然後在更新 DOM 後執行我們的 effect。每次 render 都是這樣,包括第一次。
有經驗的 JavaScript 開發人員可能會注意到,傳遞給 useEffect 的 function 在每次 render 時都會有所不同。這是刻意的。實際上,這是讓我們可以從 effect 內部讀取 count 數值,且不必擔心數值過時的原因。每次重新 render 時,我們都會安排一個 different effect 來替代上一個。在某種程度上,這使 effect 的行為更像是 render 結果的一部分 — 每個 effect 都「屬於」特定的 render。我們將在本頁稍後更清楚地看到為什麼這很有用。
### 需要清除的 Effect
先前,我們理解了怎樣表達不需要任何清除的 side effect。但是,有些 effect 需要。例如,我們可能想要設定對某些外部資料來源的 subscription。在這種情況下,請務必進行清除,以免造成 memory leak!讓我們比較一下我們可以如何用 class 和 Hook 做到這一點
### class
在 React class 中,你通常會在 componentDidMount 中設定一個 subscription,然後在 componentWillUnmount 中把它清除。例如,假設我們有一個 ChatAPI module 可讓我們訂閱朋友的線上狀態。我們可能會這樣用 class 來訂閱和顯示該狀態:
```javascript=
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); }
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
```
請注意 componentDidMount 和 componentWillUnmount 需要如何相互呼應。生命週期方法迫使我們拆開這個邏輯,即使概念上它們的程式碼都與同一個 effect 相關。
### hook
讓我們看看如何使用 Hook 撰寫這個 component。
你可能會認為我們需要一個單獨的 effect 來執行清除。但是新增和移除 subscription 的程式碼緊密相關,因此 useEffect 的設計在將其保持在一起。如果你的 effect 回傳了一個 function,React 將在需要清除時執行它:
```javascript=
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); // 指定如何在這個 effect 之後執行清除: return function cleanup() { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
```
**為什麼我們從 effect 中回傳一個 function?** 這是 effect 的可選清除機制。每個 effect 都可以回傳一個會在它之後執行清除的 function。這使我們可以把新增和移除 subscription 的邏輯彼此保持靠近。它們都屬於同一個 effect!
**React 到底什麼時候會清除 effect?** 在 component unmount 時,React 會執行清除。但是,正如我們之前看到的,effect 會在每個 render 中執行,而不僅僅是一次。這是為什麼 React 還可以在下次執行 effect 之前清除前一個 render 的 effect 的原因。我們會在下面討論為什麼這有助於避免 bug 以及如果出現效能問題,如何選擇退出此行為。
### 變數有更動才執行Effect (忽略 不必要的Effect來優化效能)
如果在重新 render 之間某些值沒有改變,你可以讓 React 忽略 effect。為此,請將 array 作為可選的第二個參數傳遞給 useEffect:
```javascript=
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 僅在計數更改時才重新執行 effect
```
在上面的範例中,我們將 [count] 作為第二個參數傳遞。這是什麼意思?如果 count 是 5,然後我們的 component 重新 render,count 仍然等於 5,React 將比對前一個 render 的 [5] 和下一個 render 的 [5]。因為 array 中的每一項都相同(5 === 5),所以 React 將忽略這個 effect。那就是我們的最佳化。
當我們 render 時將 count 更新為 6,React 將比對前一個 render 的 array [5] 與下一個 render 的 array [6]。這次,React 將重新執行 effect,因為 5 !== 6。如果 array 中有多個項目,即使其中一項不同,React 也會重新執行 effect。
---
React 怎麼知道哪個 useState 呼叫對應於 re-render 之間的哪個 state 變數?React 如何在每次更新中「匹配」上一個和下一個 effect?
# Hook 的規則
Hook 是 JavaScript function,當你使用它們時需要遵守兩個規則。我們提供了一個 linter plugin 來自動化地實行這些規則:
1. 只在最上層呼叫 Hook
不要在迴圈、條件式或是巢狀的 function 內呼叫 Hook。
2. 只在 React Function 中呼叫 Hook
別在一般的 JavaScript function 中呼叫 Hook。相反的,你可以:
✅ 在 React function component 中呼叫 Hook。
✅ 在自定義的 Hook 中呼叫 Hook。(我們將會在下一頁了解它們)。
透過遵循這些規則,你確保了在 component 中所有的 stateful 邏輯在其原始碼中可以清楚地被看見。
如我們先前所學到的,我們可以在單一的 component 中使用多個 State 或 Effect Hook:
```javascript=
function Form() {
// 1. 使用 name state 變數
const [name, setName] = useState('Mary');
// 2. 使用一個 effect 來保存表單
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
// 3. 使用 surname state 變數
const [surname, setSurname] = useState('Poppins');
// 4. 使用一個 effect 來更新標題
useEffect(function updateTitle() {
document.title = name + ' ' + surname;
});
// ...
}
```
所以 React 是如何知道哪個 state 要對應到哪個 useState 的呼叫?答案是 React 仰賴於 Hook 被呼叫的順序。我們的範例能執行是因為在每一次的 render 中 Hook 都是依照一樣的順序被呼叫:
```javascript=
// ------------
// 第一次 render
// ------------
useState('Mary') // 1. 用 'Mary' 來初始化 name state 變數
useEffect(persistForm) // 2. 增加一個 effect 來保存表單
useState('Poppins') // 3. 用 'Poppins' 來初始化 surname state 變數
useEffect(updateTitle) // 4. 增加一個 effect 來更新標題
// -------------
// 第二次 render
// -------------
useState('Mary') // 1. 讀取 name state 變數 (參數被忽略了)
useEffect(persistForm) // 2. 替換了用來保存表單的 effect
useState('Poppins') // 3. 讀取 surname state 變數 (參數被忽略了)
useEffect(updateTitle) // 4. 替換了用來更新標題的 effect
// ...
```
只要 Hook 在 render 時被呼叫的順序是一致的,React 可以將一些 local state 和它們一一聯繫在一起。
---
# useReducer
你可能有一個複雜的 component,它包含許多以一個特殊目的(ad-hoc)方式來管理的 local state。useState 沒辦法讓更新邏輯集中化,所以你可能更傾向將其寫為 Redux 的 reducer:
```javascript=
function todosReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, {
text: action.text,
completed: false
}];
// ... 其他 action ...
default:
return state;
}
}
```
Reducer 是非常方便於獨立測試的,而且可以表達複雜的更新邏輯。如果有需要的話,你可以將它們拆成更小的 reducer。然而,你可能也喜歡使用 React local state 的好處,或者你不想要安裝其他的函式庫。
那麼,如果我們可以撰寫一個 useReducer Hook,讓我們用 reducer 管理 component 的 local state 呢? 它的簡化版本看起來如下:
```javascript=
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
```
現在我們在其他的 component 使用它,讓 reducer 驅動它的 state 管理:
```javascript=
function Todos() {
const [todos, dispatch] = useReducer(todosReducer, []);
function handleAddClick(text) {
dispatch({ type: 'add', text });
}
// ...
}
```
在複雜的 component 中使用 reducer 管理 local state 的需求很常見,我們已經將 useReducer Hook 內建在 React 中。你可以在 Hooks API 參考中找到它與其他內建的 Hook。
---
# Hooks API
https://zh-hant.reactjs.org/docs/hooks-reference.html#usereducer