【譯】瞭解 useReducer

原文:Getting to Know the useReducer React Hook
作者:Kingsley Silas
譯者:博軒

useReducer 是 React 16.8.0 中爲數不多由官方提供的 React Hook 之一。它接受一個 reducer 函數 ,以及一個初始的應用程序狀態,然後返回當前應用程序的狀態,和一個調度函數(dispatch)。

一個簡單的例子:

const [state, dispatch] = useReducer(reducer, initialState);

這樣有什麼好處?一個好主意是讓我們試着想象一下,一個應用初次加載屬性時的所有情況。它可能是可交互式的地圖上的一個起點。或許是允許用戶使用一個默認的模型來自定義選項,構建一個自定義汽車。這裏有一個非常簡潔的計算器,當計算器重置時,使用 useReducer 來使應用程序恢復默認狀態。

https://codepen.io/dpgian/emb...

我們將在這篇文章中深入研究幾個例子,瞭解一下 useReducer Hook 本身,以及應該何時使用。

全能的 reducer

說起 useState ,就不得不提及 JavaScript 的 reduce 方法。最開始,我們很難將它們聯繫起來,但是 Sarah 的一篇關於 reducer 的文章 可以幫助我們更好的理解。

關於 reducer 最重要的一點就是:它每次只返回一個值reducer 的工作就是減少。那個值可以是數字,字符串,對象,數組或者對象,但是它總是一個值。reducer 在很多情況下都很有效,但是他對於處理輸入一組值,返回一個值的情況非常有用。

假設我們有一個數字數組,reduce 將依次累加每一個值。這是數組:

const numbers = [1, 2, 3]

...以及一個函數,每次 reducer 中的計算都會在控制檯打印出來。這有助於我們理解 reducer 將數組提取爲單個數字的過程。

const reducer = function (tally, number) { 
    console.log(`Tally: ${tally}, Next number: ${number}, New Total: ${tally + number}`)
    return tally + number
}

現在,讓我們運行一個 reducer 。正如我們所看到的,reduce 接收一個調度函數,以及一個初始狀態。讓我們傳入一個 reducer 函數,以及一個初始值:0。

const total = numbers.reduce(reducer, 0)

這是控制檯打印的內容:

"Tally: 0, Next number: 1, New Total: 1"
"Tally: 1, Next number: 2, New Total: 3"
"Tally: 3, Next number: 3, New Total: 6"

reduce 是如何將一個初始值累加,得到我們的最終結果的。在這個例子中,最終結果是 6。

我也十分喜歡 Dave Ceddia 的示例 ,他展示瞭如何使用 reduce 來拼寫一個單詞:

var letters = ['r', 'e', 'd', 'u', 'c', 'e'];

// `reduce` takes 2 arguments:
//   - a function to do the reducing (you might say, a "reducer")
//   - an initial value for accumulatedResult
var word = letters.reduce(
    function(accumulatedResult, arrayItem) {
        return accumulatedResult + arrayItem;
    },
''); // <-- notice this empty string argument: it's the initial value

console.log(word) // => "reduce"

使用 useReducer ,states ,actions 一起工作

好的,接下來到了這篇文章的重點: useReducer 。到了這裏的一切都很重要,因爲使用 reduce 調用一個函數來處理初始值的方式,就是我們接下來的目標。它是同一種概念,但是會返回一個數組包含兩個元素,當前的狀態調度函數

const [state, dispatch] = useReducer(reducer, initialArg, init);
第三個參數 init 是什麼?它是一個可選值,可以用來惰性提供初始狀態。這意味着我們可以使用使用一個 init 函數來計算初始狀態/值,而不是顯式的提供值。如果初始值可能會不一樣,這會很方便,最後會用計算的值來代替初始值。

爲了使它工作,我們需要做一些事情:

  • 定義初始狀態
  • 提供一個包含 action 的函數來更新 state
  • 觸發 useReducer ,基於初始值計算並更新 state

計數器就是一個經典的例子。事實上這也是官方文檔使用這個例子的原因:

https://codepen.io/kinsomicro...

這是一個很好的例子,因爲它演示了每次通過單擊增加或減少按鈕觸發操作時如何使用初始狀態(零值)來計算新值。我們甚至可以在其中輸入一個“重置”按鈕,將總數恢復到初始狀態零。

示例:汽車定製器

https://codepen.io/geoffgraha...

在此示例中,我們假設用戶已經選擇了自己要購買的汽車。但是,我們希望用戶可以爲汽車添加額外的選項。每個選項的價格都會影響汽車的總價。

首先,我們需要創建初始狀態,其中包括汽車,可以跟蹤功能的空數組 features,$26,395 的起始價格 price,一個存放未選配件的列表 store ,用戶可以選擇他們想要的東西。

const initialState = {
  additionalPrice: 0,
  car: {
    price: 26395,
    name: "2019 Ford Mustang",
    image: "https://cdn.motor1.com/images/mgl/0AN2V/s1/2019-ford-mustang-bullitt.jpg",
    features: []
  },
  store: [
    { id: 1, name: "V-6 engine", price: 1500 },
    { id: 2, name: "Racing detail package", price: 1500 },
    { id: 3, name: "Premium sound system", price: 500 },
    { id: 4, name: "Rear spoiler", price: 250 }
  ]
};

