【译】了解 useReducer

772 阅读5分钟
原文: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 来使应用程序恢复默认状态。

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 ,state ,action

好的,接下来到了这篇文章的重点: useReducer 。到了这里的一切都很重要,因为使用 reduce 调用一个函数来处理初始值的方式,就是我们接下来的目标。它是同一种概念,但是会返回一个数组包含两个元素,当前的状态调度函数

const [state, dispatch] = useReducer(reducer, initialArg, init);
第三个参数 init 是什么?它是一个可选值,可以用来惰性提供初始状态。这意味着我们可以使用使用一个 init 函数来计算初始状态/值,而不是显式的提供值。如果初始值可能会不一样,这会很方便,最后会用计算的值来代替初始值。

为了使它工作,我们需要做一些事情:

  • 定义初始状态
  • 提供一个包含 action 的函数来更新 state
  • 触发 useReducer ,基于初始值计算并更新 state

计数器就是一个经典的例子。事实上这也是官方文档使用这个例子的原因:

codepen.io/kinsomicro.…

这是一个很好的例子,因为它演示了每次通过单击增加或减少按钮触发操作时如何使用初始状态(零值)来计算新值。我们甚至可以在其中输入一个“重置”按钮,将总数恢复到初始状态零。

示例:汽车定制器

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 更好的地方?也许你可以尝试使用我们这里介绍的想法来构建一些东西,下面是一些想法。

  • 一个日历,会展示今天的日期,但允许用户选择其他日期。还可以添加一个“今天”按钮,帮助用户返回到今天的日期。
  • 您可以尝试改进汽车示例 - 让用户拥有一个购物车列表。你可以为它定义初始状态,然后用户可以添加他们想要的额外功能,并收取一定费用。这些功能可以是预定义的,也可以由用户自定义。
本文已经联系原文作者,并授权翻译,转载请保留原文链接