翻譯|How to Use the useReducer Hook

phpsmarter發表於2019-04-28

原文:How to Use the useReducer Hook

在所有的新React Hooks,或許僅僅是因為名字,就可能成為使用最多的一個. "reducer"這個單詞會讓很多人聯想起Redux-但是讀本文,你不必事先理解Redux.

我們這裡要談的"reducer"實際問題是,如何利用useReducer的優點來管理元件中的複雜狀態(state),新的hook對於Redux意味著什麼?Redux需要hook嗎?(對不起,有點跑題).

[^譯註:結合Redux和useReducer來闡述問題,可能是一個很好的出發點, Redux的reducer和useReducer核心都是根據元件dispatch的Action的type,payload來對State物件進行更新.概念是完全一樣的,如果對Redux不是太瞭解, 可以藉助useReducer來理解這個過程. 留給你大腦的轉變過程是,如果兩者之間的這種相同點存在,可以遷移嗎?]

在本文中,我們會探討一下useReducer.在元件中管理複雜state,要比useState的方式厲害的多.

什麼是Reducer?

如果你熟悉Redux,或者陣列的reduce方法,你就應該知道reducer 是什麼?.如果你不熟悉,"reducer"是一個奇特的單詞,代表一個函式接收兩個值,返回一個值.

如果有一個陣列, 你想把其中的元素組合成單個值,"函數語言程式設計"的做法是使用陣列的reduce函式. 例如,如果你有一個陣列,元素是數字,你想得到數字的綜合, 可以編寫一個reducer函式,傳遞給陣列的reduce方法,例如:

let numbers = [1, 2, 3];
let sum = numbers.reduce((total, number) => {
  return total + number;
}, 0);
複製程式碼

如果之前沒看過這樣的用法,可能有點暈. 這裡所做的是針對陣列的每個元素呼叫函式,傳遞的引數是前一個total和當前的number.函式返回值成為新的total,第二個傳遞給reduce的引數(在這裡是0)就是total的初始值. 在這個例子中,輸入的函式將會呼叫三次:

  • 用個(0,1)呼叫,返回1
  • 用個(1,2)呼叫,返回3
  • 用個(3,3)呼叫,返回1
  • reduce返回6,結果儲存在sum中.

但是,這和useReducer有什麼關係?

我花了半頁的篇幅倆解釋陣列的reduce的原因是因為,useReducer接受相同的引數,基礎的工作是相同的.你傳遞一個reducer函式和初始值(initial state). reducer接收當前的state和一個action,返回一個新的state.我們可以寫一個類似的合計reducer:

useReducer((state,action)=>{
 Return state+action;
},0)
複製程式碼

那麼如何觸發這個操作? action是如何輸入函式的. 想到這個問題就對了.

[^譯註:這裡的這個問題絕對是學習Redux時,令人最困惑的地方]

useReducer返回有兩個元素的陣列,類似useState hook. 第一個元素是當前的state,第二個引數是dispatch函式. 實際的程式碼如下:

const [sum, dispatch] = useReducer((state, action) => {
  return state + action;
}, 0);
複製程式碼

注意"state"可以是任何值,不一定非要是一個物件. 可以是數字,陣列,任何東西.

接著來看一個使用reducer的完整元件例項:

import React, { useReducer } from 'react';

function Counter() {
  // 首次渲染會建立一個state,後續的渲染會儲存結果.
  const [sum, dispatch] = useReducer((state, action) => {
    return state + action;
  }, 0);

  return (
    <>
      {sum}

      <button onClick={() => dispatch(1)}>
        Add 1
      </button>
    </>
  );
}
複製程式碼

可以在CodeSandbox 試試

可以看到,點選按鈕,dispatch一個action,引數是1, 這個值會被加到當前的state上, 之後元件會用新的state(更大的值)來渲染元件.

我可以的把"action"寫成這樣.沒有使用{type:"INCREMENT_BY",value:1}的形式或者其他類似Redux的形式,因為reducer不一定必須要準守Redux的type模式.Hooks的世界是一個全新的世界:這一點很值得考慮,是否能發現舊有模式的價值,並保持它們,還是使用新的模式.

稍微複雜一點的例子

現在來看一個和典型Redux reducer 非常接近的例項.我們要建立一個元件管理購物車列表,同時也會使用另一個hook:useRef

首先匯入兩個hook:

import React,{useReducer,useRef} from 'react'
複製程式碼

接著建立元件,設定ref和reducer.ref保留對錶單輸入的引用,便於我們獲取表單的值(也可以通過元件內部state,傳遞value,onChange props來獲取值,但是用useRef可以很好的展現它的用法)

function ShoppingList() {
  const inputRef = useRef();
  const [items, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      // do something with the action
    }
  }, []);

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input ref={inputRef} />
      </form>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            {item.name}
          </li>
        ))}
      </ul>
    </>
  );
}
複製程式碼

注意,本例中的"state"是一個陣列.我們使用一個空陣列來初始化它,(傳遞給useReducer的第二個引數),後續會從reducer函式返回一個陣列.

useRef Hook

