如何使用React组件上的Hooks管理状态

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Creative Commons 来接受捐赠。

介绍

React 开发中,跟踪应用程序数据如何随时间变化称为 状态管理 。 通过管理应用程序的状态,您将能够制作响应用户输入的动态应用程序。 React中有很多管理状态的方法,包括基于类的状态管理和第三方库如Redux。 在本教程中,您将使用 React 官方文档 鼓励的方法 :Hooks 来管理功能组件的状态。

Hooks 是一组广泛的工具,可在组件的 props 更改时运行自定义函数。 由于这种状态管理方法不需要您使用类,因此开发人员可以使用 Hooks 编写更短、更易读且易于共享和维护的代码。 Hooks 和基于类的状态管理之间的主要区别之一是没有一个对象可以保存所有状态。 相反,您可以将状态分解为多个可以独立更新的部分。

在本教程中,您将学习如何使用 useStateuseReducer Hooks 设置状态。 useState Hook 在设置值而不参考当前状态时很有价值; useReducer 钩子在您需要引用以前的值或当您有不同的操作需要复杂的数据操作时很有用。 要探索这些设置状态的不同方式,您将创建一个带有购物车的产品页面组件,您将通过从选项列表中添加购买来更新该购物车。 在本教程结束时,您将能够轻松地使用 Hooks 管理功能组件中的状态,并且您将为更高级的 Hooks 打下基础,例如 useEffectuseMemo、和 useContext

先决条件

第 1 步 – 在组件中设置初始状态

在此步骤中,您将通过使用 useState 挂钩将初始状态分配给自定义变量来设置组件的初始状态。 要探索 Hook,您将创建一个带有购物车的产品页面,然后根据状态显示初始值。 在该步骤结束时,您将了解使用 Hooks 保存状态值的不同方法以及何时使用状态而不是道具或静态值。

首先为 Product 组件创建一个目录:

mkdir src/components/Product

接下来,在 Product 目录中打开一个名为 Product.js 的文件:

nano src/components/Product/Product.js

首先创建一个没有状态的组件。 该组件将由两部分组成:购物车,其中包含商品数量和总价,以及产品,其中包含用于从购物车中添加或删除商品的按钮。 目前,这些按钮将不起作用。

将以下代码添加到文件中:

钩子教程/src/components/Product/Product.js

import React from 'react';
import './Product.css';

export default function Product() {
  return(
    <div className="wrapper">
      <div>
        Shopping Cart: 0 total items.
      </div>
      <div>Total: 0</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button>Add</button> <button>Remove</button>
    </div>
  )
}

在此代码中,您使用 JSXProduct 组件创建 HTML 元素,并使用冰淇淋表情符号来表示产品。 此外,其中两个 <div> 元素具有类名,因此您可以添加一些基本的 CSS 样式。

保存并关闭文件,然后在 Product 目录下新建一个名为 Product.css 的文件:

nano src/components/Product/Product.css

添加一些样式以增加文本和表情符号的字体大小:

钩子教程/src/components/Product/Product.css

.product span {
    font-size: 100px;
}

.wrapper {
    padding: 20px;
    font-size: 20px;
}

.wrapper button {
    font-size: 20px;
    background: none;
    border: black solid 1px;
}

表情符号需要更大的 font-size,因为它充当产品图像。 此外,您通过将 background 设置为 none 来移除按钮上的默认渐变背景。

保存并关闭文件。 现在,将组件添加到 App 组件中,以在浏览器中渲染 Product 组件。 打开App.js

nano src/components/App/App.js

导入组件并渲染它。 另外,删除 CSS 导入,因为您不会在本教程中使用它:

钩子教程/src/components/App/App.js

import React from 'react';
import Product from '../Product/Product';

function App() {
  return <Product />
}

export default App;

保存并关闭文件。 当你这样做时,浏览器会刷新,你会看到 Product 组件:

现在您有了一个工作组件,您可以用动态值替换硬编码数据。

React 导出了几个 Hooks,您可以直接从主 React 包中导入它们。 按照惯例,React Hooks 以单词 use 开头,例如 useStateuseContextuseReducer。 大多数第三方库都遵循相同的约定。 例如,Redux 有一个 useSelector 和一个 useStore Hook

