React v16.7.0-alpha 後引入了「Hooks」此一向後兼容之概念。
隨著 React 應用長期擴展和維護,終會引致以下所謂「unconnected」問題,而 Hooks 因應而生:
React 不鼓勵開發者急於改寫現有代碼,而是逐漸轉換思想,即「thinking in Hooks」。Hooks 能覆蓋 class 的所有使用場景,但仍會保留 class。
以下代碼示例片段,默認發生於此函式之內:
import { useState, useEffect } from 'react';
function Example() {
// ...
}
useState
是一個 Hook。在函式組件內調用它(傳參為初始 state 值),會返回新增的本地 state、以及用以更新該 state 的函式:
const [count, setCount] = useState(0); // 初始值為 0 的新 state,返回該 state 和更新函式
上例之 setCount
和 class 裏的 this.setState
相似,除了它不合併新舊 state,而是直接替代——此即為 Hooks 的初衷,即提倡基於哪些 state 會一道改變來分割 state(並因之分割 Hooks),故直接替代 state 值而非合併一龐雜的 state 對象。
諸如抓取數據、訂閱和手動操作 DOM 這些行為,皆是「副作用」(簡稱「effects」)。我們通常想副作用發生於 React 更新 DOM 之後,譬如把基於 state 的 DOM 手動操作放在 componentDidMount
和 componentDidUpdate
生命週期中,而這導致重複代碼。useEffect
此一 Hook,發揮並結合了 class 裏的各生命週期的用處,能在函式組件發生渲染之後執行副作用。
useEffect(() => {
document.title = `You clicked ${count} times`;
})
副作用還能透過返回一個函式,以指定 React 於組件卸載時「清理」之:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
在一函式組件內結合調用多個 useState
和 useEffect
,將無關的邏輯分離至多個代碼塊:
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
useEffect
在每次重渲染時都觸發的原因是:在 class 組件中,使用到 props 的副作用在 props 變動時會引致 bug,除非正確引入 componentDidUpdate
(which is 一常見的 bug 來源)以監測 props。
為優化性能,可擇機取消重渲染後觸發 useEffect
。不同於 componentDidUpdate
中比對 prevProps
參數和當前 props(或 prevState
和當前 state),useEffect
內建可選的第二個參數:包含 state 名的數組,於是僅在該 state 變化時才重跑 effect。
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]);
官方提供一 eslint-plugin-react-hooks 來確保這些規則。
自訂 Hooks 讓你能復用富狀態邏輯,而不似 render props 和 HOC 那般徒增新組件。
將訂閱好友在線狀態的邏輯抽取出來成為一自訂 Hook:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => { /* unsubscribe */ };
});
return isOnline;
}
然後就可在多個組件中使用之:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
須知自訂 Hooks 只復用邏輯而非 state,調用者仍有完全孤立的 state。自訂 Hooks 更似一個自然流露的 convention 而非 feature,若一個函式以「use
」冠名並其內部調用了其他 Hooks,則為一自訂 Hook。諸如表單處理、動畫、訂閱聲明、定時器等行為,皆可利用自訂 Hooks。
useContext
此 Hook 讓你毋須引入嵌套即可訂閱 React context:
const locale = useContext(LocaleContext);
const theme = useContext(ThemeContext);
開發者通常不樂意手動地在組件樹中層層傳遞回調函式。可以結合 useReducer
(下文會述及)和 context 來往下傳遞一 dispatch
:
const TodosDispatch = React.createContext(null);
function TodosApp() {
// Tip: `dispatch` won't change between re-renders
const [todos, dispatch] = useReducer(todosReducer);
return (
<TodosDispatch.Provider value={dispatch}>
<DeepTree todos={todos} />
</TodosDispatch.Provider>
);
}
TodosApp
內的任意子裔都可以讀取使用 dispatch
函數並發起 actions 至 TodosApp
:
function DeepChild(props) {
// 從 context 處取用 dispatch 來執行 action
const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({ type: 'add', text: 'hello' });
}
return (
<button onClick={handleClick}>Add todo</button>
);
}
useReducer
此 Hook 是 useState
的替代,可用一 reducer 管理複雜組件的本地 state。它接收一 reducer 和一可選的 initialState
,返回當前 state 併以一 dispatch
方法。
const initialState = { count: 0 };
function reducer(state, action) {
switch(action.type) {
case 'reset': return initialState;
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
}
}
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'reset'})}>
Reset
</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
)
}
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一個記憶化的(memoized)值。只會在輸入參數時發生改變時觸發重計算,避免每次渲染後的計算開銷。
useMemo
甚至可用來跳過對子組件的重渲染:
function Parent({ a, b }) {
// 僅當 `a` 變化時重渲染:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// 僅當 `b` 變化時重渲染:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
}
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
返回一個記憶化的回調函式,它只會在輸入參數發生改變時觸發。引用比較的子組件會引致不必要的渲染,useCallback
即能用於優化此情境(類似 shouldComponentUpdate
)。
useRef
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向加載的輸入框元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
一個小技巧:useRef
可用以保存實例變量,因為「ref」對象是一個通用的容器,而其 current
成員是可變的(mutable)並能貯存任意值,類似 class 的實例變量。
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
function handleCancelClick() {
clearInterval(intervalRef.current);
}
}
完 :)