39、使用 Redux 数据存储的全面指南

使用 Redux 数据存储的全面指南

替代数据存储包的选择

Redux 是与 React 一起使用的众多数据存储包之一,它最为知名,大多数项目也会选择它。但如果你不喜欢 Redux 的工作方式,MobX(https://github.com/mobxjs/mobx)可能是个不错的替代方案。MobX 与 React 配合良好,并且允许直接进行状态更改。不过,它的主要缺点是依赖装饰器,一些开发者觉得使用起来不太顺手,而且装饰器尚未成为 JavaScript 规范的一部分(尽管包括 Angular 在内的很多项目都广泛使用它)。

对于数据管理,如果你是 React 的忠实用户,还可以考虑 Relay(https://facebook.github.io/relay)。不过 Relay 只能与 GraphQL 一起使用,这意味着它并不适用于所有项目,但它有一些有趣的特性,并且与 React 集成得很好。

定义数据类型

在处理不同类型的数据时,代码很容易出现重复,导致数据存储的代码难以编写、理解,并且容易因复制和错误调整代码而引入错误。为了解决这个问题,我们可以定义常量值来统一标识不同类型的数据。

src/store 文件夹中创建 dataTypes.js 文件,内容如下:

export const PRODUCTS = "products";
export const SUPPLIERS = "suppliers";
重要的 Redux 术语
名称 描述
action 描述将更改存储中数据的操作。Redux 不允许直接修改数据,需要通过 action 来指定更改。
action type action 是普通的 JavaScript 对象,有一个 type 参数,用于指定 action 的类型,确保 action 能被正确识别和处理。
action creator 创建 action 的函数。action creator 作为函数 props 提供给 React 组件,调用该函数即可对数据存储应用更改。
reducer 接收 action 并处理它在数据存储中所代表的更改的函数。action 指定要对数据存储应用的操作,但实际执行操作的 JavaScript 代码在 reducer 中。
selector 为组件提供对其所需数据存储中数据的访问。selector 作为数据 props 提供给 React 组件。
定义初始数据

目前,我们使用静态定义的数据来定义数据存储的初始内容。在 store 文件夹中创建 initialData.js 文件,内容如下:

import { PRODUCTS, SUPPLIERS } from "./dataTypes";
export const initialData = {
    [PRODUCTS]: [
        { id: 1, name: "Trail Shoes", category: "Running", price: 100 },
        { id: 2, name: "Thermal Hat", category: "Running", price: 12 },
        { id: 3, name: "Heated Gloves", category: "Running", price: 82.50 }
    ],
    [SUPPLIERS]: [
        { id: 1, name: "Zoom Shoes", city: "London", products: [1] },
        { id: 2, name: "Cosy Gear", city: "New York", products: [2, 3] }
    ]
};

数据存储的初始状态被定义为一个普通的 JavaScript 对象,这是使用 Redux 的一个特点,它的很多特性都依赖于纯 JavaScript。

定义模型数据操作类型

为了描述可以对存储中的数据执行的操作(即 action),我们可以定义常量值来标识它们。在 store 文件夹中创建 modelActionTypes.js 文件,内容如下:

export const STORE = "STORE";
export const UPDATE = "UPDATE";
export const DELETE = "DELETE";

对于示例应用程序,我们需要三个事件: STORE 用于向数据存储添加对象, UPDATE 用于修改现有对象, DELETE 用于删除对象。action 类型的值只要唯一即可,最简单的方法是将每个 action 类型赋值为其名称的字符串。

定义模型 action 创建器

action 是从应用程序发送到数据存储以请求更改的对象,它有一个 action 类型和一个数据负载,action 类型指定操作,负载提供操作所需的数据。

store 文件夹中创建 modelActionCreators.js 文件,内容如下:

import { PRODUCTS, SUPPLIERS } from "./dataTypes";
import { STORE, UPDATE, DELETE } from "./modelActionTypes";
let idCounter = 100;

export const saveProduct = (product) => {
    return createSaveEvent(PRODUCTS, product);
}

export const saveSupplier = (supplier) => {
    return createSaveEvent(SUPPLIERS, supplier);
}

const createSaveEvent = (dataType, payload) => {
    if (!payload.id) {
        return {
            type: STORE,
            dataType: dataType,
            payload: { ...payload, id: idCounter++ }
        }
    } else {
        return {
            type: UPDATE,
            dataType: dataType,
            payload: payload
        }
    }
}

export const deleteProduct = (product) => ({
    type: DELETE,
    dataType: PRODUCTS,
    payload: product.id
})

export const deleteSupplier = (supplier) => ({
    type: DELETE,
    dataType: SUPPLIERS,
    payload: supplier.id
})

这里有四个 action 创建器。 saveProduct saveSupplier 函数接收一个对象参数,并将其传递给 createSaveEvent 函数,该函数会检查 id 属性的值,以确定是需要 STORE 还是 UPDATE action。 deleteProduct deleteSupplier action 创建器则更简单,创建一个 DELETE action,其负载是要删除对象的 id 属性值。

定义 reducer

reducer 是一个 JavaScript 函数,用于将 action 应用到数据存储中。它接收数据存储的当前数据和一个 action 作为参数,根据 action 创建一个新的数据对象,以替换数据存储中的现有数据。

store 文件夹中创建 modelReducer.js 文件,内容如下:

import { STORE, UPDATE, DELETE } from "./modelActionTypes";
import { initialData } from "./initialData";

export default function(storeData, action) {
    switch (action.type) {
        case STORE:
            return {
                ...storeData,
                [action.dataType]:
                    storeData[action.dataType].concat([action.payload])
            }
        case UPDATE:
            return {
                ...storeData,
                [action.dataType]: storeData[action.dataType].map(p =>
                    p.id === action.payload.id ? action.payload : p)
            }
        case DELETE:
            return {
                ...storeData,
                [action.dataType]: storeData[action.dataType]
                    .filter(p => p.id !== action.payload)
            }
        default:
            return storeData || initialData;
    }
}

使用 reducer 时需要遵循两个重要规则:
1. 必须创建一个新对象,而不是返回作为参数接收的对象,因为 Redux 会忽略对传入对象所做的任何更改。
2. 由于 reducer 创建的对象会替换存储中的数据,因此需要复制现有对象的所有属性,而不仅仅是被 action 修改的属性。最简单的方法是使用扩展运算符。

此外,当创建数据存储以获取初始数据时,reducer 也会被调用,这由 switch 语句的默认子句处理。如果函数返回 undefined ,Redux 会报告错误,因此要确保返回有用的结果。

避免 reducer 中的代码重复

大多数数据集都需要一组核心的通用操作,这可能导致数据存储定义中出现代码重复。为了避免这种情况,我们可以在 action 中包含一个属性,指定操作应应用于哪种类型的数据,然后在 reducer 中使用 JavaScript 属性访问器功能来选择适当的数据存储属性。

例如:

case STORE:
    return {
        ...store,
        [action.dataType]: store[action.dataType].concat([action.payload])
    }

当创建新的数据存储对象时,JavaScript 会计算 action.dataType 属性,并使用其值定义对象上的新属性,并访问旧数据存储上的属性。

创建数据存储

Redux 提供了 createStore 函数来创建数据存储并准备使用。在 store 文件夹中创建 index.js 文件,内容如下:

import { createStore } from "redux";
import modelReducer from "./modelReducer";

export default createStore(modelReducer);
export { saveProduct, saveSupplier, deleteProduct, deleteSupplier }
    from "./modelActionCreators";

index.js 文件的默认导出是调用 createStore 的结果,它接受 reducer 函数作为参数。同时,我们还导出了 action 创建器,这样在应用程序的其他地方可以通过单个导入语句访问数据存储的所有功能,使用起来更加方便。

以下是创建数据存储的流程图:

graph TD;
    A[开始] --> B[导入 createStore 和 modelReducer];
    B --> C[调用 createStore 函数创建数据存储];
    C --> D[导出数据存储和 action 创建器];
    D --> E[结束];
在 React 应用程序中使用数据存储

目前,我们创建的 action、reducer 和 selector 还没有集成到应用程序中,应用程序中的组件与数据存储中的数据之间也没有链接。接下来,我们将展示如何使用数据存储来替换当前管理应用程序数据的状态数据和方法。

将数据存储应用到顶级组件

React-Redux 包提供了一个 React 容器组件 Provider ,用于提供对数据存储的访问。将 Provider 应用到组件层次结构的顶部,这样整个应用程序都可以访问数据存储。

src 文件夹的 App.js 文件中,代码如下:

import React, { Component } from "react";
import ProductsAndSuppliers from "./ProductsAndSuppliers";
import { Provider } from "react-redux";
import dataStore from "./store";

export default class App extends Component {
    render() {
        return (
            <Provider store={ dataStore }>
                <ProductsAndSuppliers/>
            </Provider>
        )
    }
}

Provider 组件有一个 store 属性,用于指定数据存储。

连接产品数据

使用 React-Redux 包的功能将 ProductDisplay 组件连接到数据存储,步骤如下:
1. 定义一个函数 mapStateToProps ,接收数据存储并选择将组件和存储连接起来的 props:

const mapStateToProps = (storeData) => ({
    products: storeData.products
})

这个函数通常命名为 mapStateToProps ,它返回一个对象,将连接组件的 prop 名称映射到存储中的数据,这些映射被称为 selector。
2. 创建一个对象 mapDispatchToProps ,将组件所需的函数 props 映射到数据存储的 action 创建器:

const mapDispatchToProps = {
    saveCallback: saveProduct,
    deleteCallback: deleteProduct
}

这是将 action 创建器连接到函数 props 的最简单方法,当组件连接到数据存储时,这些 action 创建器函数会被自动调用。
3. 将数据和函数 props 的映射传递给 connect 函数:

const connectFunction = connect(mapStateToProps, mapDispatchToProps);

connect 函数创建一个高阶组件(HOC),将连接到数据存储的 props 与父组件提供的 props 合并。
4. 将组件传递给 connect 函数返回的函数:

export const ProductDisplay = connectFunction(
    class extends Component {
        // 组件代码
    }
)

以下是连接产品数据的流程图:

graph TD;
    A[开始] --> B[定义 mapStateToProps 函数];
    B --> C[创建 mapDispatchToProps 对象];
    C --> D[调用 connect 函数];
    D --> E[将组件传递给 connect 函数返回的函数];
    E --> F[结束];
连接供应商数据

连接供应商数据的过程与连接产品数据相同。在 src 文件夹的 SupplierDisplay.js 文件中,代码如下:

import React, { Component } from "react";
import { SupplierEditor } from "./SupplierEditor";
import { SupplierTable } from "./SupplierTable";
import { connect } from "react-redux";
import { saveSupplier, deleteSupplier} from "./store";

const mapStateToProps = (storeData) => ({
    suppliers: storeData.suppliers
})

const mapDispatchToProps = {
    saveCallback: saveSupplier,
    deleteCallback: deleteSupplier
}

const connectFunction = connect(mapStateToProps, mapDispatchToProps);

export const SupplierDisplay = connectFunction(
    class extends Component {
        // 组件代码
    }
)
更新 App 组件

由于数据存储已经就位, ProductsAndSuppliers 组件变得多余,我们可以直接显示 Selector ProductDisplay SupplierDisplay 组件。

src 文件夹的 App.js 文件中更新代码如下:

import React, { Component } from "react";
import { Provider } from "react-redux";
import dataStore from "./store";
import { Selector } from "./Selector";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";

export default class App extends Component {
    render() {
        return (
            <Provider store={ dataStore }>
                <Selector>
                    <ProductDisplay name="Products" />
                    <SupplierDisplay name="Suppliers" />
                </Selector>
            </Provider>
        )
    }
}

通过以上步骤,我们就可以在 React 应用程序中成功使用 Redux 数据存储,实现数据的统一管理和组件之间的数据共享。

使用 Redux 数据存储的全面指南

总结与优势回顾

通过前面的步骤,我们已经完成了在 React 应用程序中使用 Redux 数据存储的整个流程。下面我们来总结一下使用 Redux 数据存储的优势:
- 数据统一管理 :将应用程序的所有数据集中存储在一个数据存储中,方便管理和维护。
- 可预测性 :通过 reducer 函数,确保数据的变化是可预测的,便于调试和测试。
- 组件解耦 :组件通过 selector 和 action 创建器与数据存储进行交互,减少了组件之间的耦合度。

常见问题及解决方案

在使用 Redux 数据存储的过程中,可能会遇到一些常见问题,下面我们来介绍一些解决方案:
| 问题 | 解决方案 |
| ---- | ---- |
| reducer 中修改了原始对象 | 确保 reducer 函数总是返回一个新对象,而不是修改原始对象。可以使用扩展运算符( ... )来复制对象的属性。 |
| 组件未更新 | 检查 selector 是否正确返回了所需的数据,以及 action 创建器是否正确触发了 reducer。 |
| 性能问题 | 可以使用 shouldComponentUpdate 生命周期方法或 React.memo 来避免不必要的渲染。 |

最佳实践建议

为了更好地使用 Redux 数据存储,我们提供以下最佳实践建议:
1. 遵循单一数据源原则 :将应用程序的所有数据集中存储在一个数据存储中,避免数据分散。
2. 使用常量来定义 action 类型 :使用常量可以提高代码的可读性和可维护性。
3. 保持 reducer 函数的纯净性 :reducer 函数应该是一个纯函数,不应该有副作用。
4. 合理使用 selector :selector 可以帮助我们从数据存储中选择所需的数据,避免组件直接访问数据存储。

未来扩展方向

随着应用程序的发展,可能需要对 Redux 数据存储进行扩展。以下是一些未来可能的扩展方向:
- 异步操作 :处理异步操作,如网络请求,可以使用中间件(如 redux-thunk redux-saga )。
- 数据持久化 :将数据存储在本地存储或数据库中,以实现数据的持久化。
- 性能优化 :使用 reselect 库来优化 selector 的性能,避免不必要的计算。

总结

本文详细介绍了在 React 应用程序中使用 Redux 数据存储的方法,包括替代数据存储包的选择、数据类型的定义、初始数据的设置、action 和 reducer 的创建、数据存储的创建以及组件与数据存储的连接等。通过遵循这些步骤和最佳实践,可以实现数据的统一管理和组件之间的数据共享,提高应用程序的可维护性和可测试性。

以下是整个使用 Redux 数据存储的流程图:

graph LR
    A[选择数据存储包] --> B[定义数据类型]
    B --> C[设置初始数据]
    C --> D[定义 action 类型和创建器]
    D --> E[创建 reducer]
    E --> F[创建数据存储]
    F --> G[应用数据存储到顶级组件]
    G --> H[连接组件到数据存储]
    H --> I[更新 App 组件]
    I --> J[完成使用 Redux 数据存储]

希望本文对你理解和使用 Redux 数据存储有所帮助,如果你有任何问题或建议,欢迎留言讨论。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值