Hooks 是让你在 React 生命周期 中运行操作的函数。 挂钩由其他操作或组件道具的更改触发,用于创建数据或触发进一步的更改。 例如,useState Hook 生成有状态的数据片段以及用于更改该数据片段并触发重新渲染的函数。 它将创建一段动态代码,并通过在数据更改时触发重新渲染来连接到生命周期。 在实践中,这意味着您可以使用 useState Hook 将动态数据片段存储在变量中。

例如,在这个组件中,您有两条数据会根据用户操作而改变:购物车和总成本。 这些中的每一个都可以使用上述 Hook 以状态存储。

要试试这个,打开 Product.js

nano src/components/Product/Product.js

接下来,通过添加突出显示的代码从 React 导入 useState Hook:

钩子教程/src/components/Product/Product.js

import React, { useState } from 'react';
import './Product.css';

export default function Product() {
  return(
    <div className="wrapper">
      <div>
        Shopping Cart: 0 total items.
      </div>
      <div>Total: 0</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button>Add</button> <button>Remove</button>
    </div>
  )
}

useState 是一个函数,它以初始状态作为参数并返回一个包含两个项目的 array。 第一项是包含状态的变量,您将经常在 JSX 中使用它。 数组中的第二项是一个将更新状态的函数。 由于 React 将数据作为数组返回,因此您可以使用 destructuring 将值分配给您想要的任何变量名称。 这意味着您可以多次调用 useState 而不必担心名称冲突,因为您可以将每个状态和更新函数分配给一个明确命名的变量。

通过使用空数组调用 useState Hook 创建您的第一个 Hook。 添加以下突出显示的代码:

钩子教程/src/components/Product/Product.js

import React, { useState } from 'react';
import './Product.css';

export default function Product() {
  const [cart, setCart] = useState([]);
  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: 0</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button>Add</button> <button>Remove</button>
    </div>
  )
}

在这里,您将第一个值(状态)分配给名为 cart 的变量。 cart 将是一个包含购物车中产品的数组。 通过将空数组作为参数传递给 useState,您可以将初始空状态设置为 cart 的第一个值。

除了 cart 变量之外,您还将更新函数分配给名为 setCart 的变量。 此时,您没有使用 setCart 函数,您可能会看到有关有未使用变量的警告。 暂时忽略此警告; 在下一步中,您将使用 setCart 更新 cart 状态。

保存文件。 当浏览器重新加载时,您将看到没有更改的页面:

Hooks 和基于类的状态管理之间的一个重要区别是,在基于类的状态管理中,只有一个状态对象。 使用 Hooks,状态对象彼此完全独立,因此您可以拥有任意数量的状态对象。 这意味着如果您想要一条新的有状态数据,您需要做的就是使用新的默认值调用 useState 并将结果分配给新变量。

Product.js 内部,通过创建一个新状态来保存 total 来尝试一下。 将默认值设置为 0 并将值和功能分配给 totalsetTotal

钩子教程/src/components/Product/Product.js

import React, { useState } from 'react';
import './Product.css';

export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);
  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {total}</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button>Add</button> <button>Remove</button>
    </div>
  )
}

现在您有了一些有状态的数据,您可以对显示的数据进行标准化,以提供更可预测的体验。 例如,由于本例中的总计是一个价格,它总是有两位小数。 您可以使用 toLocaleString 方法将 total 从数字转换为具有两位小数的字符串。 它还将根据与浏览器区域设置匹配的数字约定将数字转换为字符串。 您将设置选项 minimumFractionDigitsmaximumFractionDigits 以提供一致的小数位数。

创建一个名为 getTotal 的函数。 此函数将使用范围内变量 total 并返回一个本地化字符串,您将使用它来显示总数。 使用 undefined 作为 toLocaleString 的第一个参数来使用系统语言环境而不是指定语言环境:

钩子教程/src/components/Product/Product.js

import React, { useState } from 'react';
import './Product.css';

const currencyOptions = {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
}

export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);

  function getTotal() {
    return total.toLocaleString(undefined, currencyOptions)
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal()}</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button>Add</button> <button>Remove</button>
    </div>
  )
}

您现在已经向显示的总数添加了一些字符串处理。 尽管 getTotal 是一个单独的函数,但它与周围的函数共享相同的范围,这意味着它可以引用组件的变量。

