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
,雖然ofollow,noindex">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