題外話解釋一下useRef的用法,之後在返回reducer話題.

useRefhook 可以讓我們建立一個DOM元素的持久化引用. 呼叫useRef會建立一個空的引用(可以傳遞引數進行初始化).返回的物件有一個current屬性,所以在例項中,我們可以通過inputRef.current來訪問DOM元素的輸入值. 如果你對React.createRef()很熟悉,這裡的工作原理是相同的.

useRef返回的物件不僅僅可以承載一個DOM元素的引用,它可以承載做元件內的任何特定值,並且在渲染中保持固定.耳熟! 必須的.

useRef也可用於建立泛型例項化變數,和React 類元件中的this.whatever=value做法一樣. 唯一的區別是要寫成"side effect"的形式,所以就不能在元件渲染過程中改變它了-只能在useEffect內部執行. 官方Hook問答 有例項講解.

回到useReducer的例子

from包裝input,在按下Enter鍵時觸發提交函式. 現在需要編寫handleSubmit函式,認為是把一個專案新增到列表上,還要在reducer中處理action

function ShoppingList() {
  const inputRef = useRef();
  const [items, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case 'add':
        return [
          ...state,
          {
            id: state.length,
            name: action.name
          }
        ];
      default:
        return state;
    }
  }, []);

  function handleSubmit(e) {
    e.preventDefault();
    dispatch({
      type: 'add',
      name: inputRef.current.value
    });
    inputRef.current.value = '';
  }

  return (
    // ... same ...
  );
}
複製程式碼

reducer函式有兩個分支: 一個是action:type==='add',預設分支:其他的任務.

當reduce獲取到"add" action 以後, 它會返回一個新的陣列包含了舊的元素,在末尾新增新的一條專案.

我們使用陣列的長度作為自增ID.在這個例項中用自增ID是可以的,但是在實際的app中,不太理想,因為有可能導致重複的ID和bugs(最好是使用類似uuid的軟體包,或者由伺服器生成一個唯一的ID!)

在使用者點選Enter鍵時,會呼叫handleSubmit函式,所以需要呼叫preventDefault來避免正頁面的過載. 之後呼叫dispatch,引數是action.在app中,我們想讓action更像Redux形式-擁有type屬性,附帶一些資料. 此外還有清除輸入.

這個階段的程式碼CodeSandBox

移除一項

現在新增從列表中移除專案的能力

挨著專案新增 刪除按鈕,點選時會dispatch一個action,引數是type==="remove",需要刪除專案的索引

接著需要在Reducer中處理action,通過過濾陣列來移除專案

function ShoppingList() {
  const inputRef = useRef();
  const [items, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case 'add':
        // ... same as before ...
      case 'remove':
        // keep every item except the one we want to remove
        return state.filter((_, index) => index != action.index);
      default:
        return state;
    }
  }, []);

  function handleSubmit(e) { /*...*/ }

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input ref={inputRef} />
      </form>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            {item.name}
            <button
              onClick={() => dispatch({ type: 'remove', index })}
            >
              X
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}
複製程式碼

這個階段的程式碼CodeSandBox

練習:清除列表

在額外新增一個內容,清空列表的按鈕,作為練習.

<ul>之上新增一個按鈕, 新增onClick屬性,可以dispatch,type為"clear"的action.之後在reducer中新增分支處理"clear"action.

那麼... Redux就此終結篇章了嗎?

很多人初次看到useReducer就想,React現在內建reducer了,還有Context可以在全域性範圍傳遞資料,所以Redux已死! 我想給出我的一些想法,因為我猜你也很想知道到Redux的命運將會如何?

[^譯註: 我個人觀點, useReducer的引入不僅不會讓Redux很難堪,反而會讓程式設計師藉助useReducer對Redux有更深的認識,Redux的構架學習可能會有很多的回報,此刻如果捨棄React-Native,投入flutter的懷抱, flutter-Redux的就不再是一個負擔了.]

我不認為useReducer會殺死Redux,Context也不會. 我認為這兩個方法只是擴充套件了React state管理的方法範圍而已,所以真正的情況是他們會減少使用Redux的用例.

Redux仍然比Context+useReducer所做的工作多得多- Redux有Redux DevTools用於拍錯,可以定製化的元件,還有全生態系統的助手軟體包.你可以大膽的說,Redux在很多情況下都有點殺雞用牛刀.但是我認為它仍然是非常強有力的.

Redux提供的全域性store可以讓你集中控制app的data.useReducer是特定元件私有的.使用useReducer,useContext構建一個迷你版的Redux也是完全可行的. 如果你想做,它們完全可以滿足需求(Twitter上有很多人已經做了,有截圖).我個人仍然想念DevTools.

總之-Redux活蹦亂跳的.Hooks不會讓Redux過時.

自己嘗試一下

一下是幾個小的應用,可以用useReducerhook來完成

  • 建一所房子,有一盞燈,按按鈕可以調光-關,低亮度,中等亮度,最高亮度
  • 做一個鍵盤鎖,有6個按鈕, 正確的順序會解鎖. 真確的按鍵順序事先記錄在state中, 順序不正確會充值.

相關文章