保存文件。 页面将重新加载,您将看到更新后的总数,保留两位小数:

该功能有效,但截至目前,getTotal只能在这段代码中操作。 在这种情况下,您可以将其转换为纯函数,它在给定相同输入时给出相同的输出,并且不依赖于特定环境来操作。 通过将函数转换为纯函数,可以使其更易于重用。 例如,您可以将其提取到一个单独的文件中并在多个组件中使用它。

更新 getTotal 以将 total 作为参数。 然后将函数移到组件之外:

钩子教程/src/components/Product/Product.js

import React, { useState } from 'react';
import './Product.css';

const currencyOptions = {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
}

function getTotal(total) {
  return total.toLocaleString(undefined, currencyOptions)
}

export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);


  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(total)}</div><^>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button>Add</button> <button>Remove</button>
    </div>
  )
}

保存文件。 当您这样做时,页面将重新加载,您将看到与以前一样的组件。

像这样的功能组件可以更轻松地移动功能。 只要没有范围冲突,您就可以将这些转换函数移动到任何您想要的位置。

在此步骤中,您使用 useState 设置有状态数据的默认值。 然后,您保存了有状态数据和一个使用数组解构将状态更新为变量的函数。 在下一步中,您将使用更新函数更改状态值,以使用更新的信息重新呈现页面。

第 2 步 — 使用 useState 设置状态

在此步骤中,您将通过使用静态值设置新状态来更新您的产品页面。 您已经创建了更新状态的函数,因此现在您将创建一个事件来使用预定义的值更新两个有状态变量。 在此步骤结束时,您将拥有一个页面,其中包含用户可以通过单击按钮来更新的状态。

与基于类的组件不同,您不能通过单个函数调用来更新多个状态。 相反,您必须单独调用每个函数。 这意味着关注点的分离程度更高,这有助于保持有状态对象的焦点。

创建一个函数以将商品添加到购物车并使用商品的价格更新总计,然后将该功能添加到 Add 按钮:

钩子教程/src/components/Product/Product.js

import React, { useState } from 'react';

...

export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);

  function add() {
    setCart(['ice cream']);
    setTotal(5);
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(total)}</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button onClick={add}>Add</button><^>
      <button>Remove</button>
    </div>
  )
}

在此代码段中,您使用包含单词“ice cream”的数组调用 setCart,并使用 5 调用 setTotal。 然后将此函数添加到 Add 按钮的 onClick 事件处理程序中。

请注意,该函数必须与设置状态的函数具有相同的范围,因此必须在组件函数中定义它。

保存文件。 当您这样做时,浏览器将重新加载,当您单击 Add 按钮时,购物车将更新为当前数量:

由于您没有引用 this 上下文,因此您可以使用 箭头函数 或函数声明。 它们在这里工作得一样好,每个开发人员或团队都可以决定使用哪种风格。 您甚至可以跳过定义一个额外的函数并将该函数直接传递给 onClick 属性。

要尝试此操作,请创建一个函数以通过将购物车设置为空对象并将总数设置为 0 来删除值。 在 Remove 按钮的 onClick 属性中创建函数:

钩子教程/src/component/Product/Product.js

import React, { useState } from 'react';
...
export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);

  function add() {
    setCart(['ice cream']);
    setTotal(5);
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(total)}</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button onClick={add}>Add</button>
      <button
        onClick={() => {
          setCart([]);
          setTotal(0);
        }}
      >
        Remove
      </button>
    </div>
  )
}

保存文件。 当你这样做时,你将能够添加和删除一个项目:

分配函数的两种策略都有效,但是直接在道具中创建箭头函数有一些轻微的性能影响。 在每次重新渲染中,React 都会创建一个新函数,该函数会触发 prop 更改并导致组件重新渲染。 在 prop 之外定义函数时,可以利用另一个称为 useCallback 的 Hook。 这将 memoize 函数,这意味着它只会在某些值发生变化时创建一个新函数。 如果没有任何变化,程序将使用函数的缓存内存而不是重新计算它。 有些组件可能不需要这种级别的优化,但通常,组件在树中的位置越高,对记忆的需求就越大。

