【譯】在React Hooks 中使用useReducer的幾種用例

sundjly發表於2019-04-13

原文:How to use useReducer in React Hooks for performance optimization

github的地址 歡迎star!
React Hook 出來已經有一段時間了,具體的一些用法以及它解決的痛點,可以檢視 Dan 的兩篇文章 useEffect 完整指南以及編寫有彈性的元件進行詳細瞭解。

本文主要是介紹了6種在 React Hooks 使用 useReducer 的不同的方法

前言

React Hooks API正式在 React V16.8 版本釋出了。這篇部落格,主要是介紹了其中 useReducer 的各種用法示例。在讀之前,你確保你已經看過React Hooks官方指南

useReducer hook 屬於官方擴充套件的 hooks:

是 useState 的另一種替代。它接受(state, action) => newState,並且返回了一個與當前state成對的dispatch的方法。(如果你熟悉 Redux ,你也很快就能理解它是怎麼工作的。)

儘管 useReducer 是擴充套件的 hook, 而 useState 是基本的 hook,但 useState 實際上執行的也是一個 useReducer。這意味著 useReducer 是更原生的,你能在任何使用 useState 的地方都替換成使用 useReducer。Reducer 如此給力,所以有各種各樣的用例。

本文接下來就介紹了幾種代表性的用例。每個例子都代表一種特定的用例,都有相關的程式碼。

用例1:最小(簡單)的模式

可以看這個簡單示例的程式碼。下文都是用這個計數的例子做延伸的。

const initialState = 0;
const reducer = (state, action) => {
  switch (action) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'reset': return 0;
    default: throw new Error('Unexpected action');
  }
};
複製程式碼

首先,我們定義了初始化的 initialState 以及 reducer。注意這裡的 state 僅是一個數字,不是物件。熟悉 Redux 的開發者可能是困惑的,但在 hook 中是適宜的。此外,action 僅是一個普通的字串。

下面是一個使用 useReducer 的元件。

const Example01 = () => {
  const [count, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      {count}
      <button onClick={() => dispatch('increment')}>+1</button>
      <button onClick={() => dispatch('decrement')}>-1</button>
      <button onClick={() => dispatch('reset')}>reset</button>
    </div>
  );
};
複製程式碼

當使用者點選一個按鈕,它就會 dispatch 一個 action 來更新計數值 count,頁面就會展示更新之後 count。你可以在 reducer中儘可能多定義 action,但這種模式有侷限,它的 action是有限的。

下面是完整的程式碼:

import React, { useReducer } from 'react';

const initialState = 0;
const reducer = (state, action) => {
  switch (action) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'reset': return 0;
    default: throw new Error('Unexpected action');
  }
};

const Example01 = () => {
  const [count, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      {count}
      <button onClick={() => dispatch('increment')}>+1</button>
      <button onClick={() => dispatch('decrement')}>-1</button>
      <button onClick={() => dispatch('reset')}>reset</button>
    </div>
  );
};

export default Example01;
複製程式碼

用例2:action是一個物件

這個例子 Redux的使用者是熟悉的。我們使用了 state物件以及一個 action物件。

const initialState = {
  count1: 0,
  count2: 0,
};
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment1':
      return { ...state, count1: state.count1 + 1 };
    case 'decrement1':
      return { ...state, count1: state.count1 - 1 };
    case 'set1':
      return { ...state, count1: action.count };
    case 'increment2':
      return { ...state, count2: state.count2 + 1 };
    case 'decrement2':
      return { ...state, count2: state.count2 - 1 };
    case 'set2':
      return { ...state, count2: action.count };
    default:
      throw new Error('Unexpected action');
  }
};
複製程式碼

在 state中存放了兩個數字。我們能使用複雜的物件表示 state,只要能把 reducer組織好(列如:react-redux中 combineReducers)。另外,因為 action是一個物件,除了 type值,你還可以給它新增其他屬性像action.count。這個例子中 reducer 是有點雜亂的,但不妨礙我們下面這樣使用它:

const Example02 = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <div>
        {state.count1}
        <button onClick={() => dispatch({ type: 'increment1' })}>+1</button>
        <button onClick={() => dispatch({ type: 'decrement1' })}>-1</button>
        <button onClick={() => dispatch({ type: 'set1', count: 0 })}>reset</button>
      </div>
      <div>
        {state.count2}
        <button onClick={() => dispatch({ type: 'increment2' })}>+1</button>
        <button onClick={() => dispatch({ type: 'decrement2' })}>-1</button>
        <button onClick={() => dispatch({ type: 'set2', count: 0 })}>reset</button>
      </div>
    </>
  );
};
複製程式碼

注意到 state 中有兩個計數器,定義各自相應的 action 型別來更新它們。線上示例點選這裡

用例3:使用多個useReducer

