React Hook 概覽與最佳實踐

Hook 簡介

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

動機

Hook 解決了如下問題react

在組件之間複用狀態邏輯很難

  • React 沒有提供將可複用性行爲 「附加」 到組件的途徑(例如,把組件鏈接到 store)。
  • 解決此類問題可使用 render props 和 高階組件,可是這類方案須要從新組織你的組件結構,使你的代碼難以理解。
  • 由 providers,consumers,高階組件,render props 等其餘抽象層組成的組件會造成 「嵌套地獄」。

使用 Hook 從組件中提取狀態邏輯,使得這些邏輯能夠單獨測試並複用。Hook 使你在無需修改組件結構的狀況下複用狀態邏輯。 這使得在組件間或社區內共享 Hook 變得更便捷。typescript

複雜組件變得難以理解

  • class 組件的狀態邏輯分散在各個生命週期內,每一個生命週期內包含的都是一些不相關的邏輯。
  • 由於狀態邏輯無處不在,class 組件難以被拆分爲更小的粒度,增長了測試的難度。
  • 引入狀態管理庫能夠將狀態集中一處,但同時引入了不少抽象概念,使複用變得更加困難。

Hook 將組件中相互關聯的部分拆分紅更小的函數(好比設置訂閱或請求數據),而並不是強制按照生命週期劃分。你還可使用 reducer 來管理組件的內部狀態,使其更加可預測。編程

難以理解的 class

  • 學習 class 有很大成本,並且必需要理解 Javascript 中的 this 的工做方式,與其餘語言存在巨大差別。
  • 使用 class 組件會無心中鼓勵開發者使用一些讓優化措施無效的方案。
  • 給工具帶來的問題:例如,class 不能很好的壓縮。

Hook 使你在非 class 的狀況下可使用更多的 React 特性,它擁抱了函數,同時沒有犧牲 React 的精神原則。無需學習複雜的函數式或響應式編程技術。數組

Hook 概覽

  • 基礎 Hook
    • useState
    • useEffect —— 反作用,class 組件生命週期的映射
    • useContext
  • 額外的 Hook
    • useReducer
    • useCallback —— 性能優化
    • useMemo —— 性能優化
    • useRef —— 花式玩法
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue

爲了讓你們快速瞭解 hook,如下內容涵蓋了大部分功能應用場景。性能優化

更多更詳細的用法請閱讀官方文檔數據結構

基礎 Hook

useState

如下是一段 useState 的示例,一個計數器組件:異步

import React, { useState } from 'react';

function Example() {
  // 聲明一個叫 "count" 的 state 變量
  const [count, setCount] = useState(0);

  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
  );
}
複製代碼

它的等價 class 示例爲:ide

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div>
    );
  }
}
複製代碼
  • useState 是一種新方法,它與 class 裏面的 this.state 提供的功能徹底相同。
  • useState 可使用數字或字符串對其進行賦值,並不必定是對象。保存多個狀態能夠屢次調用 useState
  • useState 返回當前 state 以及更新 state 的函數,使用數組解構的方式定義這兩個變量。
  • React 會確保 useState 返回的更新 state 的函數的標識是穩定的,而且不會在組件從新渲染時發生變化。

useEffect

Effect Hook 可讓你在函數組件中執行反作用操做(數據獲取,設置訂閱以及手動更改 React 組件中的 DOM 都屬於反作用)函數

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

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

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
  );
}
複製代碼

咱們爲計數器增長了一個小功能:將 document 的 title 設置爲包含了點擊次數的消息。

  • useEffect 用於定義每次 React 更新 DOM 以後執行的反作用。
  • useEffect 接收數組做爲第二個參數,經過對比數組中的參數發生改變來決定是否執行傳給 useEffect 的函數。不傳入第二個參數則每次渲染後都執行,傳入空數組則表明只在第一次渲染後執行。
  • useEffect 接收的函數能夠返回一個函數用於清除反作用。(例如,取消訂閱,清理計時器)
  • useEffect 執行前會先清除上一次渲染的反作用。

useEffect 實際上涵蓋了 class 組件的絕大部分生命週期,詳情查看 useEffect 與class 組件生命週期映射關係 一節。

