Hook API 索引

Hook API 索引

Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。

本頁面主要描述 React 中內置的 Hook API。

如果你剛開始接觸 Hook,那麼可能需要先查閱 Hook 概覽。你也可以在 Hooks FAQ 章節中獲取有用的信息。​​​​​​​

基礎 Hook

useState

useEffect

useContext

額外的 Hook

useReducer

useCallback

useMemo

useRef

useImperativeHandle

useLayoutEffect

useDebugValue

基礎 Hook

useState

const [state, setState] = useState(initialState);

返回一個 state,以及更新 state 的函數。

在初始渲染期間,返回的狀態 (state) 與傳入的第一個參數 (initialState) 值相同。

setState 函數用於更新 state。它接收一個新的 state 值並將組件的一次重新渲染加入隊列。

setState(newState);

在後續的重新渲染中,useState 返回的第一個值將始終是更新後最新的 state。

注意

React 會確保 setState 函數的標識是穩定的,並且不會在組件重新渲染時發生變化。這就是爲什麼可以安全地從 useEffect 或 useCallback 的依賴列表中省略 setState

函數式更新

如果新的 state 需要通過使用先前的 state 計算得出,那麼可以將函數傳遞給 setState。該函數將接收先前的 state,並返回一個更新後的值。下面的計數器組件示例展示了 setState 的兩種用法:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}

“+” 和 “-” 按鈕採用函數式形式,因爲被更新的 state 需要基於之前的 state。但是“重置”按鈕則採用普通形式,因爲它總是把 count 設置回初始值。

如果你的更新函數返回值與當前 state 完全相同,則隨後的重渲染會被完全跳過。

注意

與 class 組件中的 setState 方法不同,useState 不會自動合併更新對象。你可以用函數式的 setState 結合展開運算符來達到合併更新對象的效果。

setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});

useReducer 是另一種可選方案,它更適合用於管理包含多個子值的 state 對象。

惰性初始 state

initialState 參數只會在組件的初始渲染中起作用,後續渲染時會被忽略。如果初始 state 需要通過複雜計算獲得,則可以傳入一個函數,在函數中計算並返回初始的 state,此函數只在初始渲染時被調用:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

跳過 state 更新

調用 State Hook 的更新函數並傳入當前的 state 時,React 將跳過子組件的渲染及 effect 的執行。(React 使用 Object.is 比較算法 來比較 state。)

需要注意的是,React 可能仍需要在跳過渲染前渲染該組件。不過由於 React 不會對組件樹的“深層”節點進行不必要的渲染,所以大可不必擔心。如果你在渲染期間執行了高開銷的計算,則可以使用 useMemo 來進行優化。

useEffect

useEffect(didUpdate);

該 Hook 接收一個包含命令式、且可能有副作用代碼的函數。

在函數組件主體內(這裏指在 React 渲染階段)改變 DOM、添加訂閱、設置定時器、記錄日誌以及執行其他包含副作用的操作都是不被允許的,因爲這可能會產生莫名其妙的 bug 並破壞 UI 的一致性。

使用 useEffect 完成副作用操作。賦值給 useEffect 的函數會在組件渲染到屏幕之後執行。你可以把 effect 看作從 React 的純函數式世界通往命令式世界的逃生通道。

默認情況下,effect 將在每輪渲染結束後執行,但你可以選擇讓它 在只有某些值改變的時候 才執行。

清除 effect

通常,組件卸載時需要清除 effect 創建的諸如訂閱或計時器 ID 等資源。要實現這一點,useEffect 函數需返回一個清除函數。以下就是一個創建訂閱的例子:

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除訂閱
    subscription.unsubscribe();
  };
});

爲防止內存泄漏,清除函數會在組件卸載前執行。另外,如果組件多次渲染(通常如此),則在執行下一個 effect 之前,上一個 effect 就已被清除。在上述示例中,意味着組件的每一次更新都會創建新的訂閱。若想避免每次更新都觸發 effect 的執行,請參閱下一小節。

effect 的執行時機

