2019 年開始,在使用React的時候,已經逐步從 Class 組件,過渡到函數組件了。雖然函數組件十分便捷,可是在使用函數組件的時候,仍是有一些疑惑的地方,致使有時候會出現一些奇奇怪怪的問題。在這裏,我想經過官網和博客文章以及本身的一些積累,整理下最佳實踐,以備不時之需。html
Hook 不會影響你對 React 概念的理解。 偏偏相反,Hook 爲已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命週期。稍後咱們將看到,Hook 還提供了一種更強大的方式來組合他們。node
下面是一些官方列舉的,提出並使用 hooks 的一些動機。react
你可使用 Hook 從組件中提取狀態邏輯,使得這些邏輯能夠單獨測試並複用。Hook 使你在無需修改組件結構的狀況下複用狀態邏輯。 這使得在組件間或社區內共享 Hook 變得更便捷。git
useEffectgithub
咱們常常維護一些組件,組件起初很簡單,可是逐漸會被狀態邏輯和反作用充斥。每一個生命週期經常包含一些不相關的邏輯。例如,組件經常在 componentDidMount
和 componentDidUpdate
中獲取數據。可是,同一個 componentDidMount
中可能也包含不少其它的邏輯,如設置事件監聽,而以後需在 componentWillUnmount
中清除。相互關聯且須要對照修改的代碼被進行了拆分,而徹底不相關的代碼卻在同一個方法中組合在一塊兒。如此很容易產生 bug,而且致使邏輯不一致。npm
在多數狀況下,不可能將組件拆分爲更小的粒度,由於狀態邏輯無處不在。這也給測試帶來了必定挑戰。同時,這也是不少人將 React 與狀態管理庫結合使用的緣由之一。可是,這每每會引入了不少抽象概念,須要你在不一樣的文件之間來回切換,使得複用變得更加困難。編程
爲了解決這個問題,Hook 將組件中相互關聯的部分拆分紅更小的函數(好比設置訂閱或請求數據),而並不是強制按照生命週期劃分。你還可使用 reducer 來管理組件的內部狀態,使其更加可預測。json
除了代碼複用和代碼管理會遇到困難外,咱們還發現 class 是學習 React 的一大屏障。你必須去理解 JavaScript 中 this
的工做方式,這與其餘語言存在巨大差別。還不能忘記綁定事件處理器。沒有穩定的語法提案,這些代碼很是冗餘。你們能夠很好地理解 props,state 和自頂向下的數據流,但對 class 卻束手無策。即使在有經驗的 React 開發者之間,對於函數組件與 class 組件的差別也存在分歧,甚至還要區分兩種組件的使用場景。
爲了解決這些問題,Hook 使你在非 class 的狀況下可使用更多的 React 特性。 從概念上講,React 組件一直更像是函數。而 Hook 則擁抱了函數,同時也沒有犧牲 React 的精神原則。Hook 提供了問題的解決方案,無需學習複雜的函數式或響應式編程技術。api
useState
是咱們要學習的第一個 「Hook」,它的用途是管理狀態。數組
const [state, setState] = useState(initialState);複製代碼
返回一個 state,以及更新 state 的函數。
在初始渲染期間,返回的狀態 (state
) 與傳入的第一個參數 (initialState
) 值相同。
setState
函數用於更新 state。它接收一個新的 state 值並將組件的一次從新渲染加入隊列。
setState(newState);複製代碼
在後續的從新渲染中,useState
返回的第一個值將始終是更新後最新的 state。
注意
React 會確保
setState
函數的標識是穩定的,而且不會在組件從新渲染時發生變化。這就是爲何能夠安全地從useEffect
或useCallback
的依賴列表中省略setState
。
與 class 組件中的 setState
方法不一樣,useState
不會自動合併更新對象。你能夠用函數式的 setState
結合展開運算符來達到合併更新對象的效果。
setState(prevState => {
// 也可使用 Object.assign
return {...prevState, ...updatedValues};
});複製代碼
useReducer
是另外一種可選方案,它更適合用於管理包含多個子值的 state 對象。
initialState
參數只會在組件的初始渲染中起做用,後續渲染時會被忽略。若是初始 state 須要經過複雜計算得到,則能夠傳入一個函數,在函數中計算並返回初始的 state,此函數只在初始渲染時被調用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});複製代碼
useEffect
就是一個 Effect Hook,給函數組件增長了操做反作用的能力。它跟 class 組件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具備相同的用途,只不過被合併成了一個 API。
當你調用 useEffect
時,就是在告訴 React 在完成對 DOM 的更改後運行你的「反作用」函數。因爲反作用函數是在組件內聲明的,因此它們能夠訪問到組件的 props 和 state。默認狀況下,React 會在每次渲染後調用反作用函數 —— 包括第一次渲染的時候。
反作用函數還能夠經過返回一個函數來指定如何「清除」反作用。可是,這個清除做用,只有當組件被幹掉了,纔會觸發。
有時候,咱們只想在 React 更新 DOM 以後運行一些額外的代碼。好比發送網絡請求,手動變動 DOM,記錄日誌,這些都是常見的無需清除的操做。由於咱們在執行完這些操做以後,就能夠忽略他們了。
useEffect
作了什麼? 經過使用這個 Hook,你能夠告訴 React 組件須要在渲染後執行某些操做。React 會保存你傳遞的函數(咱們將它稱之爲 「effect」),而且在執行 DOM 更新以後調用它。
爲何在組件內部調用 useEffect
? 將 useEffect
放在組件內部讓咱們能夠在 effect 中直接訪問 count
state 變量(或其餘 props)。咱們不須要特殊的 API 來讀取它 —— 它已經保存在函數做用域中。Hook 使用了 JavaScript 的閉包機制,而不用在 JavaScript 已經提供瞭解決方案的狀況下,還引入特定的 React API。
useEffect
會在每次渲染後都執行嗎? 是的,默認狀況下,它在第一次渲染以後
提示
與 componentDidMount
或 componentDidUpdate
不一樣,使用 useEffect
調度的 effect 不會阻塞瀏覽器更新屏幕,這讓你的應用看起來響應更快。大多數狀況下,effect 不須要同步地執行。在個別狀況下(例如測量佈局),有單獨的 useLayoutEffect
Hook 供你使用,其 API 與 useEffect
相同。
以前,咱們研究瞭如何使用不須要清除的反作用,還有一些反作用是須要清除的。例如訂閱外部數據源。這種狀況下,清除工做是很是重要的,能夠防止引發內存泄露!
爲何要在 effect 中返回一個函數? 這是 effect 可選的清除機制。每一個 effect 均可以返回一個清除函數。如此能夠將添加和移除訂閱的邏輯放在一塊兒。它們都屬於 effect 的一部分。
React 什麼時候清除 effect? React 會在組件卸載的時候執行清除操做。正如以前學到的,effect 在每次渲染的時候都會執行。這就是爲何 React
在下面這個 Effect 中,咱們會在每次 props.friend.id
更新的時候,解除以前訂閱的函數,並用新的狀態 props.friend.id
訂閱函數。
並不須要特定的代碼來處理更新邏輯,由於 useEffect
在某些狀況下,每次渲染後都執行清理或者執行 effect 可能會致使性能問題。在 class 組件中,咱們能夠經過在 componentDidUpdate
中添加對 prevProps
或 prevState
的比較邏輯解決:
這是很常見的需求,因此它被內置到了 useEffect
的 Hook API 中。若是某些特定值在兩次重渲染之間沒有發生變化,你能夠通知 React 跳過對 effect 的調用,只要傳遞數組做爲 useEffect
的第二個可選參數便可:
注意:
若是你要使用此優化方式,請確保數組中包含了全部外部做用域中會隨時間變化而且在 effect 中使用的變量,不然你的代碼會引用到先前渲染中的舊變量。參閱文檔,瞭解更多關於如何處理函數以及數組頻繁變化時的措施內容。
若是想執行只運行一次的 effect(僅在組件掛載和卸載時執行),能夠傳遞一個空數組([]
)做爲第二個參數。這就告訴 React 你的 effect 不依賴於 props 或 state 中的任何值,因此它永遠都不須要重複執行。這並不屬於特殊狀況 —— 它依然遵循依賴數組的工做方式。
若是你傳入了一個空數組([]
),effect 內部的 props 和 state 就會一直擁有其初始值。儘管傳入 []
做爲第二個參數更接近你們更熟悉的 componentDidMount
和 componentWillUnmount
思惟模式,但咱們有更好的方式來避免過於頻繁的重複調用 effect。除此以外,請記得 React 會等待瀏覽器完成畫面渲染以後纔會延遲調用 useEffect
,所以會使得額外操做很方便。
咱們推薦啓用 eslint-plugin-react-hooks
中的 exhaustive-deps
規則。此規則會在添加錯誤依賴時發出警告並給出修復建議。
const value = useContext(MyContext);複製代碼
接收一個 context 對象(React.createContext
的返回值)並返回該 context 的當前值。當前的 context 值由上層組件中距離當前組件最近的 <MyContext.Provider>
的 value
prop 決定。
當組件上層最近的 <MyContext.Provider>
更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 MyContext
provider 的 context value
值。即便祖先使用 React.memo
或 shouldComponentUpdate
,也會在組件自己使用 useContext
時從新渲染。
別忘記 useContext
的參數必須是
useContext(MyContext)
useContext(MyContext.Consumer)
useContext(MyContext.Provider)
調用了 useContext
的組件總會在 context 值變化時從新渲染。若是重渲染組件的開銷較大,你能夠 經過使用 memoization 來優化。
提示
若是你在接觸 Hook 前已經對 context API 比較熟悉,那應該能夠理解,
useContext(MyContext)
至關於 class 組件中的static contextType = MyContext
或者<MyContext.Consumer>
。
useContext(MyContext)
只是讓你可以讀取context 的值以及訂閱 context 的變化。你仍然須要在上層組件樹中使用<MyContext.Provider>
來爲下層組件提供context。
把以下代碼與 Context.Provider 放在一塊兒
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> );
}複製代碼
對先前 Context 高級指南中的示例使用 hook 進行了修改,你能夠在連接中找到有關如何 Context 的更多信息。
const [state, dispatch] = useReducer(reducer, initialArg, init);複製代碼
useState
的替代方案。它接收一個形如 (state, action) => newState
的 reducer,並返回當前的 state 以及與其配套的 dispatch
方法。(若是你熟悉 Redux 的話,就已經知道它如何工做了。)
在某些場景下,useReducer
會比 useState
更適用,例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於以前的 state 等。而且,使用 useReducer
還能給那些會觸發深更新的組件作性能優化,由於你能夠向子組件傳遞 dispatch
而不是回調函數 。
如下是用 reducer 重寫 useState
一節的計數器示例:
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>
</>
);
}複製代碼
注意
React 會確保
dispatch
函數的標識是穩定的,而且不會在組件從新渲染時改變。這就是爲何能夠安全地從useEffect
或useCallback
的依賴列表中省略dispatch
。
有兩種不一樣初始化 useReducer
state 的方式,你能夠根據使用場景選擇其中的一種。將初始 state 做爲第二個參數傳入 useReducer
是最簡單的方法:
const [state, dispatch] = useReducer(
reducer,
{count: initialCount} );複製代碼
注意
React 不使用
state = initialState
這一由 Redux 推廣開來的參數約定。有時候初始值依賴於 props,所以須要在調用 Hook 時指定。若是你特別喜歡上述的參數約定,能夠經過調用useReducer(reducer, undefined, reducer)
來模擬 Redux 的行爲,但咱們不鼓勵你這麼作。
你能夠選擇惰性地建立初始 state。爲此,須要將 init
函數做爲 useReducer
的第三個參數傳入,這樣初始 state 將被設置爲 init(initialArg)
。
這麼作能夠將用於計算 state 的邏輯提取到 reducer 外部,這也爲未來對重置 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>
</>
);
}複製代碼
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);複製代碼
返回一個 memoized 回調函數。
把內聯回調函數及依賴項數組做爲參數傳入 useCallback
,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時纔會更新。當你把回調函數傳遞給通過優化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子組件時,它將很是有用。
useCallback(fn, deps)
至關於 useMemo(() => fn, deps)
。
注意
依賴項數組不會做爲參數傳給回調函數。雖然從概念上來講它表現爲:全部回調函數中引用的值都應該出如今依賴項數組中。將來編譯器會更加智能,屆時自動建立數組將成爲可能。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);複製代碼
返回一個 memoized 值。
把「建立」函數和依賴項數組做爲參數傳入 useMemo
,它僅會在某個依賴項改變時才從新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算。
記住,傳入 useMemo
的函數會在渲染期間執行。請不要在這個函數內部執行與渲染無關的操做,諸如反作用這類的操做屬於 useEffect
的適用範疇,而不是 useMemo
。
若是沒有提供依賴項數組,useMemo
在每次渲染時都會計算新的值。
你能夠把 useMemo
做爲性能優化的手段,但不要把它當成語義上的保證。未來,React 可能會選擇「遺忘」之前的一些 memoized 值,並在下次渲染時從新計算它們,好比爲離屏組件釋放內存。先編寫在沒有 useMemo
的狀況下也能夠執行的代碼 —— 以後再在你的代碼中添加 useMemo
,以達到優化性能的目的。
注意
依賴項數組不會做爲參數傳給「建立」函數。雖然從概念上來講它表現爲:全部「建立」函數中引用的值都應該出如今依賴項數組中。將來編譯器會更加智能,屆時自動建立數組將成爲可能。
const refContainer = useRef(initialValue);複製代碼
useRef
返回一個可變的 ref 對象,其 .current
屬性被初始化爲傳入的參數(initialValue
)。返回的 ref 對象在組件的整個生命週期內保持不變。
一個常見的用例即是命令式地訪問子組件:
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>
</>
);
}複製代碼
本質上,useRef
就像是能夠在其 .current
屬性中保存一個可變值的「盒子」。
你應該熟悉 ref 這一種訪問 DOM 的主要方式。若是你將 ref 對象以 <div ref={myRef} />
形式傳入組件,則不管該節點如何改變,React 都會將 ref 對象的 .current
屬性設置爲相應的 DOM 節點。
然而,useRef()
比 ref
屬性更有用。它能夠很方便地保存任何可變值,其相似於在 class 中使用實例字段的方式。
這是由於它建立的是一個普通 Javascript 對象。而 useRef()
和自建一個 {current: ...}
對象的惟一區別是,useRef
會在每次渲染時返回同一個 ref 對象。
請記住,當 ref 對象內容發生變化時,useRef
並
.current
屬性不會引起組件從新渲染。若是想要在 React 綁定或解綁 DOM 節點的 ref 時運行某些代碼,則須要使用
回調 ref 來實現。
獲取 DOM 節點的位置或是大小的基本方式是使用 callback ref。每當 ref 被附加到一個另外一個節點,React 就會調用 callback。這裏有一個 小 demo:
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
},
[]);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1> <h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}複製代碼
在這個案例中,咱們沒有選擇使用 useRef
,由於當 ref 是一個對象時它並不會把當前 ref 的值的
注意到咱們傳遞了 []
做爲 useCallback
的依賴列表。這確保了 ref callback 不會在再次渲染時改變,所以 React 不會在非必要的時候調用它。
本例中,僅在組件掛載和卸載時調用回調ref,由於 <h1>
組件在每次從新渲染時,高度都不會變化,因此不須要觸發回調ref。若是你但願在組件調整大小時獲得通知,可使用ResizeObserver或使用其餘第三方的Hooks。
若是你願意,你能夠 把這個邏輯抽取出來做爲 一個可複用的 Hook:
function MeasureExample() {
const [rect, ref] = useClientRect(); return (
<>
<h1 ref={ref}>Hello, world</h1>
{rect !== null &&
<h2>The above header is {Math.round(rect.height)}px tall</h2>
}
</>
);
}
function useClientRect() {
const [rect, setRect] = useState(null);
const ref = useCallback(node => {
if (node !== null) {
setRect(node.getBoundingClientRect());
}
}, []);
return [rect, ref];
}複製代碼
useImperativeHandle(ref, createHandle, [deps])複製代碼
useImperativeHandle
可讓你在使用 ref
時自定義暴露給父組件的實例值。在大多數狀況下,應當避免使用 ref 這樣的命令式代碼。useImperativeHandle
應當與 forwardRef
一塊兒使用:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);複製代碼
在本例中,渲染 <FancyInput ref={inputRef} />
的父組件能夠調用 inputRef.current.focus()
。
其函數簽名與 useEffect
相同,但它會在全部的 DOM 變動以後同步調用 effect。可使用它來讀取 DOM 佈局並同步觸發重渲染。在瀏覽器執行繪製以前,useLayoutEffect
內部的更新計劃將被同步刷新。
儘量使用標準的 useEffect
以免阻塞視覺更新。
提示
若是你正在將代碼從 class 組件遷移到使用 Hook 的函數組件,則須要注意
useLayoutEffect
與componentDidMount
、componentDidUpdate
的調用階段是同樣的。可是,咱們推薦你一開始先用useEffect
,只有當它出問題的時候再嘗試使用useLayoutEffect
。若是你使用服務端渲染,請記住,
不管useLayoutEffect
仍是useEffect
都沒法在 Javascript 代碼加載完成以前執行。這就是爲何在服務端渲染組件中引入useLayoutEffect
代碼時會觸發 React 告警。解決這個問題,須要將代碼邏輯移至useEffect
中(若是首次渲染不須要這段邏輯的狀況下),或是將該組件延遲到客戶端渲染完成後再顯示(若是直到useLayoutEffect
執行以前 HTML 都顯示錯亂的狀況下)。若要從服務端渲染的 HTML 中排除依賴佈局 effect 的組件,能夠經過使用
showChild && <Child />
進行條件渲染,並使用useEffect(() => { setShowChild(true); }, [])
延遲展現組件。這樣,在客戶端渲染完成以前,UI 就不會像以前那樣顯示錯亂了。
useDebugValue(value)複製代碼
useDebugValue
可用於在 React 開發者工具中顯示自定義 hook 的標籤。
例如,「自定義 Hook」 章節中描述的名爲 useFriendStatus
的自定義 Hook:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
// 在開發者工具中的這個 Hook 旁邊顯示標籤
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}複製代碼
其實不只是對象,函數在每次渲染時也是獨立的。這就是 Capture Value 特性。
通常來講,不安全。
function Example({ someProp }) {
function doSomething() {
console.log(someProp); }
useEffect(() => {
doSomething();
}, []); // 🔴 這樣不安全(它調用的 `doSomething` 函數使用了 `someProp`)}複製代碼
要記住 effect 外部的函數使用了哪些 props 和 state 很難。這也是爲何 一般你會想要在 effect
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log(someProp); }
doSomething();
}, [someProp]); // ✅ 安全(咱們的 effect 僅用到了 `someProp`)}複製代碼
若是這樣以後咱們依然沒用到組件做用域中的任何值,就能夠安全地把它指定爲 []
:
useEffect(() => {
function doSomething() {
console.log('hello');
}
doSomething();
}, []); // ✅ 在這個例子中是安全的,由於咱們沒有用到組件做用域中的 *任何* 值複製代碼
根據你的用例,下面列舉了一些其餘的辦法。
注意
咱們提供了一個
exhaustive-deps
ESLint 規則做爲eslint-plugin-react-hooks
包的一部分。它會幫助你找出沒法一致地處理更新的組件。
讓咱們來看看這有什麼關係。
若是你指定了一個 依賴列表 做爲 useEffect
、useMemo
、useCallback
或 useImperativeHandle
的最後一個參數,它必須包含回調中的全部值,並參與 React 數據流。這就包括 props、state,以及任何由它們衍生而來的東西。
只有 當函數(以及它所調用的函數)不引用 props、state 以及由它們衍生而來的值時,你才能放心地把它們從依賴列表中省略。下面這個案例有一個 Bug:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId); // 使用了 productId prop const json = await response.json();
setProduct(json);
}
useEffect(() => {
fetchProduct();
}, []); // 🔴 這樣是無效的,由於 `fetchProduct` 使用了 `productId` // ...
}複製代碼
推薦的修復方案是把那個函數移動到你的 effect
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// 把這個函數移動到 effect 內部後,咱們能夠清楚地看到它用到的值。 async function fetchProduct() { const response = await fetch('http://myapi/product/' + productId); const json = await response.json(); setProduct(json); }
fetchProduct();
}, [productId]); // ✅ 有效,由於咱們的 effect 只用到了 productId // ...
}複製代碼
這同時也容許你經過 effect 內部的局部變量來處理無序的響應:
useEffect(() => {
let ignore = false; async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
if (!ignore) setProduct(json); }
fetchProduct();
return () => { ignore = true }; }, [productId]);複製代碼
咱們把這個函數移動到 effect 內部,這樣它就不用出如今它的依賴列表中了。
提示
若是處於某些緣由你
useCallback
Hook。這就確保了它不隨渲染而改變,除非
function ProductPage({ productId }) {
// ✅ 用 useCallback 包裹以免隨渲染髮生改變 const fetchProduct = useCallback(() => { // ... Does something with productId ... }, [productId]); // ✅ useCallback 的全部依賴都被指定了
return <ProductDetails fetchProduct={fetchProduct} />;
}
function ProductDetails({ fetchProduct }) {
useEffect(() => {
fetchProduct();
}, [fetchProduct]); // ✅ useEffect 的全部依賴都被指定了
// ...
}複製代碼
注意在上面的案例中,咱們 須要 讓函數出如今依賴列表中。這確保了 ProductPage
的 productId
prop 的變化會自動觸發 ProductDetails
的從新獲取。
注意
咱們推薦 在 context 中向下傳遞
dispatch
而非在 props 中使用獨立的回調。下面的方法僅僅出於文檔完整性考慮,以及做爲一條出路在此說起。同時也請注意這種模式在 並行模式 下可能會致使一些問題。咱們計劃在將來提供一個更加合理的替代方案,但當下最安全的解決方案是,若是回調所依賴的值變化了,老是讓回調失效。
在某些罕見場景中,你可能會須要用 useCallback
記住一個回調,但因爲內部函數必須常常從新建立,記憶效果不是很好。若是你想要記住的函數是一個事件處理器而且在渲染期間沒有被用到,你能夠 把 ref 當作實例變量 來用,並手動把最後提交的值保存在它當中:
function Form() {
const [text, updateText] = useState('');
const textRef = useRef();
useEffect(() => {
textRef.current = text; // 把它寫入 ref });
const handleSubmit = useCallback(() => {
const currentText = textRef.current; // 從 ref 讀取它 alert(currentText);
}, [textRef]); // 不要像 [text] 那樣從新建立 handleSubmit
return (
<>
<input value={text} onChange={e => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} />
</>
);
}複製代碼
這是一個比較麻煩的模式,但這表示若是你須要的話你能夠用這條出路進行優化。若是你把它抽取成一個自定義 Hook 的話會更加好受些:
function Form() {
const [text, updateText] = useState('');
// 即使 `text` 變了也會被記住:
const handleSubmit = useEventCallback(() => { alert(text);
}, [text]);
return (
<>
<input value={text} onChange={e => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} />
</>
);
}
function useEventCallback(fn, dependencies) { const ref = useRef(() => {
throw new Error('Cannot call an event handler while rendering.');
});
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
}複製代碼
不管如何,咱們都 不推薦使用這種模式 ,只是爲了文檔的完整性而把它展現在這裏。相反的,咱們更傾向於 避免向下深刻傳遞迴調。
利用 useRef
就能夠繞過 Capture Value 的特性。能夠認爲 ref
在全部 Render 過程當中保持着惟一引用,所以全部對 ref
的賦值或取值,拿到的都只有一個最終狀態,而不會在每一個 Render 間存在隔離。
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
// ...
}複製代碼
也能夠簡潔的認爲,ref
是 Mutable 的,而 state
是 Immutable 的。
上述例子使用了 count
,然而這樣的代碼很彆扭,由於你在一個只想執行一次的 Effect 裏依賴了外部變量。
既然要誠實,那隻好 想辦法不依賴外部變量:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);複製代碼
setCount
還有一種函數回調模式,你不須要關心當前值是什麼,只要對 「舊的值」 進行修改便可。這樣雖然代碼永遠運行在第一次 Render 中,但老是能夠訪問到最新的 state
。
將更新與動做解耦就能夠了:
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" }); // Instead of setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [dispatch]);複製代碼
這就是一個局部 「Redux」,因爲更新變成了 dispatch({ type: "tick" })
因此無論更新時須要依賴多少變量,在調用更新的動做裏都不須要依賴任何變量。 具體更新操做在 reducer
函數裏寫就能夠了。在線 Demo。
Dan 也將
useReducer
比做 Hooks 的的金手指模式,由於這充分繞過了 Diff 機制,不過確實能解決痛點!
function Parent() {
const [query, setQuery] = useState("react");
// ✅ Preserves identity until query changes
const fetchData = useCallback(() => {
const url = "https://hn.algolia.com/api/v1/search?query=" + query;
// ... Fetch data and return it ...
}, [query]); // ✅ Callback deps are OK
return <Child fetchData={fetchData} />;
}
function Child({ fetchData }) {
let [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, [fetchData]); // ✅ Effect deps are OK
// ...
}複製代碼
因爲函數也具備 Capture Value 特性,通過 useCallback
包裝過的函數能夠看成普通變量做爲 useEffect
的依賴。useCallback
作的事情,就是在其依賴變化時,返回一個新的函數引用,觸發 useEffect
的依賴變化,並激活其從新執行。
自定義 Hook 是一種天然遵循 Hook 設計的約定,而並非 React 的特性。
自定義 Hook 必須以 「use
」 開頭嗎?必須如此。這個約定很是重要。不遵循的話,因爲沒法判斷某個函數是否包含對其內部 Hook 的調用,React 將沒法自動檢查你的 Hook 是否違反了 Hook 的規則。
在兩個組件中使用相同的 Hook 會共享 state 嗎?不會。自定義 Hook 是一種重用
自定義 Hook 如何獲取獨立的 state?每次
useFriendStatus
,從 React 的角度來看,咱們的組件只是調用了
useState
和
useEffect
。 正如咱們在
以前章節中
瞭解到的同樣,咱們能夠在一個組件中屢次調用
useState
和
useEffect
,它們是徹底獨立的。
因爲 Hook 自己就是函數,所以咱們能夠在它們之間傳遞信息。
咱們將使用聊天程序中的另外一個組件來講明這一點。這是一個聊天消息接收者的選擇器,它會顯示當前選定的好友是否在線:
const friendList = [
{ id: 1, name: 'Phoebe' },
{ id: 2, name: 'Rachel' },
{ id: 3, name: 'Ross' },
];
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1); const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} /> <select
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}複製代碼
咱們將當前選擇的好友 ID 保存在 recipientID
狀態變量中,並在用戶從 <select>
中選擇其餘好友時更新這個 state。
因爲 useState
爲咱們提供了 recipientID
狀態變量的最新值,所以咱們能夠將它做爲參數傳遞給自定義的 useFriendStatus
Hook:
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);複製代碼
如此可讓咱們知道
recipientID
狀態變量時,
useFriendStatus
Hook 將會取消訂閱以前選中的好友,並訂閱新選中的好友狀態。
若是你錯過自動合併,你能夠寫一個自定義的 useLegacyState
Hook 來合併對象 state 的更新。然而,咱們推薦把 state 切分紅多個 state 變量,每一個變量包含的不一樣值會在同時發生變化。
能夠 經過 ref 來手動實現:
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count); return <h1>Now: {count}, before: {prevCount}</h1>;
}
function usePrevious(value) { const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}複製代碼
注意看這是如何做用於 props, state,或任何其餘計算出來的值的。
function Counter() {
const [count, setCount] = useState(0);
const calculation = count + 100;
const prevCalculation = usePrevious(calculation); // ...複製代碼
考慮到這是一個相對常見的使用場景,極可能在將來 React 會自帶一個 usePrevious
Hook。