在此步骤中,您使用 useState 钩子创建的函数更新了状态数据。 您创建了包装函数来调用这两个函数来同时更新多条数据的状态。 但是这些函数是有限的,因为它们添加了静态的、预定义的值,而不是使用以前的状态来创建新的状态。 在下一步中,您将使用 useState Hook 和名为 useReducer 的新 Hook 使用当前状态更新状态。

第 3 步 — 使用当前状态设置状态

在上一步中,您使用静态值更新了状态。 之前的状态是什么并不重要——你总是传递相同的值。 但是一个典型的产品页面会包含许多可以添加到购物车的商品,并且您希望能够在保留之前的商品的同时更新购物车。

在此步骤中,您将使用当前状态更新状态。 您将扩展您的产品页面以包含多个产品,并且您将创建根据当前值更新购物车和总计的函数。 要更新这些值,您将同时使用 useState Hook 和一个名为 useReducer 的新 Hook。

由于 React 可以通过异步调用操作来优化代码,因此您需要确保您的函数可以访问最新状态。 解决此问题的最基本方法是将函数而不是值传递给状态设置函数。 换句话说,您应该调用 setState(previous => previous +5),而不是调用 setState(5)

要开始实现这一点,请通过创建 对象products 数组向产品页面添加更多项目,然后从 Add中删除事件处理程序移除 按钮为重构腾出空间:

钩子教程/src/component/Product/Product.js

import React, { useState } from 'react';
import './Product.css';

...

const products = [
  {
    emoji: '🍦',
    name: 'ice cream',
    price: 5
  },
  {
    emoji: '🍩',
    name: 'donuts',
    price: 2.5,
  },
  {
    emoji: '🍉',
    name: 'watermelon',
    price: 4
  }
];

export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);

  function add() {
    setCart(['ice cream']);
    setTotal(5);
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(total)}</div>
        <div>
        {products.map(product => (
          <div key={product.name}>
            <div className="product">
              <span role="img" aria-label={product.name}>{product.emoji}</span>
            </div>
            <button>Add</button>
            <button>Remove</button>
          </div>
        ))}
      <^></div><^
    </div>
  )
}

您现在有了一些 JSX,它使用 .map 方法 来遍历数组并显示产品。

保存文件。 当您这样做时,页面将重新加载,您将看到多个产品:

目前,按钮没有动作。 由于您只想在点击时添加特定产品,因此您需要将产品作为参数传递给 add 函数。 在 add 函数中,不是将新项目直接传递给 setCartsetTotal 函数,而是传递一个匿名函数,该函数采用当前状态并返回一个新的更新值:

钩子教程/src/component/Product/Product.js

import React, { useState } from 'react';
import './Product.css';
...
export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);

  function add(product) {
    setCart(current => [...current, product.name]);
    setTotal(current => current + product.price);
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(total)}</div>

      <div>
        {products.map(product => (
          <div key={product.name}>
            <div className="product">
              <span role="img" aria-label={product.name}>{product.emoji}</span>
            </div>
            <button onClick={() => add(product)}>Add</button>
            <button>Remove</button>
          </div>
        ))}
      </div>
    </div>
  )
}

匿名函数使用最新状态(carttotal)作为可用于创建新值的参数。 但请注意,不要直接改变状态。 相反,当向购物车添加新值时,您可以通过 扩展 当前值并将新值添加到末尾来将新产品添加到状态。

保存文件。 当您这样做时,浏览器将重新加载,您将能够添加多个产品:

还有另一个称为 useReducer 的 Hook,专门设计用于根据当前状态更新状态,其方式类似于 .reduce 数组方法useReducer Hook 与 useState 类似,但是当你初始化 Hook 时,你传入一个函数,当你改变状态时 Hook 将与初始数据一起运行。 该函数(称为 reducer)接受两个参数:状态和另一个参数。 另一个参数是您在调用更新函数时将提供的内容。

重构购物车状态以使用 useReducer Hook。 创建一个名为 cartReducer 的函数,它将 stateproduct 作为参数。 将 useState 替换为 useReducer,然后将 cartReducer 函数作为第一个参数传递,并将一个空数组作为第二个参数传递,这将是初始数据:

钩子教程/src/component/Product/Product.js

import React, { useReducer, useState } from 'react';

...

