Hooks are an upcoming feature that lets you use state and other React features without writing a class. They’re currently in React v16.7.0-alpha.
What
Hooks 是 React 函數組件內的一類特殊函數,使開發者能在 function component 里繼續使用 state 和 lifecycle。通過 Custom Hooks 可以復用業務邏輯,從而避免添加額外的components。
Why
過去我們使用React時,component 基本分為兩種:function component 和 class component 。其中function component就是一個 pure render component,不存在 state 和 lifecycle 。function component 使組件之間耦合度降低,但一旦需要 state 或 lifecycle ,就需要變成 class component 。而 class component 也會帶來一些缺點:
- React 組件樹過于臃腫
單向數據流使組件間的通信必須一層一層往下傳,當有些狀態不適合使用 Redux 這種 global store 的情況下,此時組件之間的邏輯復用和溝通就會變得十分困難。為此,過去我們會使用各種 HOC(高階組件)來傳遞狀態。這就導致了當應用規模越來越龐大的時候,會多了很多無關 UI 的 wrapper 組件,也就使得 React 組件樹變得越來越臃腫,開發和調試效率隨之變低。
Write
了解了Hooks的基本知識,接下來就是如何去使用 Hooks API 了。Hooks 主要分為三種:
- State Hook (在 function component 使用 state)
- Effect Hook (在 function component 使用 lifecycle 和 side effect )
- Custom Hook (通過自定義Hook來復用邏輯)
State Hook
官方 Example:
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>
);
}
Hook 的本質實際上就是一個特殊的函數,通常以"use"開頭。這里的 useState
就是一個Hook,通過它可以實現在組件內使用state。useState
會返回一個pair:分別是當前 state 的值和修改這個 state 的函數。用法有點類似 class component 里的 this.setState
,只是這里不會合并 state 對象,而且注意到沒,這里的 useState
的初始值是0,跟 this.state
不同在于它不需要是一個 Object。
官方有個例子對比 useState 與 this.state 的。(傳送門:https://reactjs.org/docs/hooks-state.html)
多個 state 變量
function ExampleWithManyStates() {
// Declare multiple state variables!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// ...
}
你完全可以聲明多個 useState
,一點問題都木有!這種做法帶來的好處是:我們的 state 將不會變得非常臃腫,每個 state 都非常直觀,獨立。
Effect Hook
我們經常在 React 組件中進行拉取數據、訂閱或操作DOM,這種行為被稱為 "side effects"(副作用),這種行為通常只能在生命周期中執行,而不能在 render 里。
React 通過 useEffect
來解決這樣的問題,具體怎么用我們看看例子:
import { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
這種做法相當于把 React classes 里的 componentDidMount
,componentDidUpdate
,componentWillUnmount
合并成了一個。
由于 Hooks 是定義在最外層函數的,所以這里是能訪問到 props 和 state 的,它默認會在每一次 render 后都會被調用(包括第一次)。
同樣的,官方有個例子對比了 useEffect 和 class lifecycle 的。(傳送門:https://reactjs.org/docs/hooks-effect.html)
cleanup
通常我們的大部分 effect 行為都是不需要清理,比如網絡請求、DOM操作或者日志記錄等。但如果我們在 effects 進行了類似外部數據訂閱這樣的操作,那么我們就需要在 Unmount 前取消訂閱。針對這種場景,可以通過在 useEffect 中返回一個函數的方式進行清理。此處應該有代碼:
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';
}
乍一看,使用 Hook 的方式相對于 class component 只是代碼寫少點而已,但實際上它帶來的好處可不止這些。如果用 class component 的話,我們需要在 componentDidMount
訂閱 props.friend.id 的狀態,然后在 componentWillUnmount
中取消訂閱。
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
但這種做法可能會引發bug(試想一下,如果 friend prop 變了會怎樣?)。這種情況下,頁面上顯示的狀態就不是當前這個 friend 的啦~ 所以這是一個bug,解決的方式就是在 componentDidUpdate
中先進行 unsubscribe
,再重新 subscribe
。
componentDidUpdate(prevProps) {
// Unsubscribe from the previous friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// Subscribe to the next friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
在實際開發中,我們可能經常會沒考慮到這種情況。那么現在有了 Hook,你可以不用擔心了!
性能優化之 Skipping effects
每次 render 都會 cleanup 或執行 effect,這可能會導致性能問題。在 class component 中我們通常會在 componentDidUpdate
里進行對比判斷。而在 Hook 里,我們可以通過給 useEffect
傳遞第二個參數(數組形式)來選擇 Hook 的觸發時機,
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
同步的 useLayoutEffect
必須說明的是,useEffect
是異步的,即它不會阻塞瀏覽器渲染頁面,因為大多數情況下,這些 effects 都不需要同步執行。在極少數的場景下,可以選擇用 useLayoutEffect
。它會在 re-render 后同步執行,阻塞瀏覽器進行渲染。
Custom Hook
過去我們在組件中復用邏輯的通常做法是使用高階函數 ( high-order component ) 和 render props。如今有了 Hooks,我們可以避免添加更多的組件到我們的組件樹中了。
我們把上面 FriendStatus 稍加改動。
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;
}
它對外接受一個 friendId 作為參數,然后返回具體狀態。而在其他組件里引用也非常簡單:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
這樣,復用了邏輯而又不需要引入新的 state,簡直完美!
其他 built-in Hooks
還好很多內置的 Hooks,如:
-
useContext
(替代<Context.Consumer> 使用render props 的寫法)
function Example() {
const locale = useContext(LocaleContext);
const theme = useContext(ThemeContext);
// ...
}
-
useReducer
(相當于組件自帶 redux reducer,負責 dispatch action 并更新 state)
function Todos() {
const [todos, dispatch] = useReducer(todosReducer);
// ...
注意事項
- 在最外層函數使用 Hooks ,不要在循環塊、條件塊或函數內調用 Hooks;
- 只在 React function component 里使用 Hooks(除非是Custom Hooks,否則不要在普通函數中使用 Hooks)
最后總結一下
在 React 里,Hooks 就是一系列的特殊函數,在 function component 內部“勾住” 組件的 state 和 lifecycle。Hook 是向后兼容的,但官方不推薦大家將舊代碼的 class component 都改成 Hook,大家可以在新代碼中體驗一下這種寫法。針對 Hooks 新特性的官方文檔很詳細,這里限于篇幅,就不過多講了,推薦大家去看官方文檔。
掰掰~