useContext

const value = useContext(MyContext);
複製代碼
  • useContext 接收一個 context 對象(React.createContext 的返回值)並返回該 context 的當前值。
  • useContext 所在的組件在 provider 的值發生變化時會從新渲染,即使祖先元素使用了 React.memoshouldComponentUpdate
  • useContext 僅僅增長了一種使用 Context 的方式,功能上並沒有區別。

額外 Hook

useReducer

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> </> ); } 複製代碼
  • useReducer 和 Redux 很像,接收 reducer 函數、初始值、初始化函數並返回一個完整的 state 和 dispatch 函數。
  • useReducer 適用於管理邏輯複雜且包含多個子值的 state,或者下一個 state 依賴以前的 state 等。
  • useReducer 能給觸發深更新的組件作性能優化。(向子組件傳遞 dispatch 而不是回調函數)
  • React 會確保 dispatch 函數的標識是穩定的,而且不會在組件從新渲染時發生變化。

useCallback、useMemo

這兩個有類似關聯之處,放在一塊兒。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
複製代碼
  • useCallback 接收內聯回調函數和依賴項數組做爲參數,它返回該函數的 memoized 版本,回調函數僅在某個依賴項改變時纔會更新。
  • useCallback 返回的函數傳遞給使用 shouldComponentUpdateReact.memo 的子組件時能夠避免非必要的渲染。
  • useCallback(fn, deps) 至關於 useMemo(() => fn, deps)
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製代碼
  • useMemo 接收 」計算「 函數和依賴項數組做爲參數,它返回一個 memoized 值,僅在某個依賴項改變時纔會從新計算 memoized 值。
  • useMemo 返回的值傳遞給使用 shouldComponentUpdateReact.memo 的子組件時能夠避免非必要的渲染。
  • useMemo 有助於避免在每次渲染時都進行高開銷的計算。這意味着若是是很簡單的計算請謹慎考慮是否使用。

useRef

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的參數。返回的 ref 對象在組件的整個生命週期內保持不變。 本質上,useRef 就像是能夠在其 .current 屬性中保存一個可變值的 「盒子」。

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>
    </>
  );
}
複製代碼

這是一種咱們比較熟悉的 ref 的使用方式,用於直接訪問真實 DOM。不過之前咱們更多的使用字符串 ref。然而 useRef 不止於此,它比 ref 屬性更有用。詳情查看 useRef 用例 一節。

Hook 規則

請移步官方文檔:Hook 規則

useEffect 與 class 組件生命週期映射關係

實質上這是個錯誤的標題,僅爲了更好的理解 Hook。在行爲上等效,並無實質關係。

useEffect 真正實現了讓相關聯的邏輯都在一處的想法,咱們能夠在 useEffect 中設置定時器,在返回的清理函數中清除定時器。沒必要像 class 組件同樣將這些本應該在一塊兒的邏輯分散在各個生命週期中。除此以外,相同的抽象邏輯能夠被抽離出來在不一樣的函數組件內複用。

如下是生命週期對照表:

class 組件生命週期 useEffect 示例代碼
componentDidMount useEffect(() => { // effect here }, [])
componentDidMount & componentDidUpdate useEffect(() => { // effect here })
componentDidUpdate useEffect(() => { if (firstRef.current) return; // effect here })
componentWillUnMount useEffect(() => () => { // clear effect here }, [])

useRef 用例

用做 DOM 的引用

這是咱們最熟悉的使用方式。

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>
    </>
  );
}
複製代碼

輔助實現 ComponentDidUpdate 生命週期

function LifeCycleExample() {
  const firstMountRef = useRef(true);
  useEffect(() => {
    if (firstMountRef.current) {
      firstMountRef.current = false;
    } else {
      // effect here
    }
  })
  return (<p>LifeCycleExample</p>);
}
複製代碼

若是頻繁使用,則能夠包裝成自定義 Hook:

function useUpdateEffect(effect) {
  const firstMountRef = useRef(true);
  useEffect(() => {
    if (firstMountRef.current) {
      firstMountRef.current = false;
    } else {
      effect();
    }
  });
}

function LifeCycleExample() {
  useUpdateEffect(() => {
    // effect here
  })
  return (<p>LifeCycleExample</p>);
}
複製代碼

保存上次渲染的狀態

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

  const prevCountRef = useRef();
  useEffect(() => {
    prevCountRef.current = count;
  });
  const prevCount = prevCountRef.current;

  return <h1>Now: {count}, before: {prevCount}</h1>;
}
複製代碼

若是頻繁使用,則能夠包裝成自定義 Hook:

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return <h1>Now: {count}, before: {prevCount}</h1>;
}
複製代碼

保存須要在異步回調中訪問最新 state 或變化過的函數

function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      // ...
    });
  });

  return (
    <> <button onClick={() => clearInterval(intervalRef.current)}>中止</button> </> ); } 複製代碼

