如何构建一个由Redux驱动的React应用程序(详细指南)

119 阅读12分钟

我们要解决的问题

在许多情况下,当我们想创建一个小的应用程序时,我们可能会有一些组件声明并使用他们自己的状态。而在少数情况下,一个组件可能希望与它的直接子代共享状态。

我们可以通过在组件内本地声明状态来处理这些情况--如果需要的话,也许可以以道具的形式将状态传递给它的子组件(这也被称为道具钻取)。

但如果你的应用程序规模扩大,你可能需要将状态传递给一个子代,而这个子代可能在层次结构中占了好几级。你也可能需要在兄弟姐妹组件之间使用一个共同的状态。

当然,在兄弟姐妹组件之间共享状态的情况下,我们可以在它们的父代中声明状态,然后通过道具钻取将状态向下传递给它们的子代。但这并不总是可行的,而且有其自身的缺点,我们稍后会看到。

请看下图:

Group-49

这是一个典型的React应用程序中的组件文件结构的示意图。

假设我们需要在子5和子6之间共享一个共同的状态。在这种情况下,我们可以很好地在他们的父级(也就是子级2)中声明一个状态,并将该状态向下传递给两个子级(5和6)。

到现在为止一切都很好。但是如果我们需要在孩子3中拥有同样的状态呢?在这种情况下,我们需要在子代5、6和3的共同父代/祖代--也就是App组件中声明该状态。

同样地,如果我们想在树上相距甚远的子代4、11和10之间共享一个状态呢?我们将再次需要在App组件中创建状态,然后进行多层次的道具钻取,将状态从App传递给这些组件。

随着时间的推移和我们的应用程序规模的扩大,它将开始使我们的App组件或任何其他这样的普通父组件被不必要的状态声明弄得杂乱不堪。这些声明并不直接被这些组件使用,但却被它们的一些下级子组件所使用。

多步骤道具钻取的弊端

多级道具钻取主要有两个缺点。它们是:

  • 不必要的组件臃肿:如上所述,随着我们的应用程序规模的扩大,一些普通的父级组件可能会因为不必要的状态声明而变得臃肿。而这些组件可能不会直接使用这些声明,但它们可能会被一些远方的子组件使用。其他一些组件也可能变得臃肿,它们只是作为子组件的道具传递者。这也会对代码的可读性产生负面影响。
  • 不必要的重新渲染:对于客户端应用程序来说,不必要的重新渲染是一个大问题。不必要的重新渲染会使应用程序变得缓慢、滞后、反应迟钝,给用户带来不好的体验。在React中,重新渲染是由状态或道具变化引起的,还有其他原因。因此,如果一个组件实际上没有使用状态,只是作为道具从父到子的通道,那么当状态/道具发生变化时,它也可能被不必要地重新渲染。请看下面的图片来更好地理解它

Group-52-1

解决这个问题的方法

这就是为什么我们使用Redux或MobX这样的状态管理应用程序,以更统一和有效的方式处理上述的状态管理场景。

在像Redux这样的状态管理解决方案中,我们可以创建一个全局状态,并把它放在一个商店里。无论哪个组件需要该存储的任何状态,都可以通过订阅该存储来轻松获得它。这样,我们就可以摆脱上述两个缺点。

  • 解除组件的杂乱:从"实际 "使用它的组件中按需获取状态,可以在很大程度上通过消除所有不必要的道具钻取来简化我们的许多组件。
  • 不再有不必要的重现:由于我们没有只充当道具传递者的组件,我们也避免了对这些组件进行不必要的重新渲染。只有那些使用全局状态一部分的组件才会在状态改变时重新渲染,这也是我们所期望的行为。

你将在这里学到什么

在本教程中,你将学习如何设置你自己的Redux驱动的React应用程序。我们将创建一个react应用,并设置redux能够全局管理状态,以便任何组件能够访问状态的任何部分(因此被称为redux驱动的react应用)。其他一些可以尝试的redux替代方案有MobX、Zustand等,但在这篇文章中我们将使用redux。

我们将通过如何创建商店并将其连接到应用程序。我们还将看到如何编写动作并在用户交互时分配它们。然后,我们将看到如何制作还原器和更新存储,从作为App的孩子的其他组件读取存储,以及更多。

我还会提供所有重要的代码片段,这样你就可以在阅读和编码的过程中快速启动应用程序。

为了让你在一开始就能看到,这就是我们最后要建立的东西:

finalAppDemo

我们将创建一个基本的应用程序,我们可以在购物车中添加和删除物品。我们将管理Redux商店中的状态变化,并在用户界面中显示信息。

在我们开始之前

在继续学习本教程之前,你应该熟悉Redux商店、动作和还原器。

如果你不熟悉,你可以看看我写的关于Redux的最后一篇文章(如果你还没有)。什么是Redux?存储器、动作和还原器的初学者解释

这将有助于你理解当前的文章。在之前的这个教程中,我试图解释Redux的基本原则/概念。我涵盖了什么是存储,什么是行动,以及还原器如何工作。我还结合一个例子讨论了什么是Redux的可预测性。

