學習 React Hooks

  • 排版有點亂,好像不支持字體自定義顏色,你可以去看我的博客

動機

  • Hook 解決了我們五年來編寫和維護成千上萬的組件遇到的各種各樣看起來不相關的問題。

Hooks 的優點

  • 組件之間 Hook 使你在無需修改組件結構的情況下復用狀態邏輯。

  • 組件周期之間互相關聯的代碼進行拆分, Hook 將組件中互相關聯的部分拆分成更小的函數 并非強制按照生命周期劃分。還可以使用 reducer 來管理組件的內容狀態,使其更加可預測。具體可看 Effect Hook。

  • 難以理解的 class,Hook 使你在非 class 的情況下可以使用更多的 React 特性 ,React 組件一直更像是函數,而 Hook 則擁抱了函數,同時也解決了 class 常見的 this 的問題。

但是 React 官方并不計劃從 React 中移除 class

什么是 Hook

  • Hook 是一種特殊的函數,可以讓你在函數組件里“鉤入” React state 及生命周期等特性的函數。Hook 不能在 class 組件中使用 —— 這使得你不使用 class 也能使用 React。

Hook 使用規則

  • Hook 就是JavaScript 函數,但是使用他們會有兩個額外的規則:
    • 只能在 函數最外層調用 Hook。不要在 循環、條件判斷或者子函數中調用
    • 只能在 React 的函數組件 中調用 Hook。不要在其他 JavaScript 函數中調用。

useState

  • 調用 useState 方法的時候做了什么?

    • 它定義了一個 state 變量,可以是任何名字。這是一種在函數調用時保存變量的方式,它與 class 里面的 this.state 提供的功能完全相同
  • 需要哪些參數?

    • useState() 方法里面唯一的參數就是初始化 state。不同于 class 的是,我們使用時可以按照數字或者字符串對其進行復制,對象也可以。
  • useState 方法的返回值是什么?

    • 返回值為:當前 state 以及更新 state 的函數。這是寫 const [count, setCount] = useState(0) 的原因。
    import React, { useState } from 'react';
    
    function Example() {
      // 聲明一個叫 "count" 的 state 變量 0 是初始化默認值
      const [count, setCount] = useState(0);
    }
    
  • 讀取 State

  <p>You clicked {this.state.count} times</p>
  • 更新
<button onClick={() => setCount(count + 1)}>
  Click me
</button>

useEffect

  • useEffect Hook 可以看做是 componentDidMount,componentDidUpdate,componentWillUnmount 這三個函數的組合

無需清理的 effect

  • 當我們想在 React 更新 DOM 之后運行一些額外的代碼。比如發送網絡請求,手動變更 DOM,記錄日志,這些都是常見的無需清除的操作

使用 Hook 的示例

import React, { useState, useEffect } from 'react'

function Example() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    document.title = `You clicked ${count} times`
  })

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
  • 上面代碼每次點擊都會更新 title

  • useEffect 做了什么? 通過使用這個 Hook,你可以告訴 React 組件需要在渲染后執行某些操作。React 會保存你傳遞的函數(我們將它稱之為 “effect”),并且在執行 DOM 更新之后調用它。在這個 effect 中,我們設置了 document 的 title 屬性,不過我們也可以執行數據獲取或調用其他命令式的 API。

  • 為什么在組件內部調用 useEffect? 將 useEffect 放在組件內部讓我們可以在 effect 中直接訪問 count state 變量(或其他 props)。我們不需要特殊的 API 來讀取它 —— 它已經保存在函數作用域中。Hook 使用了 JavaScript 的閉包機制,而不用在 JavaScript 已經提供了解決方案的情況下,還引入特定的 React API

  • useEffect 會在每次渲染后都執行嗎? 是的,默認情況下,它在第一次渲染之后和每次更新之后都會執行。(下面會講到性能優化如何控制它。)你可能會更容易接受 effect 發生在“渲染之后”這種概念,不用再去考慮“掛載”還是“更新”。React 保證了每次運行 effect 的同時,DOM 都已經更新完畢。

需要清理的 Effect

  • 之前,我們研究了如何使用不需要清除的副作用,還有一些副作用是需要清除的。例如訂閱外部數據源。這種情況下,清除工作是非常重要的,可以防止引起內存泄露!現在讓我們來比較一下如何用 Class 和 Hook 來實現。

使用 Class 的示例

  • 在 React class 中,通常會在 componentDidMount 中設置訂閱,并在 componentWillUnmount 中清除它。例如,最常見的例如我們綁定 document 事件
componentDidMount() {
  document.addEventListener(
    'mousemove',
    onMoveStart,
    false
  )
}
componentWillUnmount() {
  document.removeEventListener('mousemove', onMoveStart, false)
}
  
  • 你會注意到 componentDidMount 和 componentWillUnmount 之間相互對應。使用生命周期函數迫使我們拆分這些邏輯代碼,即使這兩部分代碼都作用于相同的副作用。

使用 Effect