上面的單個 state 中出現兩個計數器,這就是一種典型的全域性 state 的方法。但我們僅僅需要使用本地(區域性)的 state,故有另外一種方法,可以使用 useReducer 兩次。

const initialState = 0;
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'set': return action.count;
    default: throw new Error('Unexpected action');
  }
};
複製程式碼

這裡的 state 是一個數字,而不是物件,它和用例1中是一致的。注意不同點這裡的 action 是一個物件。

元件如何使用呢

const Example03 = () => {
  const [count1, dispatch1] = useReducer(reducer, initialState);
  const [count2, dispatch2] = useReducer(reducer, initialState);
  return (
    <>
      <div>
        {count1}
        <button onClick={() => dispatch1({ type: 'increment' })}>+1</button>
        <button onClick={() => dispatch1({ type: 'decrement' })}>-1</button>
        <button onClick={() => dispatch1({ type: 'set', count: 0 })}>reset</button>
      </div>
      <div>
        {count2}
        <button onClick={() => dispatch2({ type: 'increment' })}>+1</button>
        <button onClick={() => dispatch2({ type: 'decrement' })}>-1</button>
        <button onClick={() => dispatch2({ type: 'set', count: 0 })}>reset</button>
      </div>
    </>
  );
};
複製程式碼

可以看到,每個計數器有各自的 dispatch 的方法,但共享了 reducer 方法。這個的功能和用例2是一致的。

用例4:文字輸入(TextInput)

來看一個真實的例子,多個 useReducer 能各司其職。我們以 React 原生的 input 輸入元件為例,在本地狀態 state 中儲存文字資料。通過呼叫 dispatch 函式更新文字狀態值。

const initialState = '';
const reducer = (state, action) => action;
複製程式碼

注意到每次呼叫 reducer,之前舊的 state 就會被丟掉。具體使用如下:

const Example04 = () => {
  const [firstName, changeFirstName] = useReducer(reducer, initialState);
  const [lastName, changeLastName] = useReducer(reducer, initialState);
  return (
    <>
      <div>
        First Name:
        <TextInput value={firstName} onChangeText={changeFirstName} />
      </div>
      <div>
        Last Name:
        <TextInput value={lastName} onChangeText={changeLastName} />
      </div>
    </>
  );
};
複製程式碼

就是這麼簡單。當然你也可以新增一些校驗的邏輯在裡面。完整程式碼:

import React, { useReducer } from 'react';

const initialState = '';
const reducer = (state, action) => action;

const Example04 = () => {
  const [firstName, changeFirstName] = useReducer(reducer, initialState);
  const [lastName, changeLastName] = useReducer(reducer, initialState);
  return (
    <>
      <div>
        First Name:
        <TextInput value={firstName} onChangeText={changeFirstName} />
      </div>
      <div>
        Last Name:
        <TextInput value={lastName} onChangeText={changeLastName} />
      </div>
    </>
  );
};

// ref: https://facebook.github.io/react-native/docs/textinput
const TextInput = ({ value, onChangeText }) => (
  <input type="text" value={value} onChange={e => onChangeText(e.target.value)} />
);

export default Example04;
複製程式碼

用例5:Context

有些時候,我希望在元件之間共享狀態(理解為實現全域性狀態 state )。通常,全域性狀態會限制元件的複用,因此我們首先考慮使用本地 state,通過 props 進行傳遞( dispatch 來改變),但當它不是那麼方便的時候(理解巢狀傳遞過多),就可以使用 Context。如果你熟悉 Context 的 API,請點選檢視官方文件

這個例子,使用了和用例3一樣的 reducer。接下來看怎麼建立一個 context。

const CountContext = React.createContext();

const CountProvider = ({ children }) => {
  const contextValue = useReducer(reducer, initialState);
  return (
    <CountContext.Provider value={contextValue}>
      {children}
    </CountContext.Provider>
  );
};

const useCount = () => {
  const contextValue = useContext(CountContext);
  return contextValue;
};
複製程式碼

useCount 就是自定義的 hook,它也和其他官方的 hook 一樣使用。如下面一樣:

const Counter = () => {
  const [count, dispatch] = useCount();
  return (
    <div>
      {count}
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>
    </div>
  );
};
複製程式碼

contextValue就是 useReducer 返回的結果,我們也用 hook 重構了useCount。注意到這個點,使用了哪一個 context是不固定的。

最後,像這樣使用 context:

const Example05 = () => (
  <>
    <CountProvider>
      <Counter />
      <Counter />
    </CountProvider>
    <CountProvider>
      <Counter />
      <Counter />
    </CountProvider>
  </>
);
複製程式碼

如上所示,有兩個 CountProvider 元件,意味著有兩個計數器,即使我們只使用了一個 context。 在同一個 CountProvider 元件中計數器共享狀態 state。你可以執行一下這個用例瞭解它是怎麼工作的。點選這裡檢視