以上只爲舉例,並不侷限於這幾種使用方式。

自定義 Hook

自定義 Hook 是一種天然遵循 Hook 設計的約定,而並非 React 的特性。自定義 Hook 必須使用 use 開頭的方式命名。經過自定義 Hook,能夠將組件邏輯提取到可重用的函數中。實際上在上一節 useRef 用例 中已經使用了自定義 Hook。

自定義 Hook 必須以 「use」 開頭嗎?必須如此。這個約定很是重要。不遵循的話,因爲沒法判斷某個函數是否包含對其內部 Hook 的調用,React 將沒法自動檢查你的 Hook 是否違反了 Hook 規則

Hook 分層設計

基礎層,即內置 Hook

這個很少說,不須要本身實現,官方直接提供。

簡化狀態更新邏輯,用 immer 等 immutable 庫包裝 Hook

這一層在工程層面實現,咱們使用 immutable 庫將官方提供的基礎 Hook 包裝一層,便於使用。immutable 並非爲 Hook 專門準備的,在 class 組件中咱們也能夠用相似的庫對狀態進行包裝。可是 Hook 這種能夠將狀態邏輯和組件分離的能力,提供了更好的封裝的可能性。這一層將會很優雅,不會增長開發中的理解難度。

一般咱們須要這樣更新深層次的狀態:

setState((oldValue) => ({
    ...oldValue,
    foo: {
        ...oldValue.foo,
        bar: {
            ...oldValue.foo.bar,
            alice: newAlice
        },
    },
}));
複製代碼

封裝後,直接修改狀態由 immer 保證數據不可變性:

const [state, setState] = useImmerState({foo: {bar: 1}});

setState(s => s.foo.bar++);
複製代碼
const [state, dispatch] = useImmerReducer(
    (state, action) => {
        case 'ADD':
            state.foo.bar += action.payload;
        case 'SUBTRACT':
            state.foo.bar -= action.payload;
        default:
            return;
    },
    {foo: {bar: 1}}
);

dispatch('ADD', {payload: 2});
複製代碼

數據結構的抽象,將與業務邏輯無關的數據結構操做單獨封裝

這個很好理解,好比將 ArrayMapSet 等複雜數據結構封裝爲 hook。

這裏使用一個 typescript 的接口定義來體現:

const [list, methods, setList] = useArray([]);

interface ArrayMethods<T> {
    push(item: T): void;
    unshift(item: T): void;
    pop(): void;
    shift(): void;
    slice(start?: number, end?: number): void;
    splice(index: number, count: number, ...items: T[]): void;
    remove(item: T): void;
    removeAt(index: number): void;
    insertAt(index: number, item: T): void;
    concat(item: T | T[]): void;
    replace(from: T, to: T): void;
    replaceAll(from: T, to: T): void;
    replaceAt(index: number, item: T): void;
    filter(predicate: (item: T, index: number) => boolean): void;
    union(array: T[]): void;
    intersect(array: T[]): void;
    difference(array: T[]): void;
    reverse(): void;
    sort(compare?: (x: T, y: T) => number): void;
    clear(): void;
}
複製代碼

通用場景封裝

在有了基本的數據結構後,能夠對場景進行封裝,如 useVirtualList 就是一個價值很是大的場景的封裝。須要注意的是,場景的封裝不該與組件庫耦合,它應當是業務與組件之間的橋樑,不一樣的組件庫使用相同的 Hook 實現不一樣的界面,這纔是一個理想的模式。