useEffect(() => {
  document.addEventListener(
    'mousemove',
    onMoveStart,
    false
  )
  return () => {
    document.removeEventListener('mousemove', onMoveStart, false)
  }
})
  • 為什么要在 effect 中返回一個函數? 這是 effect 可選的清除機制。每個 effect 都可以返回一個清除函數。如此可以將添加和移除訂閱的邏輯放在一起。它們都屬于 effect 的一部分。

  • React 何時清除 effect? React 會在組件卸載的時候執行清除操作。正如之前學到的,effect 在每次渲染的時候都會執行。這就是為什么 React 會在執行當前 effect 之前對上一個 effect 進行清除。

使用多個 Effect 實現關注點分離

  • 使用 Hook 其中一個目的就是要解決 class 中生命周期函數經常包含不相關的邏輯,但又把相關邏輯分離到了幾個不同方法中的問題。

  • Hook 允許我們按照代碼的用途分離他們, 而不是像生命周期函數那樣。React 將按照 effect 聲明的順序依次調用組件中的每一個 effect。

Effect 進行性能優化

  • 在某些情況下,每次渲染后都執行清理或者執行 effect 可能會導致性能問題。 在 class 組件中,我們可以通過在 componentDidUpdate 中添加對 prevProps 或 prevState 的比較邏輯解決

  • 例如使用 class 優化上面的代碼

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`
  }
}
  • useEffect
    如果某些特定值在兩次重渲染之間沒有發生變化,你可以通知 React 跳過對 effect 的調用,只要傳遞數組作為 useEffect 的第二個可選參數即可:
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]);

傳入 [count] 作為第二個參數,這個參數是什么作用呢? 如果 count 的值是 5,而且我們的組件重渲染的時候 count 還是等于 5,React 將對前一次渲染的 [5] 和后一次渲染的 [5] 進行比較。因為數組中的所有元素都是相等的(5 === 5),React 會跳過這個 effect,這就實現了性能的優化。如果 count 值改變,React 會比較上一次的值,如果改變,React 會執行 effect, 數組是可以有多個元素的,如果其中一個改變,React 也會執行 effect。

  • 如果想執行只運行一次的 effect(僅在組件掛載和卸載時執行, 可以傳遞一個空數組([])作為第二個參數。這就告訴 React 你的 effect 不依賴于 props 或 state 中的任何值,所以它永遠都不需要重復執行。

自定義 Hook

  • 自定義 Hook 是一個函數,其名稱以 “use” 開頭,函數內部可以調用其他的 Hook。
創建自定義 Hook
import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

使用自定義 Hook

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

自定義 Hook 只是將兩個函數之間一些共同的代碼提取到單獨的函數中。自定義 Hook 是一種自然遵循 Hook 設計的約定,而并不是 React 的特性。

  • 自定義 Hook 必須以 “use” 開頭嗎? 必須如此。這個約定非常重要。 不遵循的話,由于無法判斷某個函數是否包含對其內部 Hook 的調用,React 將無法自動檢查你的 Hook 是否違反了 Hook 的規則

  • 在兩個組件中使用相同的 Hook 會共享 state 嗎? 不會。 自定義 Hook 是一種重用狀態邏輯的機制(例如設置為訂閱并存儲當前值),所以每次使用自定義 Hook 時,其中的所有 state 和副作用都是完全隔離的。

  • 自定義 Hook 如何獲取獨立的 state? 每次調用 Hook,它都會獲取獨立的 state。 由于我們直接調用了 useFriendStatus,從 React 的角度來看,我們的組件只是調用了 useState 和 useEffect。我們是可以在一個組件中多次調用 useState 和 useEffect,它們是完全獨立的。

在多個 Hook 之間傳遞信息

  • 由于 Hook 本身就是函數,因此我們可以在它們之間傳遞信息。
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);

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)

:::warning

如果你在接觸 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>
  );
}

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);
  • useState 的替代方案。它接收一個形如 (state, action) => newState 的 reducer,并返回當前的 state 以及與其配套的 dispatch 方法。(如果你熟悉 Redux 的話,就已經知道它如何工作了。)
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 (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </div>
  );
}

指定初始 state

  • 有兩種不同初始化 useReducer state 的方式,你可以根據使用場景選擇其中的一種。將初始 state 作為第二個參數傳入 useReducer 是最簡單的方法:
const [state, dispatch] = useReducer(
  reducer,
  {count: initialCount}
)

::: warning
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 (
    <div>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </div>
  )
}

跳過 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)。

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 把“創建”函數和依賴項數組作為參數傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。這種優化有助于避免在每次渲染時都進行高開銷的計算。

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

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

  • 注意: 你可以把 useMemo 作為性能優化的手段,但不要把它當成語義上的保證。

useRef

const refContainer = useRef(initialValue);
  • useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(initialValue)。返回的 ref 對象在組件的整個生命周期內保持不變。

  • 例如:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已掛載到 DOM 上的文本輸入元素
    inputEl.current.focus();
  };
  return (
    <div>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  );
}
  • 本質上,useRef 就像是可以在其 .current 屬性中保存一個可變值的“盒子”。

  • 將 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 以避免阻塞視覺更新。

::: warning

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

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380