了解 useReducer React Hook

Avatar of Kingsley Silas
Kingsley Silas

DigitalOcean 为您的旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

useReducer 是 React 16.7.0 中发布的众多 React Hooks 之一。它接受一个带有应用程序初始状态的 reducer 函数,返回当前应用程序状态,然后分派一个函数。

以下是如何使用它的示例;

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

它有什么用?考虑任何需要应用程序第一个加载状态的场景。例如,交互式地图上的起点。或者,它可能是一个允许用户从默认模型中构建定制汽车并选择定制选项的应用程序。以下是一个很棒的计算器应用程序演示,它使用 useReducer 来重置计算器,使其在清除时恢复到默认状态零。

查看 CodePen 上由 Gianpierangelo De Palma (@dpgian) 制作的
基本 React Hook 计算器

CodePen 上。

我们将在本文中探讨更多示例,但首先让我们看一下这个 Hook 本身,以更好地理解它是什么以及它在使用时的确切作用。

无所不能的 reducer

很难谈论 useState 而不提及 JavaScript 的 reduce 方法。我们已经在最上面链接了它,但 莎拉的这篇文章是对 reducer 的精彩概述,有助于为我们接下来要讨论的内容做好准备。

关于 reducer,最重要的是要了解,**它始终只会返回一个值**。reducer 的工作就是减少。这个值可以是数字、字符串、数组或对象,但始终只有一个。reducer 在很多情况下都非常有用,但它们尤其适合于对一组值应用一些逻辑并最终得到另一个单个结果。

因此,如果我们有一个数字数组,reduce 将将其压缩成一个单个数字,该数字将根据数组中值的个数累加。假设我们有这个简单的数组

const numbers = [1, 2, 3]

...并且我们有一个函数,它会在 reducer 进行计算时将其记录到控制台中。这将有助于我们了解 reduce 如何将数组压缩成一个单个数字。

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

现在让我们对其运行一个 reducer。如我们之前所见,reduce 接收一个分派函数,该函数将针对默认状态运行。让我们将我们的 reducer 函数和初始值为零的值插入其中。

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 与状态和操作一起使用

好的,我们已经回顾了很多内容,现在让我们谈论我们真正要讨论的主题:useReducer。了解这些内容非常重要,因为你可能已经注意到,在看到 reduce 如何针对初始值触发函数之后,我们接下来要讨论什么。它与之类似,但会以数组的形式返回两个元素:当前**状态**和一个**分派**函数。

换句话说

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

第三个 init 参数是怎么回事?它是一个可选值,它将延迟创建初始状态。这意味着我们可以使用 init 函数在 reducer 之外计算初始状态/值,而不是提供明确的值。如果初始值可能不同,例如基于上次保存的状态而不是一致的值,那么这非常方便。

为了使其工作,我们需要执行以下几件事

  • 定义初始状态。
  • 提供一个包含更新状态的操作的函数。
  • 触发 useReducer 以分派相对于初始状态计算出的更新状态。

这方面的经典示例是计数器应用程序。实际上,React 文档中使用了这个示例来阐明这个概念。以下是在实践中实现它

查看 CodePen 上由 Kingsley Silas Chijioke (@kinsomicrote) 制作的
React useReducer 1

CodePen 上。

这是一个很好的示例,因为它展示了如何使用初始状态(零值)来计算每次点击增加或减少按钮时的新值。我们甚至可以添加一个“重置”按钮,以将总值清除回初始状态零。

示例:汽车定制器

查看 CodePen 上由 Geoff Graham (@geoffgraham) 制作的
React useReducer – 汽车示例

CodePen 上。

在本示例中,我们假设用户已经选择了一辆汽车要购买。但是,我们希望应用程序允许用户为汽车添加额外的选项。每个选项都有一个价格,该价格将添加到基本总价中。

首先,我们需要创建初始状态,它将包含汽车、一个空的数组用于跟踪功能,以及一个从 26,395 美元开始的额外价格,以及商店中项目的列表,以便用户可以从中选择他们想要的东西。

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,并将该项目从商店中删除。我们确保状态的其他部分保持原样。
当用户从功能列表中删除项目时,我们会执行类似的操作 - 减少额外价格,将该项目返回到商店。
以下是如何构建 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 包含所选项目的详细信息。我们使用 action 类型来确定 reducer 函数将如何处理状态的更新。您可以看到,渲染的视图会根据您的操作发生变化 - 从商店购买项目会将该项目从商店中删除并将其添加到功能列表中。此外,总金额也会更新。毫无疑问,该应用程序可以进行一些改进,但这仅仅是为了学习目的。

那么 useState 呢?我们不能使用它吗?

一位敏锐的读者可能一直在问这个问题。我的意思是,setState 通常是一样的,对吧?返回一个有状态的值和一个函数,用于使用该新值重新渲染组件。

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

我们甚至可以在 React 文档中提供的计数器示例中使用 useState() Hook。但是,在状态必须经历复杂转换的情况下,useReducer 是首选方法。Kent C. Dodds 写了一篇文章解释了这两者之间的区别,并且(虽然他经常使用 setState),但他提供了一个使用 useReducer 而不是 setState 的良好用例

如果你的一个状态元素依赖于另一个状态元素的值,那么几乎始终最好使用 useReducer

例如,想象一下你正在编写一个井字棋游戏。你有一个名为 squares 的状态元素,它只是一个包含所有方块及其值的数组。

我的经验法则是在处理复杂状态时使用 useReducer,尤其是在初始状态基于其他元素的状态时。

等等,我们已经有了 Redux!

那些已经使用过 Redux 的人已经了解了我们这里介绍的所有内容,因为 Redux 的设计目的是 使用 Context API 在组件之间传递存储的状态 - 无需通过其他组件传递 props 来实现。

那么,`useReducer` 是否取代了 Redux?不,我的意思是,你可以用它和 `useContext` 钩子基本上自己实现 Redux,但这并不意味着 Redux 就没有用了;Redux 仍然有许多其他值得考虑的功能和优势。

你在哪里使用过 `useReducer`?你找到过明显比 `setState` 更好的用例吗?也许你可以尝试一下我们在这里介绍的东西来构建一些东西。以下是一些想法。

  • 一个日历,它聚焦于今天的日期,但允许用户选择其他日期。甚至可以添加一个“今天”按钮,让用户返回到今天的日期。
  • 你可以尝试改进汽车示例——让用户可以购买的汽车列表。你可能需要在初始状态中定义这些列表,然后用户可以添加他们想要的功能,并收取一定的费用。这些功能可以是预先定义的,也可以由用户定义。