despicable-me-minions

初始代码设置

让我们为我们的项目设置一切所需。只要遵循这些步骤,你就能马上开始运行。

1.用create-react-app命令创建一个React应用程序

npx create-react-app react-app-with-redux

2.进入新创建的文件夹

只要键入这个命令就可以导航到新的文件夹:

cd react-app-with-redux

3.安装Redux和react-redux库

你可以像这样安装Redux和react-redux:

npm install redux react-redux

4.运行应用程序

你可以用以下命令运行你的新应用程序:

npm start

如何构建主应用程序

5.如何创建减速器

要创建一个还原器,首先在src 内创建一个文件夹,命名为actionTypes 。然后在里面创建一个名为actionTypes.js 的文件。这个文件将包含应用程序要处理的所有动作

actionTypes.js 中添加以下几行:

export const ADD_ITEM = "ADD_ITEM";
export const DELETE_ITEM = "DELETE_ITEM";

由于我们的应用程序将具有添加和删除项目的功能,我们需要上述两种动作类型。

接下来在src 中创建一个文件夹,名为reducers ,并在其中创建一个新文件,名为cartReducer.js 。这个文件将包含所有与购物车组件相关的还原器逻辑。

注意:我们将在第8步创建视图/用户界面,所以请稍等。

cartReducer.js 中添加以下几行:

import { ADD_ITEM, DELETE_ITEM } from "../actionTypes/actionTypes";

const initialState = {
  numOfItems: 0,
};

export default const cartReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_ITEM:
      return {
        ...state,
        numOfItems: state.numOfItems + 1,
      };

    case DELETE_ITEM:
      return {
        ...state,
        numOfItems: state.numOfItems - 1,
      };
    default:
      return state;
  }
};

正如我们在之前的教程中讨论的那样,我们为应用程序创建了一个初始状态,并将其分配给cartReducer 函数中的默认参数state

这个函数会切换到派发的动作类型。然后根据与动作类型相匹配的情况,对状态进行必要的修改,并返回一个更新后的新的状态实例。

如果没有一个动作类型匹配,那么状态将被原样返回。

最后,我们默认导出 cakeReducer 函数,以便在商店创建过程中使用它。

6.如何创建商店并将其提供给应用程序

src 内创建一个文件,名称为store.js ,并使用此命令创建商店:

const store = createStore()

store.js 中添加以下几行:

import { createStore } from "redux";
import { cartReducer } from "./reducers/cartReducer";

const store = createStore(cartReducer);

export default store;

现在是时候把这个store 提供给App 组件了。为此,我们将使用从react-redux 库中获得的<Provider> 标签。

我们使用下面的语法将整个App 组件包裹在<Provider> 标签中:

// rest of the code ...

<Provider store={store}>
        <div>App Component</div>
        // child components of App/ other logic
</Provider>

// rest of the code ...

继续阅读App.js ,在文件中添加以下几行:

import "./App.css";
import { Provider } from "react-redux";
import store from "./store";

function App() {
  return (
    <Provider store={store}>
      <div>App Component</div>
    </Provider>
  );
}

export default App;

7.创建动作

现在在src 中创建一个文件夹,称为actions ,并在其中创建一个文件,称为cartAction.js 。在这里,我们将添加所有的动作,以便在一些用户交互中被分派

cartAction.js 中添加以下几行:

import { ADD_ITEM, DELETE_ITEM } from "../actionTypes/actionTypes";

const addItem = () => {
  return {
    type: ADD_ITEM,
  };
};

const deleteItem = () => {
  return {
    type: DELETE_ITEM,
  };
};

export { addItem, deleteItem };

在上面的代码中,我们创建了两个动作创建器(返回action 对象的纯JS函数),称为addItem()deleteItem() 。这两个动作创建者都返回action 对象和一个特定的type

注意:每个action 对象必须有一个独特的type 值。随之而来的是,与动作对象一起传递的任何其他数据都是可选的,并将取决于用于更新动作对象的逻辑。state

8.如何创建视图/用户界面

现在我们已经创建了所有需要的实体,如商店、行动和Reducers,现在是创建UI元素的时候了。

src 内创建一个component 文件夹,并在其中创建一个Cart.js 文件。在Cart.js 中添加以下几行:

import React from "react";

const Cart = () => {
  return (
    <div className="cart">
      <h2>Number of items in Cart:</h2>
      <button className="green">Add Item to Cart</button>
      <button className="red">Remove Item from Cart</button>
    </div>
  );
};

export default Cart;

App.js 文件中添加这个Cart 组件:

import "./App.css";
import { Provider } from "react-redux";
import store from "./store";
import Cart from "./component/Cart";

function App() {
  return (
    <Provider store={store}>
      <Cart />
    </Provider>
  );
}

export default App;

为了使它看起来更像样一些,我在App.css 中添加了一些基本的样式,如下所示:

