hook
https://zh-hans.reactjs.org/docs/hooks-overview.html
Hook 是一些可以讓你在函數(shù)組件里“鉤入” React state 及生命周期等特性的函數(shù)赔硫。Hook 不能在 class 組件中使用 —— 這使得你不使用 class 也能使用 React。
Hook 使用規(guī)則
Hook 就是 JavaScript 函數(shù)跟继,但是使用它們會(huì)有兩個(gè)額外的規(guī)則:
- 只能在函數(shù)最外層調(diào)用 Hook。不要在循環(huán)午阵、條件判斷或者子函數(shù)中調(diào)用解幽。
- 只能在 React 的函數(shù)組件中調(diào)用 Hook。不要在其他 JavaScript 函數(shù)中調(diào)用戳葵。(還有一個(gè)地方可以調(diào)用 Hook —— 就是自定義的 Hook 中,我們稍后會(huì)學(xué)習(xí)到汉匙。)
linter 插件: https://www.npmjs.com/package/eslint-plugin-react-hooks
State Hook
import React, { useState } from 'react';
function Example() {
// 聲明一個(gè)叫 "count" 的 state 變量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
你可能想知道:為什么叫 useState 而不叫 createState?
“Create” 可能不是很準(zhǔn)確拱烁,因?yàn)?state 只在組件首次渲染的時(shí)候被創(chuàng)建。在下一次重新渲染時(shí)噩翠,useState 返回給我們當(dāng)前的 state戏自。否則它就不是 “state”了!這也是 Hook 的名字總是以 use 開頭的一個(gè)原因伤锚。我們將在后面的 Hook 規(guī)則中了解原因擅笔。
惰性初始 state
initialState 參數(shù)只會(huì)在組件的初始渲染中起作用,后續(xù)渲染時(shí)會(huì)被忽略。如果初始 state 需要通過復(fù)雜計(jì)算獲得猛们,則可以傳入一個(gè)函數(shù)念脯,在函數(shù)中計(jì)算并返回初始的 state,此函數(shù)只在初始渲染時(shí)被調(diào)用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
Effect Hook
它給函數(shù)組件增加了操作副作用的能力弯淘。它跟 class 組件中的 componentDidMount绿店、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不過被合并成了一個(gè) API庐橙。
與 componentDidMount 或 componentDidUpdate 不同假勿,使用 useEffect 調(diào)度的 effect 不會(huì)阻塞瀏覽器更新屏幕,這讓你的應(yīng)用看起來響應(yīng)更快态鳖。大多數(shù)情況下转培,effect 不需要同步地執(zhí)行。在個(gè)別情況下(例如測(cè)量布局)郁惜,有單獨(dú)的 useLayoutEffect Hook 供你使用堡距,其 API 與 useEffect 相同甲锡。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 相當(dāng)于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用瀏覽器的 API 更新頁面標(biāo)題
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
當(dāng)你調(diào)用 useEffect 時(shí)兆蕉,就是在告訴 React 在完成對(duì) DOM 的更改后運(yùn)行你的“副作用”函數(shù)。由于副作用函數(shù)是在組件內(nèi)聲明的缤沦,所以它們可以訪問到組件的 props 和 state虎韵。默認(rèn)情況下,React 會(huì)在每次渲染后調(diào)用副作用函數(shù) —— 包括第一次渲染的時(shí)候缸废。
副作用函數(shù)還可以通過返回一個(gè)函數(shù)來指定如何“清除”副作用包蓝。例如,在下面的組件中使用副作用函數(shù)來訂閱好友的在線狀態(tài)企量,并通過取消訂閱來進(jìn)行清除操作:
import React, { 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);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
在這個(gè)示例中测萎,React 會(huì)在組件銷毀時(shí)取消對(duì) ChatAPI 的訂閱,然后在后續(xù)渲染時(shí)重新執(zhí)行副作用函數(shù)届巩。
跟 useState 一樣硅瞧,你可以在組件中多次使用 useEffect :
function FriendStatusWithCounter(props) {
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);
};
});
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
通過跳過 Effect 進(jìn)行性能優(yōu)化
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count 更改時(shí)更新
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 僅在 props.friend.id 發(fā)生變化時(shí),重新訂閱
如果你要使用此優(yōu)化方式恕汇,請(qǐng)確保數(shù)組中包含了所有外部作用域中會(huì)隨時(shí)間變化并且在 effect 中使用的變量腕唧,否則你的代碼會(huì)引用到先前渲染中的舊變量。參閱文檔瘾英,了解更多關(guān)于如何處理函數(shù)以及數(shù)組頻繁變化時(shí)的措施內(nèi)容枣接。
如果想執(zhí)行只運(yùn)行一次的 effect(僅在組件掛載和卸載時(shí)執(zhí)行),可以傳遞一個(gè)空數(shù)組([])作為第二個(gè)參數(shù)缺谴。這就告訴 React 你的 effect 不依賴于 props 或 state 中的任何值但惶,所以它永遠(yuǎn)都不需要重復(fù)執(zhí)行。這并不屬于特殊情況 —— 它依然遵循依賴數(shù)組的工作方式。
如果你傳入了一個(gè)空數(shù)組([])膀曾,effect 內(nèi)部的 props 和 state 就會(huì)一直擁有其初始值片拍。盡管傳入 [] 作為第二個(gè)參數(shù)更接近大家更熟悉的 componentDidMount 和 componentWillUnmount 思維模式,但我們有更好的方式來避免過于頻繁的重復(fù)調(diào)用 effect妓肢。除此之外捌省,請(qǐng)記得 React 會(huì)等待瀏覽器完成畫面渲染之后才會(huì)延遲調(diào)用 useEffect,因此會(huì)使得額外操作很方便碉钠。
useContext
const value = useContext(MyContext);
接收一個(gè) context 對(duì)象(React.createContext 的返回值)并返回該 context 的當(dāng)前值纲缓。當(dāng)前的 context 值由上層組件中距離當(dāng)前組件最近的 <MyContext.Provider> 的 value prop 決定。
當(dāng)組件上層最近的 <MyContext.Provider> 更新時(shí)喊废,該 Hook 會(huì)觸發(fā)重渲染祝高,并使用最新傳遞給 MyContext provider 的 context value 值畜埋。即使祖先使用 React.memo 或 shouldComponentUpdate迫肖,也會(huì)在組件本身使用 useContext 時(shí)重新渲染渗磅。
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
useReducer
用 reducer 重寫 useState 一節(jié)的計(jì)數(shù)器示例:
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
惰性初始化
你可以選擇惰性地創(chuàng)建初始 state师脂。為此牺汤,需要將 init 函數(shù)作為 useReducer 的第三個(gè)參數(shù)傳入竣付,這樣初始 state 將被設(shè)置為 init(initialArg)寇窑。
這么做可以將用于計(jì)算 state 的邏輯提取到 reducer 外部凤藏,這也為將來對(duì)重置 state 的 action 做處理提供了便利:
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useCallback
把內(nèi)聯(lián)回調(diào)函數(shù)及依賴項(xiàng)數(shù)組作為參數(shù)傳入 useCallback惋增,它將返回該回調(diào)函數(shù)的 memoized 版本叠殷,該回調(diào)函數(shù)僅在某個(gè)依賴項(xiàng)改變時(shí)才會(huì)更新。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useCallback(fn, deps) 相當(dāng)于 useMemo(() => fn, deps)诈皿。
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
如果沒有提供依賴項(xiàng)數(shù)組林束,useMemo 在每次渲染時(shí)都會(huì)計(jì)算新的值。
你可以把 useMemo 作為性能優(yōu)化的手段稽亏,但不要把它當(dāng)成語義上的保證壶冒。將來,React 可能會(huì)選擇“遺忘”以前的一些 memoized 值截歉,并在下次渲染時(shí)重新計(jì)算它們胖腾,比如為離屏組件釋放內(nèi)存。先編寫在沒有 useMemo 的情況下也可以執(zhí)行的代碼 —— 之后再在你的代碼中添加 useMemo怎披,以達(dá)到優(yōu)化性能的目的胸嘁。
useRef
它創(chuàng)建的是一個(gè)普通 Javascript 對(duì)象。而 useRef() 和自建一個(gè) {current: ...} 對(duì)象的唯一區(qū)別是凉逛,useRef 會(huì)在每次渲染時(shí)返回同一個(gè) ref 對(duì)象性宏。
請(qǐng)記住,當(dāng) ref 對(duì)象內(nèi)容發(fā)生變化時(shí)状飞,useRef 并不會(huì)通知你毫胜。變更 .current 屬性不會(huì)引發(fā)組件重新渲染书斜。如果想要在 React 綁定或解綁 DOM 節(jié)點(diǎn)的 ref 時(shí)運(yùn)行某些代碼,則需要使用回調(diào) ref 來實(shí)現(xiàn)酵使。
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已掛載到 DOM 上的文本輸入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
自定義 Hook
自定義 Hook 更像是一種約定而不是功能荐吉。如果函數(shù)的名字以 “use” 開頭并調(diào)用其他 Hook,我們就說這是一個(gè)自定義 Hook口渔。 useSomething 的命名約定可以讓我們的 linter 插件在使用 Hook 的代碼中找到 bug样屠。
前面,我們介紹了一個(gè)叫 FriendStatus 的組件缺脉,它通過調(diào)用 useState 和 useEffect 的 Hook 來訂閱一個(gè)好友的在線狀態(tài)痪欲。假設(shè)我們想在另一個(gè)組件里重用這個(gè)訂閱邏輯。
首先攻礼,我們把這個(gè)邏輯抽取到一個(gè)叫做 useFriendStatus 的自定義 Hook 里:
import React, { 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;
}
使用
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>
);
}
每個(gè)組件間的 state 是完全獨(dú)立的业踢。Hook 是一種復(fù)用狀態(tài)邏輯的方式,它不復(fù)用 state 本身礁扮。事實(shí)上 Hook 的每次調(diào)用都有一個(gè)完全獨(dú)立的 state —— 因此你可以在單個(gè)組件中多次調(diào)用同一個(gè)自定義 Hook知举。
useImperativeHandle
useImperativeHandle 可以讓你在使用 ref 時(shí)自定義暴露給父組件的實(shí)例值。在大多數(shù)情況下太伊,應(yīng)當(dāng)避免使用 ref 這樣的命令式代碼雇锡。useImperativeHandle 應(yīng)當(dāng)與 forwardRef 一起使用:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
<!--使用-->
const App = props => {
const fancyInputRef = useRef();
return (
<div>
<FancyInput ref={fancyInputRef} />
<button
onClick={() => fancyInputRef.current.focus()}
>父組件調(diào)用子組件的 focus</button>
</div>
)
}
在本例中,渲染 <FancyInput ref={inputRef} /> 的父組件可以調(diào)用 inputRef.current.focus()倦畅。
useLayoutEffect
其函數(shù)簽名與 useEffect 相同遮糖,但它會(huì)在所有的 DOM 變更之后同步調(diào)用 effect〉停可以使用它來讀取 DOM 布局并同步觸發(fā)重渲染。在瀏覽器執(zhí)行繪制之前屡江,useLayoutEffect 內(nèi)部的更新計(jì)劃將被同步刷新芭概。
盡可能使用標(biāo)準(zhǔn)的 useEffect 以避免阻塞視覺更新。
useDebugValue
useDebugValue 可用于在 React 開發(fā)者工具中顯示自定義 hook 的標(biāo)簽惩嘉。
例如罢洲,“自定義 Hook” 章節(jié)中描述的名為 useFriendStatus 的自定義 Hook:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
// 在開發(fā)者工具中的這個(gè) Hook 旁邊顯示標(biāo)簽
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
tips:
我們不推薦你向每個(gè)自定義 Hook 添加 debug 值。當(dāng)它作為共享庫的一部分時(shí)才最有價(jià)值文黎。
延遲格式化 debug 值
在某些情況下惹苗,格式化值的顯示可能是一項(xiàng)開銷很大的操作。除非需要檢查 Hook耸峭,否則沒有必要這么做桩蓉。
因此,useDebugValue 接受一個(gè)格式化函數(shù)作為可選的第二個(gè)參數(shù)劳闹。該函數(shù)只有在 Hook 被檢查時(shí)才會(huì)被調(diào)用院究。它接受 debug 值作為參數(shù)洽瞬,并且會(huì)返回一個(gè)格式化的顯示值。
例如业汰,一個(gè)返回 Date 值的自定義 Hook 可以通過格式化函數(shù)來避免不必要的 toDateString 函數(shù)調(diào)用:
useDebugValue(date, date => date.toDateString());
Tips:
如何避免向下傳遞回調(diào)伙窃?
我們已經(jīng)發(fā)現(xiàn)大部分人并不喜歡在組件樹的每一層手動(dòng)傳遞回調(diào)。盡管這種寫法更明確样漆,但這給人感覺像錯(cuò)綜復(fù)雜的管道工程一樣麻煩为障。
在大型的組件樹中,我們推薦的替代方案是通過 context 用 useReducer 往下傳一個(gè) dispatch 函數(shù)
const TodosDispatch = React.createContext(null);
function TodosApp() {
// 提示:`dispatch` 不會(huì)在重新渲染之間變化
const [todos, dispatch] = useReducer(todosReducer);
return (
<TodosDispatch.Provider value={dispatch}>
<DeepTree todos={todos} />
</TodosDispatch.Provider>
);
}
function DeepChild(props) {
// 如果我們想要執(zhí)行一個(gè) action放祟,我們可以從 context 中獲取 dispatch产场。
const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({ type: 'add', text: 'hello' });
}
return (
<button onClick={handleClick}>Add todo</button>
);
}