用例6:Subscription (訂閱)

在hooks中實現元件共享狀態 state 首選的應該就是 Context,但當在 React 元件的外部早已經有一個共享狀態 state 時,該怎麼(共享呢)?專業的做法訂閱監聽狀態 state,當共享狀態 state 更新時,更新元件。當然它還有一些侷限性,不過React官方提供了一個公共功能 create-subscription,你可以用它來進行訂閱。

不幸的,這個公共方法包還沒有用 React Hooks 進行重寫,現在只能靠我們自己用 hooks 盡力去實現。讓我們不使用 Context 實現和用例 5 一樣的功能。

首先,建立一個自定義的hook:

const useForceUpdate = () => useReducer(state => !state, false)[1];
複製程式碼

這個 reducer 僅僅是對先前的 state 取反,忽略了 action。[1]僅僅返回了 dispatch 而沒有 state。接下來,主函式實現共享狀態 state 以及返回自定義 hook:

const createSharedState = (reducer, initialState) => {
  const subscribers = [];
  let state = initialState;
  const dispatch = (action) => {
    state = reducer(state, action);
    subscribers.forEach(callback => callback());
  };
  const useSharedState = () => {
    const forceUpdate = useForceUpdate();
    useEffect(() => {
      const callback = () => forceUpdate();
      subscribers.push(callback);
      callback(); // in case it's already updated
      const cleanup = () => {
        const index = subscribers.indexOf(callback);
        subscribers.splice(index, 1);
      };
      return cleanup;
    }, []);
    return [state, dispatch];
  };
  return useSharedState;
};
複製程式碼

我們使用了 useEffect。它是非常重要的 hook,你需要仔細的看官方文件學習如何使用它。在 useEffect 中,我們訂閱了一個回撥函式來強制更新元件。在元件銷燬的時候需要清除訂閱。

接下來,我們可以建立兩個共享狀態 state。使用了和用例5,用例3一樣的 reducer 和初始值 initialState:

const useCount1 = createSharedState(reducer, initialState);
const useCount2 = createSharedState(reducer, initialState);
複製程式碼

這和用例 5 是不一樣的,這兩個 hooks 繫結了特定的共享狀態 state。然後我們使用這兩個 hooks。

const Counter = ({ count, dispatch }) => (
  <div>
    {count}
    <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
    <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
    <button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>
  </div>
);

const Counter1 = () => {
  const [count, dispatch] = useCount1();
  return <Counter count={count} dispatch={dispatch} />
};

const Counter2 = () => {
  const [count, dispatch] = useCount2();
  return <Counter count={count} dispatch={dispatch} />
};
複製程式碼

注意到,Counter 元件是一個共同的無狀態元件。這樣子使用:

const Example06 = () => (
  <>
    <Counter1 />
    <Counter1 />
    <Counter2 />
    <Counter2 />
  </>
);
複製程式碼

可以看到,我們沒有用 Context,但是也實現了共享狀態。大家應該都要具體看看 useReducer,對於效能優化很有幫助。

文中所有的程式碼都在這裡,要看線上示例點選這裡檢視

自己的一些總結:

  1. hooks每次 Render 都有自己的 Props 與 State 可以認為每次 Render 的內容都會形成一個快照並保留下來(函式被銷燬了,但變數被react保留下來了),因此當狀態變更而 Rerender 時,就形成了 N 個 Render 狀態,而每個 Render 狀態都擁有自己固定不變的 Props 與 State。 這也是函式式的特性--資料不變性
  2. 效能注意事項 useState 函式的引數雖然是初始值,但由於整個函式都是 Render,因此每次初始化都會被呼叫,如果初始值計算非常消耗時間,建議使用函式傳入,這樣只會執行一次:
  3. 如果你熟悉 React 的 class 元件的生命週期,你可以認為useEffect Hook就是組合了componentDidMount, componentDidUpdate, 以及 componentWillUnmount(在useEffect的回撥中),但是又有區別,useEffect不會阻止瀏覽器更新螢幕
  4. hooks 把相關的邏輯放在一起統一處理,不在按生命週期把邏輯拆分開
  5. useEffect 是在瀏覽器 render 之後觸發的,想要實現 DOM 改變時同步觸發的話,得用 useLayoutEffect,在瀏覽器重繪之前和 DOM 改變一起,同步觸發的。不過儘量在佈局的時候,其他的用標準的 useEffect,這樣可以避免阻止檢視更新。
```
function FunctionComponent(props) {
  const [rows, setRows] = useState(() => createRows(props.count));
}
useRef 不支援這種特性,需要寫一些[冗餘的函判定是否進行過初始化。](https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily)
```
複製程式碼

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!

參考

  1. 精讀《useEffect 完全指南》
  2. react 官網

相關文章