與 componentDidMountcomponentDidUpdate 不同的是,在瀏覽器完成佈局與繪製之後,傳給 useEffect 的函數會延遲調用。這使得它適用於許多常見的副作用場景,比如設置訂閱和事件處理等情況,因此不應在函數中執行阻塞瀏覽器更新屏幕的操作。

然而,並非所有 effect 都可以被延遲執行。例如,在瀏覽器執行下一次繪製前,用戶可見的 DOM 變更就必須同步執行,這樣用戶纔不會感覺到視覺上的不一致。(概念上類似於被動監聽事件和主動監聽事件的區別。)React 爲此提供了一個額外的 useLayoutEffect Hook 來處理這類 effect。它和 useEffect 的結構相同,區別只是調用時機不同。

雖然 useEffect 會在瀏覽器繪製後延遲執行,但會保證在任何新的渲染前執行。React 將在組件更新前刷新上一輪渲染的 effect。

effect 的條件執行

默認情況下,effect 會在每輪組件渲染完成後執行。這樣的話,一旦 effect 的依賴發生變化,它就會被重新創建。

然而,在某些場景下這麼做可能會矯枉過正。比如,在上一章節的訂閱示例中,我們不需要在每次組件更新時都創建新的訂閱,而是僅需要在 source prop 改變時重新創建。

要實現這一點,可以給 useEffect 傳遞第二個參數,它是 effect 所依賴的值數組。更新後的示例如下:

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

此時,只有當 props.source 改變後纔會重新創建訂閱。

注意

如果你要使用此優化方式,請確保數組中包含了所有外部作用域中會發生變化且在 effect 中使用的變量,否則你的代碼會引用到先前渲染中的舊變量。請參閱文檔,瞭解更多關於如何處理函數 以及數組頻繁變化時的措施 的內容。

如果想執行只運行一次的 effect(僅在組件掛載和卸載時執行),可以傳遞一個空數組([])作爲第二個參數。這就告訴 React 你的 effect 不依賴於 props 或 state 中的任何值,所以它永遠都不需要重複執行。這並不屬於特殊情況 —— 它依然遵循輸入數組的工作方式。

如果你傳入了一個空數組([]),effect 內部的 props 和 state 就會一直持有其初始值。儘管傳入 [] 作爲第二個參數有點類似於 componentDidMount 和 componentWillUnmount 的思維模式,但我們有 更好的 方式 來避免過於頻繁的重複調用 effect。除此之外,請記得 React 會等待瀏覽器完成畫面渲染之後纔會延遲調用 useEffect,因此會使得處理額外操作很方便。

我們推薦啓用 eslint-plugin-react-hooks 中的 exhaustive-deps 規則。此規則會在添加錯誤依賴時發出警告並給出修復建議。

依賴項數組不會作爲參數傳給 effect 函數。雖然從概念上來說它表現爲:所有 effect 函數中引用的值都應該出現在依賴項數組中。未來編譯器會更加智能,屆時自動創建數組將成爲可能。

useContext

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 的參數必須是 context 對象本身

  • 正確: 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 的更多信息。

額外的 Hook

以下介紹的 Hook,有些是上一節中基礎 Hook 的變體,有些則僅在特殊情況下會用到。不用特意預先學習它們。

useReducer

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

指定初始 state

有兩種不同初始化 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>
    </>
  );
}

跳過 dispatch

如果 Reducer Hook 的返回值與當前 state 相同,React 將跳過子組件的渲染及副作用的執行。(React 使用 Object.is 比較算法 來比較 state。)

需要注意的是,React 可能仍需要在跳過渲染前再次渲染該組件。不過由於 React 不會對組件樹的“深層”節點進行不必要的渲染,所以大可不必擔心。如果你在渲染期間執行了高開銷的計算,則可以使用 useMemo 來進行優化。

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

返回一個 memoized 回調函數。

把內聯回調函數及依賴項數組作爲參數傳入 useCallback,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時纔會更新。當你把回調函數傳遞給經過優化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時,它將非常有用。