我們的 reducer 功能將處理兩件事:添加和刪除新項目。

const reducer = (state, action) => {
  switch (action.type) {
    case "REMOVE_ITEM":
      return {
        ...state,
        additionalPrice: state.additionalPrice - action.item.price,
        car: { ...state.car, features: state.car.features.filter((x) => x.id !== action.item.id)},
        store: [...state.store, action.item]
      };
    case "BUY_ITEM":
      return {
        ...state,
        additionalPrice: state.additionalPrice + action.item.price,
        car: { ...state.car, features: [...state.car.features, action.item] },
        store: state.store.filter((x) => x.id !== action.item.id)
      }
    default:
      return state;
  }
}

當用戶選擇他想要的項目時,我們更新汽車的 features,增加 additionalPrice 並從商店中刪除該項目。我們確保 state 其他部分會保持原樣。當用戶從功能列表中刪除項目時,我們會執行類似操作 - 減少額外價格,將項目返回到商店。
以下是App組件的代碼。

const App = () => {
  const inputRef = useRef();
  const [state, dispatch] = useReducer(reducer, initialState);
  
  const removeFeature = (item) => {
    dispatch({ type: 'REMOVE_ITEM', item });
  }
  
  const buyItem = (item) => {
    dispatch({ type: 'BUY_ITEM', item })
  }
  
  return (
    <div>
      <div className="box">
        <figure className="image is-128x128">
          <img src={state.car.image} />
        </figure>
        <h2>{state.car.name}</h2>
        <p>Amount: ${state.car.price}</p>
        <div className="content">
          <h6>Extra items you bought:</h6>
          {state.car.features.length ? 
            (
              <ol type="1">
                {state.car.features.map((item) => (
                  <li key={item.id}>
                    <button
                      onClick={() => removeFeature(item)}
                      className="button">X
                    </button>
                    {item.name}
                  </li>
                ))}
              </ol>
            ) : <p>You can purchase items from the store.</p>
          }
        </div>
      </div>
      <div className="box">
        <div className="content">
          <h4>Store:</h4>
          {state.store.length ? 
            (
            <ol type="1">
              {state.store.map((item) => (
                <li key={item.id}>\
                  <button
                    onClick={() => buyItem(item)}
                    className="button">Buy
                  </button>
                  {item.name}
                </li>
              ))}
            </ol>
            ) : <p>No features</p>
          }
        </div>

        <div className="content">
        <h4>
          Total Amount: ${state.car.price + state.additionalPrice}
        </h4>
      </div>
      </div>
    </div>
  );
}

調度操作會包含所選項的詳細信息。我們使用 action 的類型來確定 reducer 函數如何處理狀態的更新。您可以看到渲染視圖會根據您的操作而做出改變 - 從商店購買的商品會從商店中刪除,並添加到功能列表當中。此外,總金額也會更新。毫無疑問,我們可以對示例進行修改達到學習的目的。

我們可以使用 useState 來代替嗎?

聰明的讀者可能一直在想這個問題。我的意思是,setState 大致會做相同的事情,不是嗎?返回一個具備狀態的值,以及一個可以使用新值重新渲染組件的函數。

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

我們甚至可以使用 useState 來實現官方文檔中的計數器的例子。但是 useReducer 在處理複雜狀態的時候是最優解。Kent C. Dodds 寫了一個兩者之間的差異(雖然他經常使用 setState)他提供了一個 useReducer 的最佳實踐:

當你一個元素中的狀態,依賴另一個元素中的狀態,最好使用 useReducer

例如,你正在完成一個井字遊戲。你的組件中的 狀態被稱爲 squares ,它包含了左右方格,以及其中的值。

我的經驗是使用 useReducer 來處理複雜的狀態,尤其是初始狀態是基於其他元素生成的情況下。

useReducer Example

等等,我們已經有 Redux 了!

如果你使用 Redux 工作,也會理解這裏所涉及的所有內容,因爲它的設計理念是通過 Context API 來存儲,傳遞組件之間的狀態 - 不必通過其他組件傳遞 props 來實現。

那麼, useReducer 取代 Redux 了嗎?不,我的意思是,你基本可以通過 useContext hook 來實現你自己的 Redux,但是,這並不代表 Redux 沒有用了,它仍然有許多其他的功能和優點值得考慮。

你會在哪裏使用 useReducer ?他是否有比 setState 更好的地方?也許你可以嘗試使用我們這裏介紹的想法來構建一些東西,下面是一些想法。

  • 一個日曆,會展示今天的日期,但允許用戶選擇其他日期。還可以添加一個“今天”按鈕,幫助用戶返回到今天的日期。
  • 您可以嘗試改進汽車示例 - 讓用戶擁有一個購物車列表。你可以爲它定義初始狀態,然後用戶可以添加他們想要的額外功能,並收取一定費用。這些功能可以是預定義的,也可以由用戶自定義。
本文已經聯繫原文作者,並授權翻譯,轉載請保留原文鏈接
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章