button {
  margin: 10px;
  font-size: 16px;
  letter-spacing: 2px;
  font-weight: 400;
  color: #fff;
  padding: 23px 50px;
  text-align: center;
  display: inline-block;
  text-decoration: none;
  border: 0px;
  cursor: pointer;
}
.green {
  background-color: rgb(6, 172, 0);
}
.red {
  background-color: rgb(221, 52, 66);
}
.red:disabled {
  background-color: rgb(193, 191, 191);
  cursor: not-allowed;
}
.cart {
  text-align: center;
}

这就是目前的用户界面的样子:

Screenshot-2022-05-20-at-20.01.01

9.如何使用useSelector 钩子读取和访问商店

useSelector 是一个由react-redux库提供的钩子,帮助我们读取 和它的内容(s)。store

react-redux 中导入钩子,并使用以下语法,用useSelector 钩子读取商店:

import { useSelector } from "react-redux";
// rest of the code
const state = useSelector((state) => state);

// rest of the code

在添加了useSelector 钩子后,你的Cart.js 文件将看起来像这样:

import React from "react";
import { useSelector } from "react-redux";

const Cart = () => {
  const state = useSelector((state) => state);
  console.log("store", state);
  return (
    <div className="cart">
      <h2>Number of items in Cart:</h2>
      <button className="green">Add Item to Cart</button>
      <button className="red">Remove Item from Cart</button>
    </div>
  );
};

export default Cart;

控制台记录状态将给我们提供我们在步骤5中在reducer文件中设置的初始状态:

Screenshot-2022-05-21-at-01.10.28

10.如何用useDispatch 钩子在点击按钮时派发一个动作

react-redux库给了我们另一个钩子,叫做useDispatch 钩子。它帮助我们调度动作或动作创建者,而动作创建者又会返回动作。其语法如下:

const dispatch = useDispatch();

dispatch(actionObject or calling the action creator);

因此,在我们的Cart.js 中添加一个调度器,最终将使文件看起来像这样。

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { addItem, deleteItem } from "../actions/cartAction";

const Cart = () => {
  const state = useSelector((state) => state);
  const dispatch = useDispatch();
  return (
    <div className="cart">
      <h2>Number of items in Cart: {state.numOfItems}</h2>
      <button
        onClick={() => {
          dispatch(addItem());
        }}
      >
        Add Item to Cart
      </button>
      <button
        disabled={state.numOfItems > 0 ? false : true}
        onClick={() => {
          dispatch(deleteItem());
        }}
      >
        Remove Item to Cart
      </button>
    </div>
  );
};

export default Cart;

请注意,当点击 "将商品添加到购物车"按钮时,我们dispatch 我们在第7步创建的动作创建器addItem()

同样地,当点击从购物车中删除物品按钮时,我们用deleteItem() 来分配动作创建器。

state 变量存储应用程序的状态,它基本上是一个有键的对象numOfItems 。因此,state.numOfItems 给我们提供了当前商店中的物品价值数量。

我们在视图中显示这个信息,行号为<h2>Number of items in Cart: {state.numOfItems}</h2>

再深入一点,当用户点击添加物品到购物车的按钮时,它派发了addItem() 动作创建器。这又会返回一个类型为type: ADD_ITEMaction 对象。

正如我在之前的教程中提到的,当一个动作被派发时,所有的还原器都会被激活。

目前在这个例子中,我们只有一个还原器 -cartReducer 。所以它变成了活动的,并听从action 的调度。

如步骤5所示,还原器将状态和动作作为输入,打开action type ,并返回更新状态的新实例

在这个例子中,当带有type: ADD_ITEM 的动作与第一个开关案例相匹配时,它首先使用传播操作符...state ,对整个状态进行复制。然后它进行必要的更新--在添加项目的情况下是numOfItems: state.numOfItems + 1 (也就是把numOfItems 增加1)。

同样地,使用相同的逻辑,在点击从购物车中删除物品按钮时,一个类型为type: DELETE_ITEM 的动作被派发,该动作会使numOfItems 减少1。

这里是工作应用程序的演示:

finalAppDemo-1

请注意,我们能够根据Redux商店中numOfItems 的值来控制从购物车中删除物品按钮的行为。由于负数的项目没有意义,如果state.numOfItems <= 0 ,我们就禁用 "从购物车中删除项目 "按钮。

这样我们就能防止用户减少购物车中的物品数量,如果它已经是0了。

这是一个基本的例子,告诉你我们如何根据应用程序的内部状态来控制各种DOM元素的行为

就这样吧!我们刚刚完成了我们第一个由Redux驱动的React应用程序的设置。现在你可以根据你的要求继续创建各种其他组件,并在它们之间共享一个共同的全局状态。

总结

在这篇文章中,我们学习了如何快速启动一个由Redux驱动的React应用程序。

在这一过程中,我们学习了如何:

  • 创建动作、动作生成器、还原器和商店
  • 使用存储空间向应用程序提供<Provider>
  • 使用useSelector 钩子从组件中读取/访问存储,并在用户界面中显示状态信息
  • 使用useDispatch 钩子,在用户事件(如按钮点击)上调度动作。
  • 根据应用程序的状态,用逻辑来控制DOM元素的行为
  • 我们了解到低效的状态管理和多级道具钻取的缺点是什么