function cartReducer(state, product) {
  return [...state, product]
}

export default function Product() {
  const [cart, setCart] = useReducer(cartReducer, []);
  const [total, setTotal] = useState(0);

  function add(product) {
    setCart(product.name);
    setTotal(current => current + product.price);
  }

  return(
...
  )
}

现在,当您调用 setCart 时,请传入产品名称而不是函数。 当你调用 setCart 时,你会调用 reducer 函数,乘积将是第二个参数。 您可以使用 total 状态进行类似的更改。

创建一个名为 totalReducer 的函数,它采用当前状态并添加新数量。 然后将 useState 替换为 useReducer 并传递新值 setCart 而不是函数:

钩子教程/src/component/Product/Product.js

import React, { useReducer } from 'react';

...

function totalReducer(state, price) {
  return state + price;
}

export default function Product() {
  const [cart, setCart] = useReducer(cartReducer, []);
  const [total, setTotal] = useReducer(totalReducer, 0);

  function add(product) {
    setCart(product.name);
    setTotal(product.price);
  }

  return(
    ...
  )
}

由于您不再使用 useState 挂钩,因此您将其从导入中删除。

保存文件。 当您这样做时,页面将重新加载,您将能够将商品添加到购物车:

现在是时候添加 remove 功能了。 但这会导致一个问题:reducer 函数可以处理添加项目和更新总计,但不清楚它如何处理从状态中删除项目。 reducer 函数中的一个常见模式是传递一个对象作为第二个参数,其中包含操作的名称和操作的数据。 在 reducer 中,您可以根据操作更新总数。 在这种情况下,您将通过 add 操作将商品添加到购物车,并通过 remove 操作将其移除。

totalReducer 开始。 更新函数以将 action 作为第二个参数,然后添加 conditional 以根据 action.type 更新状态:

钩子教程/src/component/Product/Product.js

import React, { useReducer } from 'react';
import './Product.css';

...

function totalReducer(state, action) {
  if(action.type === 'add') {
    return state + action.price;
  }
  return state - action.price
}

export default function Product() {
  const [cart, setCart] = useReducer(cartReducer, []);
  const [total, setTotal] = useReducer(totalReducer, 0);

  function add(product) {
    const { name, price } = product;
    setCart(name);
    setTotal({ price, type: 'add' });
  }

  return(
    ...
  )
}

action 是一个具有两个属性的对象:typeprice。 类型可以是 addremoveprice 是一个数字。 如果类型为 add,则增加总数。 如果是remove,则降低总数。 更新 totalReducer 后,使用 addtype 和使用解构赋值设置的 price 调用 setTotal

接下来,您将更新 cartReducer。 这个有点复杂:您可以使用 if/then 条件,但更常见的是使用 switch 语句。 如果您有一个可以处理许多不同操作的 reducer,则 Switch 语句特别有用,因为它使这些操作在您的代码中更具可读性。

totalReducer 一样,您将传递一个对象作为第二项 typename 属性。 如果动作是 remove,通过拼接出产品的第一个实例来更新状态。

更新 cartReducer 后,创建一个 remove 函数,该函数调用 setCartsetTotal 对象包含 type: 'remove' 和 [ X146X] 或 name。 然后使用 switch 语句根据操作类型更新数据。 一定要返回最终状态:

钩子教程/src/complicated/Product/Product.js

import React, { useReducer } from 'react';
import './Product.css';

...

function cartReducer(state, action) {
  switch(action.type) {
    case 'add':
      return [...state, action.name];
    case 'remove':
      const update = [...state];
      update.splice(update.indexOf(action.name), 1);
      return update;
    default:
      return state;
  }
}

function totalReducer(state, action) {
  if(action.type === 'add') {
    return state + action.price;
  }
  return state - action.price
}

export default function Product() {
  const [cart, setCart] = useReducer(cartReducer, []);
  const [total, setTotal] = useReducer(totalReducer, 0);

  function add(product) {
    const { name, price } = product;
    setCart({ name, type: 'add' });
    setTotal({ price, type: 'add' });
  }

  function remove(product) {
    const { name, price } = product;
    setCart({ name, type: 'remove' });
    setTotal({ price, type: 'remove' });
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(total)}</div>

      <div>
        {products.map(product => (
          <div key={product.name}>
            <div className="product">
              <span role="img" aria-label={product.name}>{product.emoji}</span>
            </div>
            <button onClick={() => add(product)}>Add</button>
            <button onClick={() => remove(product)}>Remove</button>
          </div>
        ))}
      </div>
    </div>
  )
}

