原文連結:Github
React Hooks (Proposal)
在 React v16.7.0 alpha 版本里,提出了一個新的 Feature Proposal :Hooks ,對社群以及以後前端發展所帶來的影響是巨大的。
學習 Hooks 的知識需要對 React 生態有較深入的理解
What is the Hooks ?
Hooks 是 React 內部元件中的一系列特殊函式,直觀帶來的改變是引入state、生命週期函式、或者其他 React 功能,無需使用 classes 編寫元件(類語法帶來的問題有很多),背後為前端帶來更深入更普及的 functional programming
思想。
引入 Hooks 的動機
React 官方闡明瞭引入 Hooks 的動機,Hooks 出現前,我們編寫 React 元件 會經常遇到的問題:
It’s hard to reuse stateful logic between components
- React 沒有提供官方方案去解決
元件之間共享複用有狀態邏輯
,元件間邏輯的複用和資料傳遞就變得十分困難(必須一層一層往下傳),所以我們使用render props
和higher-order components
來解決複用邏輯的同時引來了新的問題,一些無關 UI 的 wrapper 元件越來越多,巢狀元件越來越深,形成wrapper hell
,雖然 React devTools 有過濾器來幫助我們更容易地除錯。 - 使用 Hooks 可以在不改變元件層次結構的情況下複用有狀態邏輯。可以利用 custom hooks,複用包含狀態的邏輯,這些邏輯不再出現在元件樹中,而是形成一個獨立、可測試的單元,但仍然響應 React 在渲染之間的變化;社群之間分享 自定義hooks 更容易,hooks 就像外掛一樣。
- React 沒有提供官方方案去解決
Complex components become hard to understand
- 隨著專案深入,我們逐漸會編寫越來越複雜的邏輯在元件中,這導致了再生命週期函式內編寫的邏輯非常臃腫,例如
新增監聽器
,我們需要在componentDidMount
與componentWillUnmount
中分別編寫新增與刪除監聽器的邏輯,而一般在componentDidMount
中,我們也會編寫請求資料
的邏輯。各種功能不相關聯的邏輯寫在一起,而且相同功能的邏輯散落在不同函式內,這帶來許多隱患以及除錯上的困難 - 使用 Hooks 可以 將相關聯的邏輯code由元件拆分出來成更簡單直觀的函式(例如訂閱事件、請求資料)
- 隨著專案深入,我們逐漸會編寫越來越複雜的邏輯在元件中,這導致了再生命週期函式內編寫的邏輯非常臃腫,例如
Classes confuse both people and machines
- React 官方認為 JS 的 Class 語法的學習成本很高,使用類語法,要必須清楚
this
在 JS 的工作方式,例如我們需要 繫結事件處理程式 (以何種方式繫結這裡不是重點,個人推薦箭頭函式形式);另外一些重要實踐上,使用 Class 語法也帶來諸多問題,詳細參閱 classes-confuse-both-people-and-machines) - 使用 Hooks 可以 在無需編寫 Class 語法的情況下 引入state、生命週期函式、或者其他 React 功能
- React 官方認為 JS 的 Class 語法的學習成本很高,使用類語法,要必須清楚
實際上引入 Hooks 並不會給現有的程式碼帶來問題
- 完全可選(將使用 Hooks 的選擇權交給開發者)
- 向後相容(不會有任何破壞性更改)
- 在可預見的未來內,不會從 React 中刪除 類語法
- Hooks 並沒有顛覆之前的 React 概念。相反,帶來更直觀的 API 實現相同的功能
編寫 Hooks
目前主要的 Hooks :
- State hooks
- Effect hooks
- Custom hooks (自定義 hooks 用來複用包含狀態的邏輯)
useState
import { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
複製程式碼
使用 state hooks
在 function components
中可以像上面程式碼這樣,等同於Class語法的程式碼就不貼了。
值得一提的是,在 Hooks 出現之前,我們通常叫這樣形式的元件為
stateless components
orstateless function components
,但現在,有了 Hooks ,我們可以在這類元件中使用 state,所以改稱function components
。
- useState 的引數是 我們需要定義的 state 名的初始值(不必像以前一樣,state 必須為 Object,如果我們想要建立兩個state,就呼叫兩次 useState)
- 返回值是包含兩個值的陣列,兩個值分別為
當前狀態
和更新它的函式
。(這裡我們使用array destructuring
的方式將值取出來。)
建立多個 state 就像這樣
function ExampleWithManyStates() {
// Declare multiple state variables!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
複製程式碼
與 this.setState
不同,更新狀態總是替換它而不是合併它(也解決了很多之前合併帶來的問題)
Functional updates
如果新的 state 值是依賴上一個 state 值來計算的,我們可以給 setState
傳遞一個函式引數,這個函式的引數為上一個 state 的值,返回值是更新後的 state 值,例如:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
</>
);
}
複製程式碼
所以如果需要更新的 state 值為 Object,我們應該使用 object spread syntax
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
複製程式碼
延遲初始化 state
如果初始化的值是需要大量計算得到的結果,可以使用函式代替,此函式只會在初始化階段執行
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
複製程式碼
useEffect
Effect 其實就是 請求資料,操作DOM,以及訂閱事件等一系列 副作用/效果
而 useEffect 則是 之前 componentDidMount
,componentDidUpdate
和componentWillUnmount
的結合
React元件中有兩種常見的 Effect:需要清理和不需要清理的 Effect
不需要清理的 Effect
import { 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
- useEffect 寫在 函式內部是為了直接訪問到state值,利用了閉包的性質,不需要額外 API
需要清理的 Effect
import { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
複製程式碼
需要單獨的 API 來執行清理邏輯。因為新增和刪除訂閱的邏輯是相關的,useEffect 旨在將其保持在一起。 如果 useEffect 返回一個函式,React 將在清理時執行它
清理的時機是
當元件解除安裝時
,但,useEffect 會在每次渲染後執行而不僅僅是一次, 這就是 React 在下次執行 useEffect 之前還清除前一個 useEffect 的原因;Using the Effect Hook – React
如果要減少 useEffect 內並不是每次渲染都必要的邏輯,可以:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
複製程式碼
React 會比較兩次渲染的 count 值,如果一樣,就會跳過這次 useEffect
Custom Hooks
我們可以封裝在多個元件可重用的包含狀態的邏輯,例如
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
複製程式碼
useFriendStatus
就是一個我們寫好的複用邏輯函式,供其他元件呼叫。
多個元件使用 相同自定義Hooks,它們的狀態和效果是 獨立隔離的,僅僅是邏輯的複用。因為本質是
呼叫 Custom Hooks
是呼叫useState
和useEffect
,它們在一個元件呼叫很多次,彼此產生的狀態也是完全獨立的。
詳細參見文件:Writing Custom Hooks – React
使用 Hooks 的規則:
務必遵守的規則: Rules of Hooks – React
Hooks API: Hooks API Reference – React
Conclusion
React Hooks 帶來的邊際效應可以說是巨大的,希望更加完善之後,可以看到開啟新窗的前端。
啟發 Hooks 的產生:Hooks FAQ – React
關於 Hooks 的討論:RFC: React Hooks by sebmarkbage · Pull Request #68 · reactjs/rfcs · GitHub
有趣的是,Vue的作者也很快建立了在 Vue 實驗 Hooks 的repo:GitHub - yyx990803/vue-hooks: Experimental React hooks implementation in Vue