useCallback(fn, deps) 相當於 useMemo(() => fn, deps)

注意

依賴項數組不會作爲參數傳給回調函數。雖然從概念上來說它表現爲:所有回調函數中引用的值都應該出現在依賴項數組中。未來編譯器會更加智能,屆時自動創建數組將成爲可能。

我們推薦啓用 eslint-plugin-react-hooks 中的 exhaustive-deps 規則。此規則會在添加錯誤依賴時發出警告並給出修復建議。

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一個 memoized 值。

把“創建”函數和依賴項數組作爲參數傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算。

記住,傳入 useMemo 的函數會在渲染期間執行。請不要在這個函數內部執行與渲染無關的操作,諸如副作用這類的操作屬於 useEffect 的適用範疇,而不是 useMemo

如果沒有提供依賴項數組,useMemo 在每次渲染時都會計算新的值。

你可以把 useMemo 作爲性能優化的手段,但不要把它當成語義上的保證。將來,React 可能會選擇“遺忘”以前的一些 memoized 值,並在下次渲染時重新計算它們,比如爲離屏組件釋放內存。先編寫在沒有 useMemo 的情況下也可以執行的代碼 —— 之後再在你的代碼中添加 useMemo,以達到優化性能的目的。

注意

依賴項數組不會作爲參數傳給“創建”函數。雖然從概念上來說它表現爲:所有“創建”函數中引用的值都應該出現在依賴項數組中。未來編譯器會更加智能,屆時自動創建數組將成爲可能。

我們推薦啓用 eslint-plugin-react-hooks 中的 exhaustive-deps 規則。此規則會在添加錯誤依賴時發出警告並給出修復建議。

useRef

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 來實現。

useImperativeHandle

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()

useLayoutEffect

其函數簽名與 useEffect 相同,但它會在所有的 DOM 變更之後同步調用 effect。可以使用它來讀取 DOM 佈局並同步觸發重渲染。在瀏覽器執行繪製之前,useLayoutEffect 內部的更新計劃將被同步刷新。

儘可能使用標準的 useEffect 以避免阻塞視覺更新。

提示

如果你正在將代碼從 class 組件遷移到使用 Hook 的函數組件,則需要注意 useLayoutEffect 與 componentDidMountcomponentDidUpdate 的調用階段是一樣的。但是,我們推薦你一開始先用 useEffect,只有當它出問題的時候再嘗試使用 useLayoutEffect

如果你使用服務端渲染,請記住,無論 useLayoutEffect 還是 useEffect 都無法在 Javascript 代碼加載完成之前執行。這就是爲什麼在服務端渲染組件中引入 useLayoutEffect 代碼時會觸發 React 告警。解決這個問題,需要將代碼邏輯移至 useEffect 中(如果首次渲染不需要這段邏輯的情況下),或是將該組件延遲到客戶端渲染完成後再顯示(如果直到 useLayoutEffect 執行之前 HTML 都顯示錯亂的情況下)。

若要從服務端渲染的 HTML 中排除依賴佈局 effect 的組件,可以通過使用 showChild && <Child /> 進行條件渲染,並使用 useEffect(() => { setShowChild(true); }, []) 延遲展示組件。這樣,在客戶端渲染完成之前,UI 就不會像之前那樣顯示錯亂了。

useDebugValue

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;
}

提示

我們不推薦你向每個自定義 Hook 添加 debug 值。當它作爲共享庫的一部分時才最有價值。

延遲格式化 debug 值

在某些情況下,格式化值的顯示可能是一項開銷很大的操作。除非需要檢查 Hook,否則沒有必要這麼做。

因此,useDebugValue 接受一個格式化函數作爲可選的第二個參數。該函數只有在 Hook 被檢查時纔會被調用。它接受 debug 值作爲參數,並且會返回一個格式化的顯示值。

例如,一個返回 Date 值的自定義 Hook 可以通過格式化函數來避免不必要的 toDateString 函數調用:

useDebugValue(date, date => date.toDateString());

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章