在处理代码时,请注意不要直接改变 reducer 函数中的状态。 相反,请在 splicing 之前复制对象。 另请注意,最好在 switch 语句上添加 default 操作以解决不可预见的边缘情况。 在这种情况下,只需返回对象。 default 的其他选项是抛出错误或退回到添加或删除等操作。

进行更改后,保存文件。 当浏览器刷新时,您将能够添加和删除项目:

该产品仍然存在一个微妙的错误。 在 remove 方法中,即使商品不在购物车中,您也可以从价格中减去。 如果您在冰淇淋上单击删除而不将其添加到购物车,则显示的总数将为-5.00

您可以通过在减去项目之前检查项目是否存在来修复此错误,但更有效的方法是通过仅将相关数据保存在一个位置来最小化不同的状态片段。 换句话说,尽量避免重复引用相同的数据,在这种情况下是产品。 相反,将原始数据存储在一个状态变量(整个产品对象)中,然后使用该数据执行计算。

重构组件,以便 add() 函数将整个产品传递给减速器,而 remove() 函数删除整个对象。 getTotal 方法将使用购物车,因此您可以删除 totalReducer 功能。 然后您可以将购物车传递给 getTotal(),您可以对其进行重构以将数组缩减为单个值:

钩子教程/src/component/Product/Product.js

import React, { useReducer } from 'react';
import './Product.css';

const currencyOptions = {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
}

function getTotal(cart) {
  const total = cart.reduce((totalCost, item) => totalCost + item.price, 0);
  return total.toLocaleString(undefined, currencyOptions)
}

...

function cartReducer(state, action) {
  switch(action.type) {
    case 'add':
      return [...state, action.product];
    case 'remove':
      const productIndex = state.findIndex(item => item.name === action.product.name);
      if(productIndex < 0) {
        return state;
      }
      const update = [...state];
      update.splice(productIndex, 1)
      return update
    default:
      return state;
  }
}

export default function Product() {
  const [cart, setCart] = useReducer(cartReducer, []);

  function add(product) {
    setCart({ product, type: 'add' });
  }

  function remove(product) {
    setCart({ product, type: 'remove' });
  } 

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(cart)}</div>

      <div>
        {products.map(product => (
          <div key={product.name}>
            <div className="product">
              <span role="img" aria-label={product.name}>{product.emoji}</span>
            </div>
            <button onClick={() => add(product)}>Add</button>
            <button onClick={() => remove(product)}>Remove</button>
          </div>
        ))}
      </div>
    </div>
  )
}

保存文件。 当您这样做时,浏览器将刷新,您将拥有最终的购物车:

通过使用 useReducer Hook,您可以使主要组件主体组织良好且清晰易读,因为解析和拼接数组的复杂逻辑位于组件之外。 如果您想重用它,您也可以将减速器移到组件之外,或者您可以创建一个自定义 Hook 以跨多个组件使用。 您可以将自定义 Hooks 制作为围绕基本 Hooks 的函数,例如 useStateuseReduceruseEffect

Hooks 让您有机会将有状态的逻辑移入和移出组件,而不是类,您通常绑定到组件。 这种优势也可以扩展到其他组件。 由于 Hook 是函数,因此您可以将它们导入多个组件,而不是使用继承或其他复杂形式的类组合。

在这一步中,您学习了使用当前状态设置状态。 您创建了一个使用 useStateuseReducer Hooks 更新状态的组件,并将组件重构为不同的 Hooks 以防止错误并提高可重用性。

结论

Hooks 是 React 的一个重大变化,它创造了一种无需使用类即可共享逻辑和更新组件的新方法。 现在您可以使用 useStateuseReducer 创建组件,您可以使用工具来制作响应用户和动态信息的复杂项目。 您还拥有可用于探索更复杂 Hook 或创建自定义 Hook 的知识基础。

如果您想查看更多 React 教程,请查看我们的 React 主题页面,或返回 如何在 React.js 系列页面中编码