業務中比較經常使用的場景 Hooks:

Hook 狀態粒度

上一節中 Hook 分層設計 已經從某種程度上解決了一部分 Hook 使用的粒度問題。這裏簡單補充一下:

若是僅僅將 class 中的 state 平移過來當作一整個狀態,那分離狀態,將狀態複用的好處將徹底得不到體現。不相關的狀態堆砌在一塊兒,不只徹底沒法複用,還會隱藏其中通用的狀態。再者,若是每一個 state 都被單獨拆分出來,在一次觸發好幾個狀態變動時,咱們須要分別對其進行更新。代碼變的難以理解,增長維護難度。

咱們應從 Hook 的動機入手,實現關注點分離,將關聯的邏輯和狀態放在一塊兒。以可以拆分紅自定義的 Hook 達到複用的目的來設計 Hook 的狀態粒度。

函數組件性能優化(使用 Hook 時)

其實在上面對各項 Hook 作介紹時,咱們已經提到了幾種優化方式。在此處作一下總結。

跟 class 組件相似,性能優化的思路都是經過如下兩個方面入手:

  • 減小 render 的次數
  • 減小高開銷計算的次數

使用 React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});
複製代碼

React.memo 實際上和 Hook 關係不大,它是針對函數組件的一種性能優化方式。它於 React.PureComponent 很是類似,但只適用於函數組件。默認狀況下 React.memo 只對 props 和 prevProps 作淺層比較,但咱們能夠經過傳入第二個參數來控制比較過程。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /* 若是把 nextProps 傳入 render 方法的返回結果與 將 prevProps 傳入 render 方法的返回結果一致則返回 true, 不然返回 false */
}
export default React.memo(MyComponent, areEqual);
複製代碼

能夠將 areEqual 理解爲 React.Component 中由開發者本身控制的 shouldComponentUpdate

使用 useCallback

當使用 React.memoshouldComponentUpdate 來決定是否進行從新渲染時,則強烈依賴外部傳入的 props 的穩定性。因爲函數組件每次渲染都會執行一次,其內部定義的回調函數每次都是新的。這些回調函數傳遞給子組件時,即使使用 React.memoshouldComponentUpdate 也沒法實現指望的效果。

const Child = React.memo(function (props) {
  return (<button onClick={props.onClick}>點擊</button>)
})

function Parent() {
  // 經過 useCallback 進行記憶 callback,並將記憶的 callback 傳遞給 Child
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, /** deps = */ [])
  return (
    <>
      <p>Parent</p>
      <Child onClick={handleClick} />
    </>
  );
}
複製代碼

經過使用 useCallback,在依賴項沒有發生變化時,React 會確保 handleClick 函數的標識是穩定的,而且不會在組件從新渲染時發生變化。這樣 Child 接收到的 props 在 React.memo 對比中則沒有變化,Child 不會觸發從新渲染,達到性能優化的目的。

使用 useMemo

const Child = React.memo(function ({ button }) {
  return (<button>{button.text}</button>)
})

function Parent() {
  const button = React.useMemo(() => {
    // 此處有高開銷的計算過程
    return {
      text: '保存',
      // ...
    }
  }, [])
  return (
    <>
      <p>Parent</p>
      <Child button={button} />
    </>
  );
}
複製代碼

useCallback 類似,爲了不從新渲染,咱們可使用 useMemo 記憶計算過的值。當依賴項沒有發生變化時,高開銷的計算過程將會被跳過,useMemo 將返回相同的值(若是是引用值,則是相同的引用標識)。這樣 Child 接收到的 props 在 React.memo 對比中則沒有變化,Child 不會觸發從新渲染,達到性能優化的目的。

注意事項:使用 useMemo 在每次渲染時都會有函數從新定義的過程,計算量若是很小的計算函數,也能夠選擇不使用 useMemo,由於這點優化並不會做爲性能瓶頸的要點,反而可能使用錯誤還會引發一些性能問題。

參考文章

相關文章
相關標籤/搜索