React16-高级教程-九-

65 阅读1小时+

React16 高级教程(九)

原文:Pro React 16

协议:CC BY-NC-SA 4.0

十八、创建完整的应用

React 为向用户呈现 HTML 内容提供了一组优秀的特性,并依赖第三方包来提供开发完整的 web 应用所需的支持功能。可以和 React 一起使用的包数不胜数,在本书的这一部分,我介绍了那些使用最广泛,也最有可能被本书读者需要的包。这些软件包都是开源的,可以免费获得,在某些情况下还有付费支持选项。

在这一章中,我仅使用本书第二部分中描述的特性构建了一个示例应用。在接下来的章节中,我将介绍第三方软件包,演示它们提供的特性,并解释它们解决的问题。表 18-1 提供了本书这一部分涵盖的包的简要概述。

表 18-1

本书这一部分描述的包

|

名字

|

描述

| | --- | --- | | Redux | Redux 提供了一个数据存储,用于管理应用组件之外的数据。我在第十九章和第二十章中使用了这个包。 | | React 还原 | React Redux 通过其 props 将 React 组件连接到 Redux 数据存储,允许直接访问数据,而不依赖于 prop 线程。我在第十九章和第二十章中使用了这个包。 | | React 路由 | React 路由为 React 应用提供 URL 路由,允许根据浏览器的 URL 选择显示给用户的组件。我在第二十一章和第二十二章使用这个包。 | | 阿克斯 | Axios 为异步 HTTP 请求提供了一致的 API。我在第二十三章使用这个包来消费一个 RESTful web 服务,在第二十五章使用一个 GraphQL 服务。 | | 阿波罗助推 | Apollo 是一个使用 GraphQL 服务的客户端,它比传统的 RESTful web 服务更加灵活。我在第二十五章中使用这个包的 Boost 版本来消费一个 GraphQL 服务,它为 React 应用提供了合理的缺省值。 | | 阿波罗 React | React Apollo 将 React 组件连接到 GraphQL 查询和变异,允许通过 props 消费 GraphQL 服务。 |

我选择的每一个包都有可信的替代方案,我在每一章都提出了建议,以防你无法与所涵盖的包相处。如果您对我在本书的这一部分没有涉及的包感兴趣,请发电子邮件到adam@adam-freeman.com给我。虽然我不做任何承诺,但我会尝试在本书的下一个版本中,或者如果有足够的需求,在发布到本书的 GitHub 资源库的更新中,包含经常被请求的包。

创建项目

打开一个新的命令提示符,导航到一个方便的位置,并运行清单 18-1 中所示的命令。

小费

你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。

npx create-react-app productapp

Listing 18-1Creating the Example Project

运行清单 18-2 中所示的命令,导航到productapp文件夹添加引导包。

cd productapp
npm install bootstrap@4.1.2

Listing 18-2Adding the Bootstrap CSS Framework

为了在应用中包含引导 CSS 样式表,将清单 18-3 中所示的语句添加到index.js文件中,该文件可以在productapp/src文件夹中找到。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

import 'bootstrap/dist/css/bootstrap.css';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

Listing 18-3Including Bootstrap in the index.js File in the src Folder

启动开发工具

使用命令提示符,运行productapp文件夹中清单 18-4 所示的命令来启动开发工具。

npm start

Listing 18-4Starting the Development Tools

一旦项目的初始准备工作完成,一个新的浏览器窗口将打开并显示 URL http://localhost:3000,它显示了图 18-1 中的占位符内容。

img/473159_1_En_18_Fig1_HTML.jpg

图 18-1

运行示例应用

创建示例应用

本章中的应用很简单,但代表了仅使用 React 提供的功能构建的典型项目。该应用为用户提供了两种类型的数据(产品和供应商)的创建、读取、更新和删除(CRUD)功能,用户可以在被管理的数据之间切换。图 18-2 显示了一旦创建了以下章节中定义的组件,应用将如何出现。

img/473159_1_En_18_Fig2_HTML.jpg

图 18-2

示例应用

当然,示例应用是人为设计的,在这种情况下,我的目标是展示核心的 React 特性是强大的,但它们本身不足以创建复杂的 web 应用。一旦定义了应用,我就强调它所包含的问题,每一个问题我都使用下面章节中描述的工具和软件包来解决。

创建产品功能

为了开始应用的功能,我在src文件夹中添加了一个名为ProductTableRow.js的文件,并用它来定义清单 18-5 中所示的组件。

import React, { Component } from "react";

export class ProductTableRow extends Component {

    render() {
        let p = this.props.product;
        return <tr>
            <td>{ p.id }</td>
            <td>{ p.name }</td>
            <td>{ p.category}</td>
            <td className="text-right">${ Number(p.price).toFixed(2) }</td>
            <td>
                <button className="btn btn-sm btn-warning m-1"
                    onClick={ () => this.props.editCallback(p) }>
                        Edit
                </button>
                <button className="btn btn-sm btn-danger m-1"
                    onClick={ () => this.props.deleteCallback(p) }>
                        Delete
                    </button>
            </td>
        </tr>
    }
}

Listing 18-5The Contents of the ProductTableRow.js File in the src Folder

该组件在一个表中呈现一个单独的行,包含属性的列idnamecategoryprice,这些属性是从一个名为product的属性对象中获得的。还有一列显示编辑和删除按钮,这些按钮调用名为editCallbackdeleteCallback的函数属性,并将product属性作为参数传递。

创建产品表

我在src文件夹中添加了一个名为ProductTable.js的文件,并用它来定义清单 18-6 中所示的组件。

import React, { Component } from "react";
import { ProductTableRow } from "./ProductTableRow";

export class ProductTable extends Component {

    render() {
        return <table className="table table-sm table-striped table-bordered">
                <thead>
                    <tr>
                        <th colSpan="5"
                                className="bg-primary text-white text-center h4 p-2">
                            Products
                        </th>
                    </tr>
                    <tr>
                        <th>ID</th><th>Name</th><th>Category</th>
                        <th className="text-right">Price</th>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    {
                        this.props.products.map(p =>
                            <ProductTableRow product={ p }
                                key={ p.id }
                                editCallback={ this.props.editCallback }
                                deleteCallback={ this.props.deleteCallback } />)
                    }
                </tbody>
            </table>
    }
}

Listing 18-6The Contents of the ProductTable.js File in the src Folder

该组件呈现一个表,表体由名为products的数组prop中每个对象的ProductTableRow组件填充。该组件将deleteCallbackeditCallback功能属性传递给ProductTableRow实例。

创建产品编辑器

为了允许用户编辑产品或为新产品提供价值,我在src文件夹中添加了一个名为ProductEditor.js的文件,并添加了清单 18-7 中所示的代码。

import React, { Component } from "react";

export class ProductEditor extends Component {

    constructor(props) {
        super(props);
        this.state = {
            formData: {
                id: props.product.id || "",
                name: props.product.name || "",
                category: props.product.category || "",
                price: props.product.price || ""
            }
        }
    }

    handleChange = (ev) => {
        ev.persist();
        this.setState(state => state.formData[ev.target.name] =  ev.target.value);
    }

    handleClick = () => {
        this.props.saveCallback(this.state.formData);
    }

    render() {
        return <div className="m-2">
            <div className="form-group">
                <label>ID</label>
                <input className="form-control" name="id"
                    disabled
                    value={ this.state.formData.id }
                    onChange={ this.handleChange } />
            </div>
            <div className="form-group">
                <label>Name</label>
                <input className="form-control" name="name"
                    value={ this.state.formData.name }
                    onChange={ this.handleChange } />
            </div>
            <div className="form-group">
                <label>Category</label>
                <input className="form-control" name="category"
                    value={ this.state.formData.category }
                    onChange={ this.handleChange } />
            </div>
            <div className="form-group">
                <label>Price</label>
                <input className="form-control" name="price"
                    value={ this.state.formData.price }
                    onChange={ this.handleChange } />
            </div>
            <div className="text-center">
                <button className="btn btn-primary m-1" onClick={ this.handleClick }>
                    Save
                </button>
                <button className="btn btn-secondary"
                        onClick={ this.props.cancelCallback }>
                    Cancel
                </button>
            </div>
        </div>
    }
}

Listing 18-7The Contents of the ProductEditor.js File in the src Folder

ProductEditor组件为用户提供编辑对象属性的字段。这些字段的初始值是从名为product的 prop 接收的,用于填充状态数据。有一个保存按钮,当它被单击时调用一个名为saveCallback的函数属性,传递状态数据值以便保存。还有一个取消按钮,当它被点击时调用一个名为cancelCallback的函数回调。

创建产品展示组件

接下来,我需要一个在产品表和产品编辑器之间切换的组件。我在src文件夹中添加了一个名为ProductDisplay.js的文件,并用它来定义清单 18-8 中所示的组件。

import React, { Component } from "react";
import { ProductTable } from "./ProductTable"
import { ProductEditor } from "./ProductEditor";

export class ProductDisplay extends Component {

    constructor(props) {
        super(props);
        this.state = {
            showEditor: false,
            selectedProduct: null
        }
    }

    startEditing = (product) => {
        this.setState({ showEditor: true, selectedProduct: product })
    }

    createProduct = () => {
        this.setState({ showEditor: true, selectedProduct: {} })
    }

    cancelEditing = () => {
        this.setState({ showEditor: false, selectedProduct: null })
    }

    saveProduct = (product) => {
        this.props.saveCallback(product);
        this.setState({ showEditor: false, selectedProduct: null })
    }

    render() {
        if (this.state.showEditor) {
            return <ProductEditor
                key={ this.state.selectedProduct.id || -1 }
                product={ this.state.selectedProduct }
                saveCallback={ this.saveProduct }
                cancelCallback={ this.cancelEditing } />
        } else {
            return <div className="m-2">
                <ProductTable products={ this.props.products }
                    editCallback={ this.startEditing }
                    deleteCallback={ this.props.deleteCallback } />
                <div className="text-center">
                    <button className="btn btn-primary m-1"
                        onClick={ this.createProduct }>
                        Create Product
                    </button>
                </div>
            </div>
        }
    }
}

Listing 18-8The Contents of the ProductDisplay.js File in the src Folder

该组件定义状态数据,以确定是否应该显示数据表或编辑器,如果是编辑器,则确定用户想要修改哪个产品。该组件将函数属性传递给ProductEditorProductTable组件,并引入自己的功能。

创建供应商功能

应用中处理供应商数据的部分遵循与前面部分中创建的组件相似的模式。我在src文件夹中添加了一个名为SupplierTableRow.js的文件,并用它来定义清单 18-9 中所示的组件。

import React, { Component } from "react";

export class SupplierTableRow extends Component {

    render() {
        let s = this.props.supplier;
        return <tr>
            <td>{ s.id }</td>
            <td>{ s.name }</td>
            <td>{ s.city}</td>
            <td>{ s.products.join(", ") }</td>
            <td>
                <button className="btn btn-sm btn-warning m-1"
                    onClick={ () => this.props.editCallback(s) }>
                        Edit
                </button>
                <button className="btn btn-sm btn-danger m-1"
                    onClick={ () => this.props.deleteCallback(s) }>
                        Delete
                    </button>
            </td>
        </tr>
    }
}

Listing 18-9The Contents of the SupplierTableRow.js File in the src Folder

该组件使用名为supplier的属性对象的idnamecityproducts属性呈现一个表格行。还有调用功能属性的编辑和删除按钮。

创建供应商表

为了向用户呈现一个供应商表,我在src文件夹中添加了一个名为SupplierTable.js的文件,并添加了清单 18-10 中所示的代码。

import React, { Component } from "react";
import { SupplierTableRow } from "./SupplierTableRow";

export class SupplierTable extends Component {

    render() {
        return <table className="table table-sm table-striped table-bordered">
                <thead>
                    <tr>
                        <th>ID</th><th>Name</th><th>City</th>
                        <th>Products</th><th></th>
                    </tr>
                </thead>
                <tbody>
                    {
                        this.props.suppliers.map(s =>
                            <SupplierTableRow supplier={ s }
                                key={ s.id }
                                editCallback={ this.props.editCallback }
                                deleteCallback={ this.props.deleteCallback } />)
                    }
                </tbody>
            </table>
    }
}

Listing 18-10The Contents of the SupplierTable.js File in the src Folder

该组件呈现一个表格,将suppliers属性数组中的每个对象映射到一个SupplierTableRow。回调的属性从父组件接收并传递。

创建供应商编辑器

为了创建供应商编辑器,我在src文件夹中添加了一个名为SupplierEditor.js的文件,并用它来定义清单 18-11 中所示的组件。

import React, { Component } from "react";

export class SupplierEditor extends Component {

    constructor(props) {
        super(props);
        this.state = {
            formData: {
                id: props.supplier.id || "",
                name: props.supplier.name || "",
                city: props.supplier.city || "",
                products: props.supplier.products || [],
            }
        }
    }

    handleChange = (ev) => {
        ev.persist();
        this.setState(state =>
            state.formData[ev.target.name] =
                ev.target.name === "products"
                    ? ev.target.value.split(",") : ev.target.value);
    }

    handleClick = () => {
        this.props.saveCallback(
            {
                ...this.state.formData,
                products: this.state.formData.products.map(val => Number(val))
            });
    }

    render() {
        return <div className="m-2">
            <div className="form-group">
                <label>ID</label>
                <input className="form-control" name="id"
                    disabled
                    value={ this.state.formData.id }
                    onChange={ this.handleChange } />
            </div>
            <div className="form-group">
                <label>Name</label>
                <input className="form-control" name="name"
                    value={ this.state.formData.name }
                    onChange={ this.handleChange } />
            </div>
            <div className="form-group">
                <label>City</label>
                <input className="form-control" name="city"
                    value={ this.state.formData.city }
                    onChange={ this.handleChange } />
            </div>

            <div className="form-group">
                <label>Products</label>
                <input className="form-control" name="products"
                    value={ this.state.formData.products }
                    onChange={ this.handleChange } />
            </div>

            <div className="text-center">
                <button className="btn btn-primary m-1" onClick={ this.handleClick }>
                    Save
                </button>
                <button className="btn btn-secondary"
                        onClick={ this.props.cancelCallback }>
                    Cancel
                </button>
            </div>
        </div>
    }
}

Listing 18-11The Contents of the SupplierEditor.js File in the src Folder

创建供应商显示组件

为了管理应用中处理供应商数据的部分,以便只显示表格或编辑器,我在src文件夹中添加了一个名为SupplierDisplay.js的文件,并用它来定义清单 18-12 中所示的组件。

import React, { Component } from "react";
import { SupplierEditor } from "./SupplierEditor";
import { SupplierTable } from "./SupplierTable";

export class SupplierDisplay extends Component {

    constructor(props) {
        super(props);
        this.state = {
            showEditor: false,
            selected: null
        }
    }

    startEditing = (supplier) => {
        this.setState({ showEditor: true, selected: supplier })
    }

    createSupplier = () => {
        this.setState({ showEditor: true, selected: {} })
    }

    cancelEditing = () => {
        this.setState({ showEditor: false, selected: null })
    }

    saveSupplier= (supplier) => {
        this.props.saveCallback(supplier);
        this.setState({ showEditor: false, selected: null })
    }

    render() {
        if (this.state.showEditor) {
            return <SupplierEditor
                key={ this.state.selected.id || -1 }
                supplier={ this.state.selected }
                saveCallback={ this.saveSupplier }
                cancelCallback={ this.cancelEditing } />
        } else {
            return <div className="m-2">
                    <SupplierTable suppliers={ this.props.suppliers }
                        editCallback={ this.startEditing }
                        deleteCallback={ this.props.deleteCallback }
                    />
                    <div className="text-center">
                        <button className="btn btn-primary m-1"
                            onClick={ this.createSupplier }>
                                Create Supplier
                        </button>
                    </div>
            </div>
        }
    }
}

Listing 18-12The Contents of the SupplierDisplay.js File in the src Folder

SupplierDisplay组件有自己的状态数据,用于确定是否应该显示编辑器或表格组件。

完成申请

为了允许用户在产品或供应商特性之间进行选择,我在src文件夹中添加了一个名为Selector.js的文件,并添加了清单 18-13 中所示的代码。

import React, { Component } from "react";

export class Selector extends Component {

    constructor(props) {
        super(props);
        this.state = {
            selection: React.Children.toArray(props.children)[0].props.name
        }
    }

    setSelection = (ev) => {
        ev.persist();
        this.setState({ selection: ev.target.name});
    }

    render() {
        return <div className="container-fluid">
            <div className="row">
                <div className="col-2">
                    { React.Children.map(this.props.children, c =>
                        <button
                            name={ c.props.name }
                            onClick={ this.setSelection }
                            className={`btn btn-block m-2
                            ${this.state.selection === c.props.name
                                ? "btn-primary active": "btn-secondary"}`}>
                                    { c.props.name }
                        </button>
                    )}

                </div>
                <div className="col">
                    {
                        React.Children.toArray(this.props.children)
                            .filter(c => c.props.name === this.state.selection)
                    }
                </div>
            </div>
        </div>
    }
}

Listing 18-13The Contents of the Selector.js File in the src Folder

Selector组件是一个容器,它为每个子组件呈现一个按钮,并且只显示用户选择的那个按钮。为了提供将由应用显示的数据以及对其进行操作的回调函数的实现,我向src文件夹添加了一个名为ProductsAndSuppliers.js的文件,并使用它来定义清单 18-14 中所示的组件。

import React, { Component } from 'react';
import { Selector } from './Selector';
import { ProductDisplay } from './ProductDisplay';
import { SupplierDisplay } from './SupplierDisplay';

export default class ProductsAndSuppliers extends Component {

    constructor(props) {
        super(props);
        this.state = {
            products: [
                { id: 1, name: "Kayak",
                category: "Watersports", price: 275 },
                { id: 2, name: "Lifejacket",
                    category: "Watersports", price: 48.95 },
                { id: 3, name: "Soccer Ball", category: "Soccer", price: 19.50 }
            ],
            suppliers: [
                { id: 1, name: "Surf Dudes", city: "San Jose", products: [1, 2] },
                { id: 2, name: "Field Supplies", city: "New York", products: [3] },
            ]
        }
        this.idCounter = 100;
    }

    saveData = (collection, item) => {
        if (item.id === "") {
            item.id = this.idCounter++;
            this.setState(state => state[collection]
                = state[collection].concat(item));
        } else {
            this.setState(state => state[collection]
                = state[collection].map(stored =>
                      stored.id === item.id ? item: stored))
        }
    }

    deleteData = (collection, item) => {
        this.setState(state => state[collection]
            = state[collection].filter(stored => stored.id !== item.id));
    }

    render() {
        return <div>
            <Selector>
                <ProductDisplay
                    name="Products"
                    products={ this.state.products }
                    saveCallback={ p => this.saveData("products", p) }
                    deleteCallback={ p => this.deleteData("products", p) } />
                <SupplierDisplay
                    name="Suppliers"
                    suppliers={ this.state.suppliers }
                    saveCallback={ s => this.saveData("suppliers", s) }
                    deleteCallback={ s => this.deleteData("suppliers", s) } />
            </Selector>
        </div>
    }
}

Listing 18-14The Contents of the ProductsAndSuppliers.js File in the src Folder

该组件定义了productsuppliers状态数据属性,并定义了允许为每个数据类别删除或保存对象的方法。该组件呈现一个Selector,并提供类别显示组件作为其子组件。

最后一步是替换App组件的内容,以便向用户显示前面几节中定义的定制组件,如清单 18-15 所示。

import React, { Component } from "react";
import ProductsAndSuppliers from "./ProductsAndSuppliers";

export default class App extends Component {

    render() {
        return <ProductsAndSuppliers/>
    }
}

Listing 18-15Adding Data and Methods to the App.js File in the src Folder

一旦保存了对App组件的更改,浏览器将显示完整的示例应用。为了确保一切正常,请单击“供应商”按钮,单击“创建供应商”按钮,并填写表单。点击保存按钮,您应该会在表格中看到一个新条目,其中包含您输入的详细信息,如图 18-3 所示。

img/473159_1_En_18_Fig3_HTML.jpg

图 18-3

测试示例应用

理解示例应用的局限性

示例应用展示了如何组合 React 组件来创建应用——但也展示了 React 提供的特性的局限性。

示例应用的最大限制是,它使用硬编码到App组件中的静态定义数据。每次启动应用时都会显示相同的数据,当浏览器重新加载或关闭时,更改会丢失。

尽管现代浏览器支持在本地存储有限数量的数据,但在 web 应用之外保存数据的最常见方式是使用 web 服务。React 不包括对使用 web 服务的集成支持,但是有一些好的选择,既包括简单的 web 服务,我在第二十三章中描述了它们,也包括那些呈现更复杂数据的服务,我在第 24 和 25 章中描述了它们。

下一个限制是状态数据一直被提升到应用的顶部。正如我在第二部分中所解释的,状态数据可以用于组件之间的协调,并且状态数据可以提升到需要访问相同数据的组件的共同祖先。

示例应用展示了这种方法的缺点,即重要的数据——在本例中是productssuppliers数组——最终被推到应用的顶层。当组件被卸载并且它们的状态数据丢失时,React 会销毁组件,这意味着示例应用中位于Selector下面的任何组件都不适合存储应用的数据。因此,应用的所有数据都已经在App组件中定义了,还有操作这些数据的方法。我为应用选择的结构加剧了这个问题,但潜在的问题是,组件的状态非常适合跟踪管理呈现给用户的内容所需的数据——例如是否应该显示数据表或编辑器——但不太适合管理与应用目的相关的数据,通常称为域数据模型数据

防止模型数据被推送到顶层组件的最好方法是将它放在一个单独的数据存储中,这样 React 组件就可以处理数据的表示,而不必管理它。我将在第十九章和第二十章中解释数据存储的用途,并向您展示如何创建数据存储。

该应用还受到限制,因为它要求用户通过特定的任务序列来获得特定的功能。在许多应用中,尤其是那些被设计来支持特定公司功能的应用,用户必须执行一小部分任务,并且希望能够尽可能容易地启动它们。示例应用仅呈现其响应于点击特定元素的特性。在第 21 和 22 章中,我添加了对 URL 路由的支持,这使得用户可以直接导航到特定的功能。

摘要

在这一章中,我创建了一个示例应用,我将在本书的这一部分对其进行增强。在下一章中,我将通过引入一个数据存储来开始这个过程,它将允许模型数据从App组件中移除,并直接分发给应用中需要它的部分。

十九、使用 Redux 数据存储

一个数据存储库将应用的数据移动到 React 组件层次结构之外。使用数据存储意味着数据不必提升到顶层组件,也不必通过线程来确保在需要的地方访问数据。结果是一个更自然的应用结构,让 React 组件专注于他们擅长的事情,即为用户呈现内容。

但是数据存储可能很复杂,将它们引入应用可能是一个违反直觉的过程。在这一章中,我将介绍 Redux,它是 React 项目中最受欢迎的数据存储选择,并向您展示如何创建数据存储并集成到应用中。在第二十章中,我将更深入地解释 Redux 的工作原理,并解释它的一些高级特性。表 19-1 将使用 Redux 数据存储放在上下文中。

表 19-1

将 Redux 数据存储放在上下文中

|

问题

|

回答

| | --- | --- | | 这是什么? | 数据存储将应用的数据移动到组件层次结构之外,这意味着数据不必提升,然后通过适当的线程提供给后代。 | | 为什么有用? | 数据存储可以简化项目中的组件,生成更易于开发和测试的应用。 | | 如何使用? | 数据被移动到应用的专用部分,需要数据的组件可以直接访问这些数据。在 Redux 的情况下,组件通过 props 连接到数据存储,这利用了 React 的性质,尽管映射过程本身可能很笨拙,需要密切关注。 | | 有什么陷阱或限制吗? | 数据存储可能很复杂,并且经常以违反直觉的方式工作。一些数据存储包,包括 Redux,强制执行一些特定的方法来处理一些开发人员认为有限制性的数据。 | | 还有其他选择吗? | 并非所有应用都需要数据存储。对于较少量的数据,使用组件状态特性可能是可接受的,第十四章中描述的 React 上下文 API 可用于基本的数据管理特性。 |

表 19-2 总结了本章内容。

表 19-2

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 创建数据存储 | 定义初始数据、活动类型、创建者和缩减者 | 3–8, 13–21 | | 向 React 应用添加数据存储 | 使用 React-Redux 包中的Provider组件 | nine | | 使用 React 组件中的数据存储 | 使用connect函数将组件的属性映射到数据存储的数据和动作创建者 | 10, 12 | | 分派多个数据存储操作 | 将数据存储动作创建者映射到组件功能属性时,直接使用dispatch函数。 | Twenty-two |

为本章做准备

在本章中,我继续使用在第十八章中创建的productapp项目。为了准备本章,打开一个新的命令提示符并运行清单productapp文件夹中的 19-1 所示的命令。

小费

你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。

npm install redux@4.0.1
npm install react-redux@6.0.0

Listing 19-1Installing Packages

为了快速参考,表 19-3 描述了清单 19-1 中的命令添加到项目中的包。

表 19-3

添加到项目中的包

|

名字

|

描述

| | --- | --- | | redux | 这个包包含 Redux 数据存储的主要特性。 | | react-redux | 这个包包含使用 Redux 和 React 的集成特性。 |

一旦软件包安装完毕,运行productapp文件夹中清单 19-2 所示的命令来启动 React 开发工具。

npm start

Listing 19-2Starting the Development Tools

一旦应用被编译,开发 HTTP 服务器将启动并显示如图 19-1 所示的内容。

img/473159_1_En_19_Fig1_HTML.jpg

图 19-1

运行示例应用

创建数据存储

与 React 非常相似,Redux 为数据和更改强加了一个特定的流。而且,像 React 一样,理解 Redux 的不同部分是如何组合在一起的,一开始可能会很困难。Redux 有两个特点会引起混淆。

首先,Redux 中的更改不会直接应用于存储中的数据,即使这些数据被表示为常规的 JavaScript 对象。相反,Redux 依赖于接受有效负载并更新存储中数据的函数,类似于 React 组件强制使用setState方法来更新状态数据的方式。

第二个困惑点是术语。Redux 数据存储中有许多不同的部分,它们的名称不能直观地描述它们的用途。作为 Redux 入门的快速参考,表 19-4 描述了您将会遇到的术语,并且在接下来的章节中有更详细的解释,在后面的章节中,我创建了一个数据存储并将其集成到示例应用中。

表 19-4

重要的冗余术语

|

名字

|

描述

| | --- | --- | | 行为 | 动作描述了将改变存储中的数据的操作。Redux 不允许直接修改数据,需要动作来指定更改。 | | 动作类型 | 动作是普通的 JavaScript 对象,有一个指定动作类型的类型参数。这确保了动作可以被正确地识别和处理。 | | 动作创建者 | 动作创建器是创建动作的功能。动作创建者被呈现来将组件作为功能属性进行 React,以便调用动作创建者功能来将改变应用于数据存储。 | | 还原剂 | 缩减器是接收动作并处理它在数据存储中表示的变化的功能。动作指定应该对数据存储应用哪个操作,但是包含 JavaScript 代码的 reducer 实现了这一点。 | | 选择器 | 选择器为组件提供了对数据存储中所需数据的访问。选择器的作用是将组件作为数据属性。 |

选择替代的数据存储包

Redux 只是可与 React 一起使用的数据存储包之一,尽管它是最知名的,也是大多数项目选择的。如果你不喜欢 Redux 的工作方式,那么 MobX ( https://github.com/mobxjs/mobx )可能是一个不错的选择。MobX 与 React 配合得很好,并允许直接的状态改变。主要缺点是它依赖于 decorators,一些开发人员觉得这很笨拙,而且它还不是 JavaScript 规范的一部分(尽管它们被广泛使用,包括 Angular)。

在第二十四章和第二十五章中,我介绍了 GraphQL 并解释了它在为应用检索数据中的用途。如果您是一个忠实的 React 用户,那么您可能会考虑使用 Relay ( https://facebook.github.io/relay )进行数据管理。Relay 只适用于 GraphQL,这意味着它并不适合所有项目,但是它有一些有趣的特性,并且可以很好地与 React 集成。

定义数据类型

示例应用包含应用于两种类型数据的类似功能集。在这种情况下,很容易导致管理数据存储的代码重复,执行本质上相同的操作,但在不同的对象集合上执行,结果是数据存储更难编写、更难理解,并且容易因复制一种类型数据的代码并不正确地对其进行调整而导致错误。

这是一个非常常见的问题,因此我将展示一个数据存储,它整合了尽可能多的常见代码。第一步是定义常量值,让我能够一致地识别整个数据存储中不同类型的数据。我创建了src/store文件夹,并在其中添加了一个名为dataTypes.js的文件,其语句如清单 19-3 所示。

export const PRODUCTS = "products";
export const SUPPLIERS = "suppliers";

Listing 19-3The Contents of the dataTypes.js File in the src/store Folder

定义初始数据

在后面的章节中,我将向您展示如何从 web 服务中获取数据,但是目前我将继续使用静态定义的数据。为了定义数据存储的初始内容,我在store文件夹中创建了一个名为initialData.js的文件,并添加了清单 19-4 中所示的语句。

注意

随着我向示例应用添加更多的特性,我将创建数据存储的不同部分来保持特性的分离。我将把呈现给用户的产品和供应商数据称为模型数据,以将其与用于在组件之间进行协调的内部数据区分开来,我将把内部数据称为状态数据

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] }],
}

Listing 19-4The Contents of the initialData.js File in the src/store Folder

数据存储的初始状态被定义为常规的 JavaScript 对象;使用 Redux 的一个特点是,它的许多特性都依赖于纯 JavaScript。为了清楚地说明数据存储何时被使用,我对PRODUCTSSUPPLIERS数组中的对象使用了不同的细节。

定义模型数据操作类型

下一步是描述可以对存储中的数据执行的操作,这些操作被称为操作。在一个复杂的应用中可能会有很多操作,定义常量值来标识它们会很有帮助。我在store文件夹中添加了一个名为modelActionTypes.js的文件,并添加了清单 19-5 中所示的内容。

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

Listing 19-5The Contents of the modelActionTypes.js File in the src/store Folder

为了提供示例应用的功能,我需要三个事件:STORE向数据存储添加对象,UPDATE修改现有对象,以及DELETE删除对象。

分配给动作类型的值并不重要,只要它是唯一的,最简单的方法是为每个动作类型分配一个名称字符串值。

定义模型动作创建者

动作是从应用发送到数据存储以请求更改的对象。动作具有动作类型和数据有效负载,其中动作类型指定操作,有效负载提供操作所需的数据。动作是普通的 JavaScript 对象,可以定义描述操作所需的任何属性组合。约定是定义一个type属性来指示事件类型,我将用dataTypepayload属性来补充这个属性,以指定动作应该应用到的数据和动作所需的数据。

动作是由动作创建者创建的,?? 是从应用接受数据并返回描述数据存储变化的动作的函数的名字。为了定义动作创建者,我在store文件夹中添加了一个名为modelActionCreators.js的文件,并添加了清单 19-6 中所示的代码。

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
})

Listing 19-6The Contents of the modelActionCreators.js File in the src/store Folder

清单中有四个动作创建者。saveProductsaveSupplier函数接收一个对象参数并将其传递给createSaveEvent,后者检查id属性的值以确定是否需要STOREUPDATE动作。deleteProductdeleteSupplier动作创建器更简单,创建一个DELETE动作,其有效负载是要删除的对象的id属性值。

定义减速器

被称为缩减器的 JavaScript 函数将动作应用于数据存储。换句话说,一个动作描述了所需要的改变的类型,而 reducer 包含了实现它的逻辑。我在store文件夹中添加了一个名为modelReducer.js的文件,并添加了清单 19-7 中所示的代码。

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;
    }
}

Listing 19-7The Contents of the modelReducer.js File in the src/store Folder

reducer 从数据存储中接收当前数据,并接收一个动作作为其参数。它检查该动作并使用它来创建新的数据对象,该对象将替换数据存储中的现有数据。

有两条重要的规则要遵循。首先,reducer 必须创建一个新的对象,并且不返回作为参数接收的对象,因为 Redux 将忽略已经做出的任何更改。其次,因为 reducer 创建的对象替换了存储中的数据,所以复制现有对象的属性很重要,而不仅仅是被操作修改的对象。复制属性的最简单方法是使用 spread 运算符,如下所示:

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

这确保了所有属性都被复制到结果对象中。然后,被更改的数据的属性被替换为由该操作修改的数据。

reducer 的另一个重要方面是,它将在创建数据存储以获取初始数据时被调用。这由switch语句的default子句处理,如下所示:

...
default:
    return storeData || initialData;
...

如果函数返回undefined,Redux 会报告一个错误,确保你返回一个有用的结果是很重要的。在清单中,我返回清单 19-4 中定义的initialData对象。

避免缩减器中的代码重复

大多数数据集需要一组核心的常见操作。这可以在示例应用中看到,其中产品和供应商数据都需要存储、更新和删除操作。当您使用相似的动作类型、动作创建者和缩减器代码定义数据存储时,这可能会导致代码重复。我在本节中采用的方法是在操作中包含一个属性,该属性指定操作应该应用于哪种类型的数据,然后我依靠 JavaScript 属性访问器特性在 reducer 中选择适当的数据存储属性,如下所示:

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

当创建新的数据存储对象时,JavaScript 将评估action.dataType属性,并使用它的值定义对象的新属性,并访问旧数据存储的属性,使用我在清单 19-5 中定义的值,因此值PRODUCTSdataType选择产品数据,值SUPPLIERS选择供应商数据。您不必在自己的项目中使用这种技术,但它有助于保持代码简洁和易于管理。

创建数据存储

Redux 提供了createStore函数,该函数创建数据存储并准备使用。我在store文件夹中添加了一个名为index.js的文件,并添加了清单 19-8 中所示的代码。

小费

您不必使用index.js文件名,但是这样做允许只使用文件夹的名称来导入数据存储,如清单 19-9 所示。

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

export default createStore(modelReducer);

export { saveProduct, saveSupplier, deleteProduct, deleteSupplier }
    from "./modelActionCreators";

Listing 19-8The Contents of the index.js File in the src/store Folder

index.js文件的默认导出是调用createStore的结果,它接受 reducer 函数作为它的参数。我还导出了动作创建器,这样所有数据存储的功能都可以通过应用中其他地方的单个import语句来访问,这使得使用数据存储稍微简单了一些。

在 React 应用中使用数据存储

我在上一节中创建的操作、缩减器和选择器还没有集成到应用中,应用中的组件和数据存储中的数据之间也没有链接。在接下来的小节中,我将向您展示如何使用数据存储来替换当前管理应用数据的状态数据和方法。

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

React-Redux 包包括一个 React 容器组件,它提供对数据存储的访问。这个组件叫做Provider,应用在组件层次结构的顶层,这样数据存储在整个应用中都是可用的。在清单 19-9 中,我从清单 19-8 中创建的index.js文件中导入数据存储,并使用Provider组件将数据存储应用于应用中的组件。

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>
        )
    }
}

Listing 19-9Applying the Data Store in the App.js File in the src Folder

Provider组件有一个用于指定数据存储的store属性,我在import语句中将其命名为dataStore

连接产品数据

下一步是将数据存储连接到需要它所包含的数据的组件和操作它的动作创建器。我将采用最直接的方法,即使用 React-Redux 包提供的特性将ProductDisplay组件连接到数据存储,如清单 19-10 所示。

import React, { Component } from "react";
import { ProductTable } from "./ProductTable"
import { ProductEditor } from "./ProductEditor";

import { connect } from "react-redux";

import { saveProduct, deleteProduct } from "./store"

const mapStateToProps = (storeData) => ({

    products: storeData.products

})

const mapDispatchToProps = {

    saveCallback: saveProduct,
    deleteCallback: deleteProduct

}

const connectFunction = connect(mapStateToProps, mapDispatchToProps);

export const ProductDisplay = connectFunction(

        class extends Component {

        constructor(props) {
            super(props);
            this.state = {
                showEditor: false,
                selectedProduct: null
            }
        }

        startEditing = (product) => {
            this.setState({ showEditor: true, selectedProduct: product })
        }

        createProduct = () => {
            this.setState({ showEditor: true, selectedProduct: {} })
        }

        cancelEditing = () => {
            this.setState({ showEditor: false, selectedProduct: null })
        }

        saveProduct = (product) => {
            this.props.saveCallback(product);
            this.setState({ showEditor: false, selectedProduct: null })
        }

        render() {
            if (this.state.showEditor) {
                return <ProductEditor
                    key={ this.state.selectedProduct.id || -1 }
                    product={ this.state.selectedProduct }
                    saveCallback={ this.saveProduct }
                    cancelCallback={ this.cancelEditing } />
            } else {
                return <div className="m-2">
                    <ProductTable products={ this.props.products }
                        editCallback={ this.startEditing }
                        deleteCallback={ this.props.deleteCallback } />
                    <div className="text-center">
                        <button className="btn btn-primary m-1"
                            onClick={ this.createProduct }>
                            Create Product
                        </button>
                    </div>
                </div>
            }
        }
    })

Listing 19-10Connecting to the Data Store in the ProductDisplay.js File in the src Folder

第一步是定义一个接收数据存储并选择连接组件和存储的属性的函数,如下所示:

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

这个函数通常被命名为mapStateToProps,它返回一个对象,该对象将连接组件的正确名称映射到存储中的数据。这些映射被称为选择器,因为它们选择将被映射到组件属性的数据。在这种情况下,选择器将商店的products数组映射到一个名为products的属性。

下一步是创建对象,该对象将组件所需的功能属性映射到数据存储操作创建者,如下所示:

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

React-Redux 包支持动作创建者与功能属性的不同连接方式,但这是最简单的,就是创建一个对象,将属性名称映射到动作创建者功能。当组件连接到数据存储时,这个对象中定义的 action creator 函数将被连接起来,以便自动调用 reducer。在这种情况下,我将saveProductdeleteProduct动作创建者映射到名为saveCallbackdeleteCallback的功能属性。

一旦定义了数据和函数属性的映射,它们就被传递给由 React-Redux 包提供的connect函数。

...
const connectFunction = connect(mapStateToProps, mapDispatchToProps);
...

connect函数创建一个更高阶的组件(HOC ),它传递连接到数据存储的属性,这些属性与父组件提供的属性合并。

小费

高阶组件在第十四章中描述。

最后一步是将一个组件传递给由connect返回的函数,就像这样:

...
export const ProductDisplay = connectFunction(class extends Component {
...

结果是一个组件,其属性连接到数据存储。当您保存清单 19-10 中的更改时,应用将显示清单 19-4 中定义的数据,如图 19-2 所示。

img/473159_1_En_19_Fig2_HTML.jpg

图 19-2

使用产品数据的数据存储

因为数据存储提供的属性替换了来自父组件的属性,ProductDisplay组件完全在数据存储数据上操作,包括创建、编辑和删除对象。

连接供应商数据

同样的过程可以应用于连接供应商数据,如清单 19-11 所示,其中我使用了connect方法为SupplierDisplay组件提供对数据存储的访问。

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 {

        constructor(props) {
            super(props);
            this.state = {
                showEditor: false,
                selected: null
            }
        }

        startEditing = (supplier) => {
            this.setState({ showEditor: true, selected: supplier })
        }

        createSupplier = () => {
            this.setState({ showEditor: true, selected: {} })
        }

        cancelEditing = () => {
            this.setState({ showEditor: false, selected: null })
        }

        saveSupplier= (supplier) => {
            this.props.saveCallback(supplier);
            this.setState({ showEditor: false, selected: null })
        }

        render() {
            if (this.state.showEditor) {
                return <SupplierEditor
                    key={ this.state.selected.id || -1 }
                    supplier={ this.state.selected }
                    saveCallback={ this.saveSupplier }
                    cancelCallback={ this.cancelEditing } />
            } else {
                return <div className="m-2">
                        <SupplierTable suppliers={ this.props.suppliers }
                            editCallback={ this.startEditing }
                            deleteCallback={ this.props.deleteCallback }
                        />
                        <div className="text-center">
                            <button className="btn btn-primary m-1"
                                onClick={ this.createSupplier }>
                                    Create Supplier
                            </button>
                        </div>
                </div>
            }
        }
    })

Listing 19-11Connecting to the Data Store in the SupplierDisplay.js File in the src Folder

结果是SupplierDisplay组件接收连接它到数据存储的属性,如图 19-3 所示。

img/473159_1_En_19_Fig3_HTML.jpg

图 19-3

使用供应商数据的数据存储

有了数据存储之后,ProductsAndSuppliers组件是多余的,因为它的作用是提供产品和供应商数据以及存储和删除这些数据的方法。在清单 19-12 中,我已经更新了App组件以直接显示SelectorProductDisplaySupplierDisplay组件。

import React, { Component } from "react";

//import ProductsAndSuppliers from "./ProductsAndSuppliers";

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>
        )
    }
}

Listing 19-12Displaying Content Directly in the App.js File in the src Folder

注意,我不必为ProductDisplaySupplierDisplay组件提供属性来让它们访问数据和方法;这些将通过将组件连接到数据存储的connect方法来设置。

扩展数据存储

数据存储不仅用于显示给用户的数据,还可以用于存储用于协调和管理组件的状态数据。扩展数据存储以包含状态数据将允许我将存储中的模型数据直接连接到使用它的组件,这在当前是不可能的,因为ProductDisplaySupplierDisplay维护用于选择呈现给用户的内容的状态数据。

在接下来的小节中,我将状态数据和管理它的代码移到数据存储中,这样我可以进一步简化应用。

将状态数据添加到存储中

我希望将状态数据与模型数据分开,所以我将向存储中添加一些结构。我喜欢在用来填充数据存储的初始数据中表示结构,尽管这完全是为了帮助我理解我正在处理的数据的形状,而不是 Redux 强制要求的。

为了构建商店数据,我将现有数据移动到名为modelData的属性中,并添加了一个新的stateData部分,如清单 19-13 所示。

import { PRODUCTS, SUPPLIERS } from "./dataTypes";

export const initialData = {
    modelData: {
        [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] }],
    },
    stateData: {
        editing: false,
        selectedId: -1,
        selectedType: PRODUCTS
    }
}

Listing 19-13Expanding the Data in the initialData.js File in the src/store Folder

我的目标是将ProductDisplaySupplierDisplay组件中的状态数据和逻辑移动到数据存储中。这些组件跟踪用户对编辑的选择,以及是否应该呈现表格或编辑器组件。为了在商店中提供这些信息,我在stateData部分定义了editingselectedselectedType属性。

定义状态数据的操作类型和创建者

接下来,我需要为存储中的状态数据定义操作。当我建立数据存储时,我在不同的文件中定义了动作类型和创建者,但这不是必需的,两者可以一起定义。为了将状态数据动作与存储中的其他动作分开,我在src/store文件夹中添加了一个名为stateActions.js的文件,并使用它来定义动作类型和创建者,如清单 19-14 所示。

import { PRODUCTS, SUPPLIERS } from "./dataTypes";

export const STATE_START_EDITING = "state_start_editing";
export const STATE_END_EDITING = "state_end_editing";
export const STATE_START_CREATING = "state_start_creating";

export const startEditingProduct = (product) => ({
    type: STATE_START_EDITING,
    dataType: PRODUCTS,
    payload: product
})

export const startEditingSupplier = (supplier) => ({
    type: STATE_START_EDITING,
    dataType: SUPPLIERS,
    payload: supplier
})

export const endEditing = () => ({
    type: STATE_END_EDITING
})

export const startCreatingProduct = () => ({
    type: STATE_START_CREATING, dataType: PRODUCTS
})

export const startCreatingSupplier = () => ({
    type: STATE_START_CREATING, dataType: SUPPLIERS
})

Listing 19-14The Contents of the stateActions.js File in the src/store Folder

动作创建器对应于由ProductDisplaySupplierDisplay组件定义的方法,并允许用户开始编辑对象、取消编辑和开始创建新对象。

定义状态数据缩减器

为了更新数据存储以响应动作,我需要定义一个缩减器。我将定义一个单独的函数来处理状态数据,而不是在现有的 reducer 中添加代码。我在src/store文件夹中添加了一个名为stateReducer.js的文件,并添加了清单 19-15 中所示的代码。

import { STATE_START_EDITING, STATE_END_EDITING, STATE_START_CREATING }
    from "./stateActions";
import { initialData } from "./initialData";

export default function(storeData, action) {
    switch(action.type) {
        case STATE_START_EDITING:
        case STATE_START_CREATING:
            return {
                ...storeData,
                editing: true,
                selectedId: action.type === STATE_START_EDITING
                    ? action.payload.id : -1,
                selectedType: action.dataType
            }
        case STATE_END_EDITING:
            return {
                ...storeData,
                editing: false
            }
        default:
            return storeData || initialData.stateData;
    }
}

Listing 19-15The Contents of the stateReducer.js File in the src/store Folder

状态数据的缩减器跟踪用户正在编辑或创建的内容,这与示例应用中现有组件所采用的方法相呼应,尽管我将使用一组属性来协调应用中两种模型数据的编辑器。

将状态数据特征合并到存储中

Redux 提供了combineReducers函数,它允许在一个数据存储中组合使用多个 reducer,每个 reducer 负责数据存储数据的一个部分。在清单 19-16 中,我使用了combineReducers函数来组合模型和状态数据的归约器。

import { createStore, combineReducers } from "redux";

import modelReducer from "./modelReducer";

import stateReducer from "./stateReducer";

export default createStore(combineReducers(

    {
        modelData: modelReducer,
        stateData: stateReducer
    }));

export { saveProduct, saveSupplier, deleteProduct, deleteSupplier }
    from "./modelActionCreators";

Listing 19-16Configuring the Data Store in the index.js File in the src/store Folder

createReducers函数的参数是一个对象,其属性名对应于数据存储的各个部分以及管理它们的 reducers。在清单中,我让原来的缩减器负责数据存储的modelData部分,让清单 19-15 中定义的缩减器负责stateData部分。组合的缩减器被传递给createStore函数来创建数据存储。

注意

每个缩减器在数据存储的一个单独部分上操作,但是当一个动作被处理时,每个缩减器被传递该动作,直到其中一个缩减器返回一个新的数据存储对象,指示该动作已经被处理。

向存储中的数据添加结构需要对模型数据的 reducer 函数返回的初始状态进行相应的更改,如清单 19-17 所示。

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.modelData;
    }
}

Listing 19-17Changing the Initial State in the modelReducer.js File in the src/store Folder

当使用combineReducers功能时,每个缩减器仅被提供其在存储器中的数据部分,并不知道其余的数据和其他缩减器。这意味着我只需要改变初始数据的来源,而不必担心在应用一个动作时在新的数据结构中导航。

将 React 组件连接到存储的状态数据

现在,状态数据已经放入数据存储中,我可以将它连接到组件。我将定义单独的连接器组件,负责将数据存储特性映射到组件属性,而不是分别配置每个组件。我在src/store文件夹中创建了一个名为EditorConnector.js的文件,代码如清单 19-18 所示。

了解演示者/连接器模式

使用数据存储的一种常见方法是使用两种不同类型的组件。演示者组件负责向用户呈现内容并响应用户输入。它们接收数据和函数属性,但不直接连接到数据存储。连接器组件——令人困惑的是,也被称为容器组件——连接到数据存储,为 presenter 组件提供属性。这是我在本章的这一部分中所采用的一般方法,尽管与 React/Redux 世界中的许多方法一样,实现细节可能会有所不同,并且对于如何最好地实现这种分离还存在争议。

import { connect } from "react-redux";
import { endEditing } from "./stateActions";
import { saveProduct, saveSupplier } from "./modelActionCreators";
import { PRODUCTS, SUPPLIERS  } from "./dataTypes";

export const EditorConnector = (dataType, presentationComponent) => {

    const mapStateToProps = (storeData) => ({
        editing: storeData.stateData.editing
            && storeData.stateData.selectedType === dataType,
        product: (storeData.modelData[PRODUCTS]
            .find(p => p.id === storeData.stateData.selectedId)) || {},
        supplier:(storeData.modelData[SUPPLIERS]
            .find(s => s.id === storeData.stateData.selectedId)) || {}
    })

    const mapDispatchToProps = {
        cancelCallback: endEditing,
        saveCallback: dataType === PRODUCTS ? saveProduct: saveSupplier
    }

    return connect(mapStateToProps, mapDispatchToProps)(presentationComponent);
}

Listing 19-18The Contents of the EditorConnector.js File in the src/store Folder

EditorConnector是一个高阶组件,它为表示组件提供了ProductEditorSupplierEditor组件所需的属性,这意味着这些组件可以使用相同的代码连接到数据存储,而不需要单独使用connect函数。为了支持这两种类型的编辑器,HOC 函数接受一种数据类型,用于选择将被映射到 props 的数据和动作创建者。

小费

注意,由combineReducers函数创建的数据存储的分段对数据选择没有任何影响,这意味着我可以从整个存储中选择数据。

为了给显示表格组件的组件提供相同的服务,我在src/store文件夹中添加了一个名为TableConnector.js的文件,并用它来定义清单 19-19 中所示的 HOC。

import { connect } from "react-redux";
import { startEditingProduct, startEditingSupplier } from "./stateActions";
import { deleteProduct, deleteSupplier } from "./modelActionCreators";
import { PRODUCTS, SUPPLIERS } from "./dataTypes";

export const TableConnector = (dataType, presentationComponent) => {

    const mapStateToProps = (storeData) => ({
        products: storeData.modelData[PRODUCTS],
        suppliers: storeData.modelData[SUPPLIERS]
    })

    const mapDispatchToProps = {
        editCallback: dataType === PRODUCTS
            ? startEditingProduct : startEditingSupplier,
        deleteCallback: dataType === PRODUCTS ? deleteProduct : deleteSupplier
    }

    return connect(mapStateToProps, mapDispatchToProps)(presentationComponent);
}

Listing 19-19The Contents of the TableConnector.js File in the src/store Folder

应用连接器组件

连接器组件就位后,我可以从ProductDisplaySupplierDisplay组件中移除状态数据和方法。清单 19-20 显示了ProductDisplay组件的简化。

import React, { Component } from "react";
import { ProductTable } from "./ProductTable"
import { ProductEditor } from "./ProductEditor";
import { connect } from "react-redux";

//import { saveProduct, deleteProduct } from "./store"

import { EditorConnector } from "./store/EditorConnector";

import { PRODUCTS } from "./store/dataTypes";

import { TableConnector } from "./store/TableConnector";

import { startCreatingProduct } from "./store/stateActions";

const ConnectedEditor = EditorConnector(PRODUCTS, ProductEditor);

const ConnectedTable = TableConnector(PRODUCTS, ProductTable);

const mapStateToProps = (storeData) => ({
    editing: storeData.stateData.editing,
    selected: storeData.modelData.products
        .find(item =>  item.id === storeData.stateData.selectedId) || {}
})

const mapDispatchToProps = {
    createProduct: startCreatingProduct,
}

const connectFunction = connect(mapStateToProps, mapDispatchToProps);

export const ProductDisplay = connectFunction(
    class extends Component {

        // constructor(props) {
        //     super(props);
        //     this.state = {
        //         showEditor: false,
        //         selectedProduct: null
        //     }
        // }

        // startEditing = (product) => {
        //     this.setState({ showEditor: true, selectedProduct: product })
        // }

        // createProduct = () => {
        //     this.setState({ showEditor: true, selectedProduct: {} })
        // }

        // cancelEditing = () => {
        //     this.setState({ showEditor: false, selectedProduct: null })
        // }

        // saveProduct = (product) => {
        //     this.props.saveCallback(product);
        //     this.setState({ showEditor: false, selectedProduct: null })
        // }

        render() {
            if (this.props.editing) {
                return <ConnectedEditor key={ this.props.selected.id || -1 } />
                // return <ProductEditor
                //     key={ this.state.selectedProduct.id || -1 }
                //     product={ this.state.selectedProduct }
                //     saveCallback={ this.saveProduct }
                //     cancelCallback={ this.cancelEditing } />
            } else {
                return <div className="m-2">
                    <ConnectedTable />
                    {/* <ProductTable products={ this.props.products }
                        editCallback={ this.startEditing }
                        deleteCallback={ this.props.deleteCallback } />  */}
                    <div className="text-center">
                        <button className="btn btn-primary m-1"
                            onClick={ this.props.createProduct }>
                            Create Product
                        </button>
                    </div>
                </div>
            }
        }
    })

Listing 19-20Using Connector Components in the ProductDisplay.js File in the src Folder

注释掉的语句的数量显示了专用于向其子级提供数据和函数属性的ProductDisplay组件的数量,所有这些现在都通过数据存储和连接器组件来处理。不再需要本地状态数据,因此可以移除构造函数和除了render c之外的所有方法。但是,该组件仍然需要连接到数据存储,因为它需要知道显示哪个子组件,并且需要为编辑器组件生成键值。

清单 19-21 显示了简化的SupplierDisplay组件,去掉了多余的语句,而不仅仅是注释掉。

import React, { Component } from "react";
import { SupplierEditor } from "./SupplierEditor";
import { SupplierTable } from "./SupplierTable";
import { connect } from "react-redux";
import { startCreatingSupplier } from "./store/stateActions";
import { SUPPLIERS } from "./store/dataTypes";
import { EditorConnector } from "./store/EditorConnector";
import { TableConnector } from "./store/TableConnector";

const ConnectedEditor = EditorConnector(SUPPLIERS, SupplierEditor);
const ConnectedTable = TableConnector(SUPPLIERS, SupplierTable);

const mapStateToProps = (storeData) => ({
    editing: storeData.stateData.editing,
    selected: storeData.modelData.suppliers
        .find(item => item.id === storeData.stateData.selectedId) || {}
})

const mapDispatchToProps = {
    createSupplier: startCreatingSupplier
}

const connectFunction = connect(mapStateToProps, mapDispatchToProps);

export const SupplierDisplay = connectFunction(
    class extends Component {

        render() {
            if (this.props.editing) {
                return <ConnectedEditor key={ this.props.selected.id || -1 } />
            } else {
                return <div className="m-2">
                    <ConnectedTable />
                    <div className="text-center">
                        <button className="btn btn-primary m-1"
                            onClick={ this.props.createSupplier }>
                                Create Supplier
                        </button>
                    </div>
                </div>
            }
        }
    })

Listing 19-21Using Connector Components in the SupplierDisplay.js File in the src Folder

过度简化组件

随着我将数据存储的使用进一步推进到组件层次结构中,产品和供应商数据组件之间的差异已经减少,这些组件正在融合。此时,我可以用处理两种数据类型的单个组件替换ProductDisplaySupplierDisplay组件,并继续从数据存储中驱动整个应用。然而,实际上,到了某一点,融合不再简化应用,而是开始简单地转移复杂性。随着您获得使用数据存储的经验,您将会发现您对数据存储的依赖程度和组件中的重复量感到满意。像 React 和 Redux 开发一样,这既是一种好的实践,也是一种个人偏好,值得尝试,直到找到适合自己的方法。

调度多个操作

示例应用使用数据存储的方式存在问题。如果您创建一个新对象或编辑一个现有对象,点击保存按钮更新数据存储但不改变显示给用户的组件,如图 19-4 所示,并且您必须点击取消按钮来更新改变所选组件的数据值。

img/473159_1_En_19_Fig4_HTML.jpg

图 19-4

使用示例应用进行更改

问题在于,将动作创建者映射到属性的connect函数默认只允许选择一个动作创建者,但是我需要两个动作创建者来解决这个问题:更新模型数据的saveProductsaveSupplier创建者和发出编辑完成信号并向用户显示表格的endEditing创建者。

我不能定义一个新的创建者来执行这两项任务,因为每个操作都由一个缩减器处理,每个缩减器负责存储中数据的一个独立部分,这意味着一个操作可以导致模型数据或状态数据的改变,但不能同时导致两者的改变。

幸运的是,connect函数提供了另一种将属性映射到动作创建者的方法,这种方法提供了更多的灵活性。当 connect 方法的mapDispatchToProps参数为对象时,connect函数将每个动作创建者函数包装在一个dispatch方法中,该方法负责将动作创建者返回的动作发送给 reducer。这意味着这样映射创建者的对象:

...
const mapDispatchToProps = {
    createSupplier: startCreatingSupplier
}
...

被转化成这样一个物体:

...
const mapDispatchToProps = {
    createSupplier: payload => dispatch(startCreatingSupplier(payload))
}
...

调用动作创建器来获取动作,然后将动作传递给dispatch函数,这样它就可以被一个缩减器处理。不要定义一个对象,让connect函数包装每个创建者,你可以定义一个接受dispatch作为参数的函数,并产生明确处理动作创建和分派的属性,如清单 19-22 所示。

import { connect } from "react-redux";
import { endEditing } from "./stateActions";
import { saveProduct, saveSupplier } from "./modelActionCreators";
import { PRODUCTS, SUPPLIERS  } from "./dataTypes";

export const EditorConnector = (dataType, presentationComponent) => {

    const mapStateToProps = (storeData) => ({
        editing: storeData.stateData.editing
            && storeData.stateData.selectedType === dataType,
        product: (storeData.modelData[PRODUCTS]
            .find(p => p.id === storeData.stateData.selectedId)) || {},
        supplier:(storeData.modelData[SUPPLIERS]
            .find(s => s.id === storeData.stateData.selectedId)) || {}
    })

    const mapDispatchToProps = dispatch => ({
        cancelCallback: () => dispatch(endEditing()),
        saveCallback: (data) => {
            dispatch((dataType === PRODUCTS ? saveProduct: saveSupplier)(data));
            dispatch(endEditing());
        }
    });

    return connect(mapStateToProps, mapDispatchToProps)(presentationComponent);
}

Listing 19-22Dispatching Actions in the EditorConnector.js File in the src Folder

需要一个调度动作的函数作为每个映射属性的值,实现可以简单地调用动作创建者,或者在使用saveCallback属性的情况下,创建并调度多个动作。结果是由编辑器组件呈现的保存按钮调用一个功能属性,该属性分派更新模型数据和状态数据的动作,如图 19-5 所示。

img/473159_1_En_19_Fig5_HTML.jpg

图 19-5

调度多个操作

理解对参考文献的需求

您可能已经注意到,我使用id属性值和数据类型的组合来跟踪用户选择的对象,如下所示:

...
stateData: {
    editing: false,
    selectedId: -1,
    selectedType: PRODUCTS
}
...

表组件将一个完整的对象传递给启动编辑过程的动作创建者,您可能想知道为什么我选择只保留对所选对象的 ID 引用,而不存储对象本身,特别是因为这种方法需要一些额外的工作来为编辑器组件获取对象。

...
const mapStateToProps = (storeData) => ({
    editing: storeData.stateData.editing
        && storeData.stateData.selectedType === dataType,
    product: (storeData.modelData[PRODUCTS]
        .find(p => p.id === storeData.stateData.selectedId)) || {},
    supplier:(storeData.modelData[SUPPLIERS]
        .find(s => s.id === storeData.stateData.selectedId)) || {}
})
...

间接性是必需的,因为数据存储代表应用中的权威数据源,它可能会被将数据连接到组件的选择器更改。作为演示,我在TableConnector连接器组件中更改了供应商数据的选择器,如清单 19-23 所示。

import { connect } from "react-redux";
import { startEditingProduct, startEditingSupplier } from "./stateActions";
import { deleteProduct, deleteSupplier } from "./modelActionCreators";
import { PRODUCTS, SUPPLIERS } from "./dataTypes";

export const TableConnector = (dataType, presentationComponent) => {

    const mapStateToProps = (storeData) => ({
        products: storeData.modelData[PRODUCTS],
        suppliers: storeData.modelData[SUPPLIERS].map(supp => ({
            ...supp,
            products: supp.products.map(id =>
                storeData.modelData[PRODUCTS].find(p => p.id === Number(id)) || id)
                    .map(val => val.name || val)
        }))
    })

    const mapDispatchToProps = {
        editCallback: dataType === PRODUCTS
            ? startEditingProduct : startEditingSupplier,
        deleteCallback: dataType === PRODUCTS ? deleteProduct : deleteSupplier
    }

    return connect(mapStateToProps, mapDispatchToProps)(presentationComponent);
}

Listing 19-23Changing a Selector in the TableConnector.js File in the src Folder

新的选择器匹配供应商和产品数据,用包含相应产品的名称而不是值的属性替换每个供应商对象的products p属性,如图 19-6 所示。

img/473159_1_En_19_Fig6_HTML.jpg

图 19-6

改变选择器中的数据

每当需要相同的数据视图时,在选择器中转换数据可以确保一致性,但这并不意味着连接的组件不再使用数据存储中的原始数据。因此,依赖一个组件接收的数据来驱动另一个组件的行为可能会导致问题,正因为如此,我使用 ID 值来跟踪用户选择进行编辑的对象。

摘要

在本章中,我创建了一个 Redux 数据存储,并将其连接到示例应用中的组件。我向您展示了如何定义动作、动作创建者、缩减者和选择器,并且演示了如何将数据存储特性作为属性呈现给组件。在下一章,我将描述 Redux 通过其 API 提供的高级特性。

二十、使用数据存储 API

在第十九章中,我向您展示了如何使用 Redux 和 React-Redux 包来创建数据存储并将其连接到示例应用。在这一章中,我描述了这两个包提供的高级使用的 API,允许直接访问数据存储和管理组件和它需要的数据特性之间的连接。表 20-1 将数据存储 API 放在上下文中。

表 20-1

将数据存储 API 放在上下文中

|

问题

|

回答

| | --- | --- | | 它们是什么? | Redux 和 React-Redux 包都定义了支持高级使用的 API,超出了第十九章描述的基本技术。 | | 它们为什么有用? | 这些 API 对于探索数据存储如何工作以及组件如何与它们连接非常有用。它们还可以用于向数据存储添加功能,以及微调应用对数据存储的使用。 | | 它们是如何使用的? | Redux API 直接用在数据存储对象上或在其创建期间使用。React-Redux API 在将组件连接到数据存储时使用,要么使用connect函数,要么使用其更灵活的connectAdvanced替代函数。 | | 有什么陷阱或限制吗? | 本章中描述的 API 需要仔细考虑,以确保达到预期的效果。很容易创建不能正确响应数据存储更改或更新过于频繁的应用。 | | 还有其他选择吗? | 您不必使用本章中描述的 API,大多数项目只需使用第十九章中描述的基本技术就能有效地利用数据存储。 |

表 20-2 总结了本章内容。

表 20-2

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 访问 Redux 数据存储 API | 使用由createStore方法返回的数据存储对象定义的方法 | 2–4 | | 观察数据存储的变化 | 使用 subscribe 方法 | five | | 调度操作 | 使用分派方法 | six | | 创建自定义连接器 | 将组件的属性映射到数据存储功能 | 7–8 | | 将要素添加到数据存储中 | 创建一个还原增强器 | 9–11 | | 在传递给缩减器之前处理操作 | 创建中间件功能 | 12–16 | | 扩展数据存储 API | 创建一个增强功能 | 17–19 | | 将组件的属性合并到数据存储映射中 | 使用 connect 函数的可选参数 | 20–24 |

为本章做准备

在本章中,我继续使用在第十八章创建并在第十九章修改的productapp项目。本章不需要修改。打开一个新的命令提示符,导航到productapp文件夹,运行清单 20-1 中所示的命令来启动开发工具。

小费

你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。

npm start

Listing 20-1Starting the Development Tools

一旦开发工具启动,一个新的浏览器窗口将打开并显示如图 20-1 所示的内容。

img/473159_1_En_20_Fig1_HTML.jpg

图 20-1

运行示例应用

使用 Redux 数据存储 API

在大多数 React 应用中,对 Redux 数据存储的访问是通过 React-Redux 包进行的,该包将数据存储特性映射到 props。这是使用 Redux 最方便的方式,但是还有一个完整的 API 提供对数据存储特性的直接访问,我将在接下来的小节中描述,从提供对存储中数据的访问的特性开始。

在第十九章中,我使用 Redux createStore函数创建了一个新的数据存储,这样我就可以将它作为属性从 React-Redux 包中传递给Provider组件。createStore函数返回的对象也可以通过表 20-3 中描述的四种方法直接使用。

表 20-3

数据存储方法

|

名字

|

描述

| | --- | --- | | getState() | 该方法从数据存储中返回数据,如“获取数据存储状态”一节中所述。 | | subscribe(listener) | 该方法注册一个函数,每次对数据存储进行更改时都会调用该函数,如“观察数据存储更改”一节中所述。 | | dispatch(action) | 该方法接受一个动作,通常由动作创建者产生,并将其发送到数据存储,以便 reducer 可以处理它,如“调度动作”一节所述。 | | replaceReducer(next) | 此方法取代了数据存储用来处理动作的 reducer。这种方法在大多数项目中没有用,中间件提供了一种更有用的机制来改变数据存储的行为。 |

获取数据存储状态

getState方法返回数据存储中的数据,并允许读取存储的内容。作为演示,我在store文件夹中添加了一个名为StoreAccess.js的文件,并用它来定义清单 20-2 中所示的组件。

import React, { Component } from "react";

export class StoreAccess extends Component {

    render() {
        return <div className="bg-info">
            <pre className="text-white">
                { JSON.stringify(this.props.store.getState(), null, 2) }
            </pre>
        </div>
    }
}

Listing 20-2The Contents of the StoreAccess.js File in the src/store Folder

组件接收数据存储对象作为属性,并调用getState方法,该方法返回存储的数据对象。为了格式化数据,我使用了JSON.stringify方法,该方法将 JavaScript 对象序列化为 JSON,然后格式化结果以便于阅读。在清单 20-3 中,我添加了一个网格布局,这样新的组件就会显示在应用其余功能的旁边。

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";

import { StoreAccess } from "./store/StoreAccess";

export default class App extends Component {

    render() {
        return <div className="container-fluid">
            <div className="row">
                <div className="col-3">
                    <StoreAccess store={ dataStore } />
                </div>
                <div className="col">
                    <Provider store={ dataStore }>
                        <Selector>
                            <ProductDisplay name="Products" />
                            <SupplierDisplay name="Suppliers" />
                        </Selector>
                    </Provider>
                </div>
            </div>
        </div>
    }
}

Listing 20-3Displaying the Data Store Contents in the App.js File in the src Folder

一个商店中可能有很多数据,所以我显示了 JSON 文本,这样它将出现在自己的列中,如图 20-2 所示。如果您不能在屏幕上显示所有的文本,请不要担心,因为我很快会将焦点缩小到数据的子集。

img/473159_1_En_20_Fig2_HTML.jpg

图 20-2

获取数据存储的内容

如果您检查通过getState方法获得的数据,您将会看到所有的内容都包含在内,因此modelDatastateData属性的内容都是可用的。应用于 reducers 的分段不会影响由getState方法返回的数据,该方法提供了对数据存储中所有内容的访问。

缩小对特定数据的关注

为了更容易跟踪数据存储的内容,我将重点关注由getState方法返回的数据的子集,这将允许我更容易地演示其他 Redux 特性。在清单 20-4 中,我修改了StoreAccess组件,使其只显示第一个产品对象和一组状态数据变量。

import React, { Component } from "react";

export class StoreAccess extends Component {

    constructor(props) {
        super(props);
        this.selectors = {
            product: (storeState) => storeState.modelData.products[0],
            state: (storeState) => storeState.stateData
        }
    }

    render() {
        return <div className="bg-info">
            <pre className="text-white">
                { JSON.stringify(this.selectData(), null, 2) }
            </pre>
        </div>
    }

    selectData() {
        let storeState = this.props.store.getState();
        return Object.entries(this.selectors).map(([k, v]) => [k, v(storeState)])
            .reduce((result, [k, v]) => ({ ...result, [k]: v}), {});
    }
}

Listing 20-4Focusing the Data in the StoreAccess.js File in the src/store Folder

我定义了一个selectors对象,它的属性值是从存储中选择数据的函数。selectData方法使用getState方法从数据存储中获取数据,并调用每个选择器函数来生成由组件呈现的数据。(entriesmapreduce方法的使用产生了一个对象,该对象具有与selectors属性相同的属性名称,并具有通过调用每个选择器函数产生的值。)

对组件的更改从存储中选择了更易管理的数据部分,如图 20-3 所示。

img/473159_1_En_20_Fig3_HTML.jpg

图 20-3

选择商店数据的子集

观察数据存储更改

getState方法返回的对象是存储中数据的快照,当存储改变时不会自动更新。通常的 React 变化检测特性对存储不起作用,因为它不是组件状态数据的一部分。因此,对存储中数据的更改不会触发 React 更新。

Redux 提供了subscribe方法来在数据存储发生变化时接收通知,这允许再次调用getState方法来获得数据的新快照。在清单 20-5 中,我在StoreAccess组件中使用了subscribe方法来确保组件显示的数据是最新的。

import React, { Component } from "react";

export class StoreAccess extends Component {

    constructor(props) {
        super(props);
        this.selectors = {
            product: (storeState) => storeState.modelData.products[0],
            state: (storeState) => storeState.stateData
        }
        this.state = this.selectData();
    }

    render() {
        return <div className="bg-info">
            <pre className="text-white">
                { JSON.stringify(this.state, null, 2) }
            </pre>
        </div>
    }

    selectData() {
        let storeState = this.props.store.getState();
        return Object.entries(this.selectors).map(([k, v]) => [k, v(storeState)])
            .reduce((result, [k, v]) => ({ ...result, [k]: v}), {});
    }

    handleDataStoreChange() {
        let newData = this.selectData();
        Object.keys(this.selectors)
            .filter(key => this.state[key] !== newData[key])
            .forEach(key => this.setState({ [key]: newData[key]}));
    }

    componentDidMount() {
        this.unsubscriber =
            this.props.store.subscribe(() => this.handleDataStoreChange());
    }

    componentWillUnmount() {
        this.unsubscriber();
    }
}

Listing 20-5Subscribing to Change Notifications in the StoreAccess.js File in the src/store Folder

我在componentDidMount方法中订阅更新。subscribe方法的结果是一个可以用来取消订阅更新的函数,我在componentWillUnmount方法中调用了这个函数。

subscribe方法的参数是一个当数据存储发生变化时将被调用的函数。没有向该函数提供任何参数,这只是一个信号,表明已经发生了更改,并且可以使用getState方法来获取数据存储的新内容。

Redux 不提供任何关于哪些数据发生了变化的信息,所以我定义了handleStoreChange方法来检查每个选择器函数获得的数据,以查看组件呈现的数据是否发生了变化。我使用组件状态数据特性来跟踪显示的数据,并使用setState方法来触发更新。重要的是,只有当组件显示的数据已经改变时,才执行状态改变;否则,将对数据存储的每次更改执行更新。

要查看更改的效果,请单击 Trail Shoes 产品的“编辑”按钮,对“名称”字段进行更改,然后单击“保存”按钮。当你经历这个过程时,StoreAccess组件显示的数据将反映数据存储中的变化,如图 20-4 所示。

img/473159_1_En_20_Fig4_HTML.jpg

图 20-4

从数据存储接收变更通知

调度操作

可以使用dispatch方法来分派动作,当我需要分派多个动作时,React-Redux 包在第十九章中提供了对同一dispatch的访问。

正如我在第十九章中解释的,行动是通过行动创造者创造的。在清单 20-6 中,我向StoreAccess组件添加了一个button,该组件使用一个动作创建器来获取一个动作对象,然后使用dispatch方法将该对象发送到数据存储。

import React, { Component } from "react";

import { startCreatingProduct } from "./stateActions";

export class StoreAccess extends Component {

    constructor(props) {
        super(props);
        this.selectors = {
            product: (storeState) => storeState.modelData.products[0],
            state: (storeState) => storeState.stateData
        }
        this.state = this.selectData();
    }

    render() {
        return <React.Fragment>
            <div className="text-center">
                <button className="btn btn-primary m-1"
                    onClick={ this.dispatchAction }>
                        Dispatch Action
                </button>
            </div>
            <div className="bg-info">
                <pre className="text-white">
                    { JSON.stringify(this.state, null, 2) }
                </pre>
            </div>
        </React.Fragment>
    }

    dispatchAction = () => {
        this.props.store.dispatch(startCreatingProduct())
    }

    selectData() {
        let storeState = this.props.store.getState();
        return Object.entries(this.selectors).map(([k, v]) => [k, v(storeState)])
            .reduce((result, [k, v]) => ({ ...result, [k]: v}), {});
    }

    handleDataStoreChange() {
        let newData = this.selectData();
        Object.keys(this.selectors)
            .filter(key => this.state[key] !== newData[key])
            .forEach(key => this.setState({ [key]: newData[key]}));
    }

    componentDidMount() {
        this.unsubscriber =
            this.props.store.subscribe(() => this.handleDataStoreChange());
    }

    componentWillUnmount() {
        this.unsubscriber();
    }
}

Listing 20-6Dispatching an Action in the StoreAccess.js File in the src/store Folder

按钮通过调用dispatchAction方法来响应点击事件,该方法调用startCreatingProduct动作创建器并将结果传递给数据存储的dispatch方法。结果是点击按钮切换显示编辑器,如图 20-5 所示。

img/473159_1_En_20_Fig5_HTML.jpg

图 20-5

调度操作

创建连接器组件

从存储中获取当前数据、接收更改通知和分派操作的能力提供了创建基本连接器组件的所有特性,该组件提供了我在示例应用中使用的 React-Redux 包的基本等价物。为了创建通过 Redux API 将组件连接到数据存储的工具,我在store文件夹中添加了一个名为CustomConnector.js的文件,并添加了清单 20-7 中所示的代码。

警告

我不建议在实际项目中使用自定义连接器。React-Redux 包具有额外的特性,并且已经过全面的测试,但是将核心 React 特性与 Redux 数据存储 API 相结合提供了一个如何创建高级特性的有用示例。

import React, { Component } from "react";

export const CustomConnectorContext = React.createContext();

export class CustomConnectorProvider extends Component {

    render() {
        return <CustomConnectorContext.Provider value={ this.props.dataStore }>
            { this.props.children }
        </CustomConnectorContext.Provider>
    }
}

export class CustomConnector extends React.Component {
    static contextType = CustomConnectorContext;

    constructor(props, context) {
        super(props, context);
        this.state = this.selectData();
        this.functionProps = Object.entries(this.props.dispatchers)
            .map(([k, v]) => [k, (...args) => this.context.dispatch(v(...args))])
            .reduce((result, [k, v]) => ({...result, [k]: v}), {})
    }

    render() {
        return  React.Children.map(this.props.children, c =>
            React.cloneElement(c, { ...this.state, ...this.functionProps }))
    }

    selectData() {
        let storeState = this.context.getState();
        return Object.entries(this.props.selectors).map(([k, v]) =>
                [k, v(storeState)])
            .reduce((result, [k, v]) => ({ ...result, [k]: v}), {});
    }

    handleDataStoreChange() {
        let newData = this.selectData();
        Object.keys(this.props.selectors)
            .filter(key => this.state[key] !== newData[key])
            .forEach(key => this.setState({ [key]: newData[key]}));
    }

    componentDidMount() {
        this.unsubscriber =
            this.context.subscribe(() => this.handleDataStoreChange());
    }

    componentWillUnmount() {
        this.unsubscriber();
    }
}

Listing 20-7The Contents of the CustomConnector.js File in the src/store Folder

我已经使用上下文 API 通过一个CustomConnectorProvider组件使数据存储可用,该组件由一个接收选择器和动作创建者属性的CustomConnector组件接收。选择器属性被处理来设置组件的状态,以便检测和处理变化,而动作创建器属性被包装在dispatch方法中,以便它们可以作为功能属性被连接的子组件调用。为了演示定制连接器,我将清单 20-8 中所示的内容添加到了App组件中。

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

import { CustomConnector, CustomConnectorProvider } from "./store/CustomConnector";

import { startEditingProduct } from "./store/stateActions";

import { ProductTable } from "./ProductTable";

const selectors = {

    products: (store) => store.modelData.products

}

const dispatchers = {

    editCallback: startEditingProduct,
    deleteCallback: deleteProduct

}

export default class App extends Component {

    render() {
        return <div className="container-fluid">
            <div className="row">
                <div className="col-3">
                    <StoreAccess store={ dataStore } />
                </div>
                <div className="col">
                    <Provider store={ dataStore }>
                        <Selector>
                            <ProductDisplay name="Products" />
                            <SupplierDisplay name="Suppliers" />
                        </Selector>
                    </Provider>
                </div>
            </div>
            <div className="row">
                <div className="col">
                    <CustomConnectorProvider dataStore={ dataStore }>
                        <CustomConnector selectors={ selectors }
                                dispatchers={ dispatchers }>
                            <ProductTable/>
                        </CustomConnector>
                    </CustomConnectorProvider>
                </div>
            </div>
        </div>
    }
}

Listing 20-8Using the Custom Connector in the App.js File in the src Folder

我不想替换现有的应用内容,所以我在引导网格布局中添加了一行,并用它来显示一个使用清单 20-8 中定义的组件连接到数据存储的ProductTable组件。为了简洁起见,CustomConnector组件被定义为CustomConnectorProvider组件的子组件,其作用是将选择器和动作创建者映射到呈现给ProductTable组件的属性。结果是应用显示第二个产品表,这两个表都反映了它们所显示的数据的变化,如图 20-6 所示。

img/473159_1_En_20_Fig6_HTML.jpg

图 20-6

使用自定义数据存储连接器

增强还原剂

正如我在第十九章中解释的,缩减器是一个处理动作和更新数据存储的函数。一个缩减器增强器是一个函数,它接受一个或多个正常的缩减器,并使用它们向数据存储添加额外的特性。

当使用一个缩减器增强器时,Redux 没有特别的意识,因为结果看起来就像一个常规的缩减器,并以同样的方式传递给createStore方法,正如来自src/store文件夹中的index.js文件的语句所示:

...
export default createStore(combineReducers(
    {
        modelData: modelReducer,
        stateData: stateReducer
    }));
...

combineReducers函数是 Redux 内置的一个 reducer 增强器,我在第十九章中使用它来保持模型和状态数据的 reducer 逻辑分离。

缩减增强器很有用,因为它们在处理动作之前接收动作,这意味着它们可以改变动作,拒绝动作,或者以特殊的方式处理动作,比如使用多个缩减器来处理动作,这就是combineReducers函数所做的。

为了演示一个 reducer enhancer,我在store文件夹中添加了一个名为customReducerEnhancer.js的文件,并添加了清单 20-9 中所示的代码。

import { initialData } from "./initialData";

export const STORE_RESET = "store_clear";

export const resetStore = () => ({ type: STORE_RESET });

export function customReducerEnhancer(originalReducer) {

    let intialState = null;

    return (storeData, action) => {
        if (action.type === STORE_RESET && initialData != null) {
            return intialState;
        } else {
            const result = originalReducer(storeData, action);
            if (intialState == null) {
                intialState = result;
            }
            return result;
        }
    }
}

Listing 20-9The Contents of the customReducerEnhancer.js File in the src/store Folder

customReducerEnhancer函数接受一个 reducer 作为它的参数,并返回一个新的 reducer 函数,这个函数可以被数据存储使用。enhancer 函数记录数据存储的初始状态,这是通过发送给 reducers 的第一个操作获得的。新的动作类型STORE_RESET使增强器函数返回初始数据存储状态,这具有重置数据存储的效果。所有其他动作都传递给普通减速器。为了帮助实现商店重置特性,清单 20-9 定义了一个resetStore动作创建函数。在清单 20-10 中,我将 reducer enhancer 应用于数据存储。

import { createStore, combineReducers } from "redux";
import modelReducer from "./modelReducer";
import stateReducer from "./stateReducer";

import { customReducerEnhancer } from "./customReducerEnhancer";

const enhancedReducer = customReducerEnhancer(

    combineReducers(
        {
            modelData: modelReducer,
            stateData: stateReducer
        })

);

export default createStore(enhancedReducer);

export { saveProduct, saveSupplier, deleteProduct, deleteSupplier }
    from "./modelActionCreators";

Listing 20-10Applying a Reducer Enhancer in the index.js File in the src/store Folder

还原增强剂可以结合使用。在这个清单中,我使用由combineReducers函数创建的缩减器作为customReducerEnhancer函数的参数。在清单 20-11 中,我使用了resetStore动作创建器在用户点击由StoreAccess组件呈现的按钮时创建一个动作。

import React, { Component } from "react";

//import { startCreatingProduct } from "./stateActions";

import { resetStore } from "./customReducerEnhancer";

export class StoreAccess extends Component {

    constructor(props) {
        super(props);
        this.selectors = {
            product: (storeState) => storeState.modelData.products[0],
            state: (storeState) => storeState.stateData
        }
        this.state = this.selectData();
    }

    render() {
        return <React.Fragment>
            <div className="text-center">
                <button className="btn btn-primary m-1"
                    onClick={ this.dispatchAction }>
                        Dispatch Action
                </button>
            </div>
            <div className="bg-info">
                <pre className="text-white">
                    { JSON.stringify(this.state, null, 2) }
                </pre>
            </div>
        </React.Fragment>
    }

    dispatchAction = () => {
        this.props.store.dispatch(resetStore())
    }

    // ...other methods omitted for brevity...
}

Listing 20-11Changing Actions in the StoreAccess.js File in the src/store Folder

增强器的作用是当用户点击 Dispatch Action 按钮时,应用的状态和模型数据被重置,结果是任何更改都被丢弃,如图 20-7 所示。

img/473159_1_En_20_Fig7_HTML.jpg

图 20-7

重置存储中的数据

使用数据存储中间件

Redux 提供了对数据存储中间件的支持,这些中间件是在动作被传递给dispatch方法之后、到达 reducer 之前接收动作的函数,允许它们被拦截、转换或以其他方式处理。中间件最常见的用途是添加对执行异步任务的动作的支持,并将动作包装在函数中,以便它们可以有条件地或在将来被调度。

注意

有一些中间件包可以解决常见的项目需求,您应该考虑用它们来代替编写定制代码。redux-promise包支持异步动作(见https://github.com/redux-utilities/redux-promise),redux-thunk包支持返回函数的动作创建器(见 https://github.com/reduxjs/redux-thunk )。然而,我发现这两个包都不符合我的要求,所以我更喜欢创建自己的中间件。

为了演示中间件的使用,我在src/store文件夹中添加了一个名为multiActionMiddleware.js的文件,并添加了清单 20-12 中所示的代码。

export function multiActions({dispatch, getState}) {
    return function receiveNext(next) {
        return function processAction(action) {
            if (Array.isArray(action)) {
                action.forEach(a => next(a));
            } else {
                next(action);
            }
        }
    }
}

Listing 20-12The Contents of the multiActionMiddleware.js File in the src/store Folder

中间件被表达为一组返回其他函数的函数,为了更容易理解,我在清单 20-12 中使用了function关键字。当中间件向数据存储注册时,调用外部函数multiActions,它接收数据存储的dispatchgetState方法,如下所示:

...
export function multiActions({dispatch, getState}) {
...

这为中间件提供了分派动作和获取数据存储中当前数据的能力。一个数据存储可以使用多个中间件组件;动作在链中从一个传递到下一个,然后传递给数据存储的dispatch方法。multiActions函数的工作是返回一个函数,该函数将在中间件链组装完成后被调用,并提供链中的下一个中间件组件。

...
export function multiActions({dispatch, getState}) {
    return function receiveNext(next) {
...

中间件组件通常会处理一个动作,然后通过调用next函数将它传递给链中的下一个组件。

receiveNext函数的结果是返回最里面的函数,当一个动作被分派到数据存储时调用这个函数,我在清单 20-12 中调用了这个函数processAction

...
export function multiActions({dispatch, getState}) {
    return function receiveNext(next) {
        return function processAction(action) {
...

这个函数能够在动作对象被传递到下一个中间件组件之前改变或替换它。也可以通过调用外部函数接收的dispatch方法来缩短链,或者什么都不做(在这种情况下,数据存储不会处理该动作)。我在清单 20-12 中定义的中间件组件检查动作是否是一个数组,在这种情况下,它将数组中包含的每个对象传递给下一个中间件组件进行处理。

定义嵌套函数有助于解释中间件组件是如何定义的,但是惯例是使用粗箭头函数,如清单 20-13 所示。

export const multiActions = ({dispatch, getState}) => next => action => {

    if (Array.isArray(action)) {
        action.forEach(a => next(a));
    } else {
        next(action);
    }
}

Listing 20-13Using Fat Arrow Functions in the multiActionMiddleware.js File in the src/store Folder

这与清单 20-12 的功能相同,但表述更简洁。Redux 提供了一个applyMiddlware函数,该函数用于创建与数据存储一起使用的中间件链,我在清单 20-14 中使用该函数将新的中间件组件添加到应用中。

import { createStore, combineReducers, applyMiddleware } from "redux";

import modelReducer from "./modelReducer";
import stateReducer from "./stateReducer";
import { customReducerEnhancer } from "./customReducerEnhancer";

import { multiActions } from "./multiActionMiddleware";

const enhancedReducer = customReducerEnhancer(
    combineReducers(
        {
            modelData: modelReducer,
            stateData: stateReducer
        })
);

export default createStore(enhancedReducer, applyMiddleware(multiActions));

export { saveProduct, saveSupplier, deleteProduct, deleteSupplier }
    from "./modelActionCreators";

Listing 20-14Registering Middleware in the index.js File in the src/store Folder

中间件函数作为参数传递给 Redux applyMiddleware函数,然后 ReduxapplyMiddleware函数的结果作为参数传递给createStore函数。

小费

多个中间件函数可以作为单独的参数传递给applyMiddleware函数,该函数将按照它们被指定的顺序将它们链接在一起。

既然数据存储可以处理动作数组,我就可以定义动作创建器来生成更复杂的结果,并允许更简单地表达连接器组件。我在src/store文件夹中添加了一个名为multiActionCreators.js的文件,并用它来定义清单 20-15 中所示的动作创建器。

import { PRODUCTS } from "./dataTypes";
import { saveProduct, saveSupplier } from "./modelActionCreators";
import { endEditing } from "./stateActions";

export const saveAndEndEditing = (data, dataType) =>
    [dataType === PRODUCTS ? saveProduct(data) : saveSupplier(data), endEditing()];

Listing 20-15The Contents of the multiActionCreators.js File in the src/store Folder

不要求将这样的动作创建器放在一个单独的文件中,但是这个创建器混合了影响模型和状态数据的动作,我更喜欢将它们分开。saveAndEndEditing动作接收一个数据对象和类型,并使用它产生一个动作数组,该数组将被中间件接收并按顺序分派。在清单 20-16 中,我替换了直接使用dispatch方法发送多个事件的EditorConnector组件中的语句。

import { connect } from "react-redux";
import { endEditing } from "./stateActions";

//import { saveProduct, saveSupplier } from "./modelActionCreators";

import { PRODUCTS, SUPPLIERS  } from "./dataTypes";

import { saveAndEndEditing } from "./multiActionCreators";

export const EditorConnector = (dataType, presentationComponent) => {

    const mapStateToProps = (storeData) => ({
        editing: storeData.stateData.editing
            && storeData.stateData.selectedType === dataType,
        product: (storeData.modelData[PRODUCTS]
            .find(p => p.id === storeData.stateData.selectedId)) || {},
        supplier:(storeData.modelData[SUPPLIERS]
            .find(s => s.id === storeData.stateData.selectedId)) || {}
    })

    const mapDispatchToProps = {
        cancelCallback: endEditing,
        saveCallback: (data) => saveAndEndEditing(data, dataType)
    }

    return connect(mapStateToProps, mapDispatchToProps)(presentationComponent);
}

Listing 20-16Dispatching Multiple Actions in the EditorConnector.js File in the src/store Folder

应用的行为没有变化,但是代码更简洁,更容易理解。

增强数据存储

大多数项目不需要修改数据存储的行为,如果需要,前一章描述的中间件特性就足够了。但是如果中间件不能提供足够的灵活性,一个更高级的选择是使用一个增强函数,这个函数负责创建数据存储对象,并且可以提供标准方法的包装器或者定义新的方法。

我之前用的applyMiddleware函数是增强器函数。这个函数取代了数据存储的dispatch方法,因此它可以在将动作传递给 reducer 之前,通过它的中间件组件链来引导动作。

为了演示增强器函数的使用,我将向数据存储中添加一个新方法,该方法异步调度操作。我在src/store文件夹中添加了一个名为asyncEnhancer.js的文件,并添加了清单 20-17 中所示的代码。

export function asyncEnhancer(delay) {
    return function(createStoreFunction) {
        return function(...args) {
            const store = createStoreFunction(...args);
            return {
                ...store,
                dispatchAsync: (action) => new Promise((resolve, reject) => {
                    setTimeout(() => {
                        store.dispatch(action);
                        resolve();
                    }, delay);
                })
            };
        }
    }
}

Listing 20-17The Contents of the asyncEnhancer.js File in the src/store Folder

增强器异步调度动作,返回一个Promise,一旦动作被调度,它就会被解析。示例应用中目前没有需要异步工作的任务,因此我在将动作分派给类似的后台活动之前引入了一个延迟。

这是另一个 Redux 特性,它需要一组嵌套的函数,我使用function关键字定义了这些函数,以便解释它们是如何组合在一起的。当增强器被应用到数据存储时,外部函数被调用,并提供一个机会来接收配置增强器行为的参数。清单 20-17 中最外层的函数接收一个动作被分派前应用的延迟长度。

...
export function asyncEnhancer(delay) {
...

现在它变得更加复杂:外部函数的结果是一个接收createStore函数的函数。功能这个词出现了太多次,以至于前面的句子无法立即理解,因此有必要解释一下发生了什么。

为了给增强器完全的控制权,Redux 让它们用一个定制的替代函数来代替createStore函数。但是大多数 reducers 只需要向标准数据存储添加特性,所以 Redux 提供了现有的createStore函数。

...
export function asyncEnhancer(delay) {
    return function(createStoreFunction) {
...

当增强器被应用时,这个函数将被调用,结果将被用来替换标准的createStore函数,这将我们带到清单 20-17 中最里面的函数,它完成所有的工作。

...
return function(...args) {
    const store = createStoreFunction(...args);
    return {
        ...store,
        dispatchAsync: (action) => new Promise((resolve, reject) => {

             // ...statements omitted for brevity...

        })
    };
}
...

创建数据存储时,Redux 调用增强器提供的函数,并将结果用作数据存储对象,确保应用的其余部分可以使用任何附加特性。在这种情况下,增强器使用标准的createStore函数,然后向结果添加一个dispatchAsync方法。新方法接收一个动作,并在一段延迟后调度它。使用function关键字可以更容易地看到嵌套函数之间的关系,但是增强器通常使用粗箭头函数来表示,如清单 20-18 所示。这是相同的功能,但表达更简洁。

export const asyncEnhancer = delay => createStoreFunction => (...args) => {

    const store = createStoreFunction(...args);
    return {
        ...store,
        dispatchAsync: (action) => new Promise((resolve, reject) => {
            setTimeout(() => {
                store.dispatch(action);
                resolve();
            }, delay);
        })
    };
}

Listing 20-18Using Arrow Functions in the asyncEnhancer.js File in the src/store Folder

应用增强剂

标准的createStore函数只能接受一个增强函数,我已经在使用applyMiddleware增强函数了。幸运的是,reducer 函数可以被组合,这样来自一个增强器的结果可以传递给另一个增强器。为了简化组合函数的过程,Redux 提供了compose函数,我在清单 20-19 中使用了这个函数来将新的增强器应用到数据存储中。

import { createStore, combineReducers, applyMiddleware, compose } from "redux";

import modelReducer from "./modelReducer";
import stateReducer from "./stateReducer";
import { customReducerEnhancer } from "./customReducerEnhancer";
import { multiActions } from "./multiActionMiddleware";
import { asyncEnhancer } from "./asyncEnhancer";

const enhancedReducer = customReducerEnhancer(
    combineReducers(
        {
            modelData: modelReducer,
            stateData: stateReducer
        })
);

export default createStore(enhancedReducer,
    compose(applyMiddleware(multiActions), asyncEnhancer(2000)));

export { saveProduct, saveSupplier, deleteProduct, deleteSupplier }
    from "./modelActionCreators";

Listing 20-19Adding an Enhancer in the index.js File in the src/store Folder

来自compose函数的结果被传递给createStore,两个增强器都被应用于数据存储,增加了中间件和新的dispatchAsync方法。在清单 20-20 中,我更新了StoreAccess组件,以便它在调度动作时使用增强的数据存储方法,并禁用button元素,直到后台任务完成。

import React, { Component } from "react";
import { resetStore } from "./customReducerEnhancer";

export class StoreAccess extends Component {

    constructor(props) {
        super(props);
        this.selectors = {
            product: (storeState) => storeState.modelData.products[0],
            state: (storeState) => storeState.stateData
        }
        this.state = this.selectData();
        this.buttonRef = React.createRef();
    }

    render() {
        return <React.Fragment>
            <div className="text-center">
                <button className="btn btn-primary m-1" ref={ this.buttonRef }
                    onClick={ this.dispatchAction }>
                        Dispatch Action
                </button>
            </div>
            <div className="bg-info">
                <pre className="text-white">
                    { JSON.stringify(this.state, null, 2) }
                </pre>
            </div>
        </React.Fragment>
    }

    dispatchAction = () => {
        this.buttonRef.current.disabled = true;
        this.props.store.dispatchAsync(resetStore())
            .then(data => this.buttonRef.current.disabled = false);
    }

    // ...other methods omitted for brevity...
}

Listing 20-20Using the Enhanced Data Store in the StoreAccess.js File in the src/store Folder

结果是,单击按钮将调度操作,该操作将在两秒钟的显示后被处理。组件在调度动作时会收到一个Promise,这个问题一旦被调度就会被解决,允许组件再次启用按钮元素,如图 20-8 所示。

img/473159_1_En_20_Fig8_HTML.jpg

图 20-8

使用增强型数据存储

使用 React-Redux API

前面几节演示了您可以直接使用 Redux API 将组件连接到数据存储。然而,对于大多数项目来说,使用 React-Redux 包更简单、更容易,如第十九章所示。在接下来的小节中,我将描述 React-Redux 包提供的高级选项,用于配置组件如何连接到数据存储。

高级连接功能

connect方法通常与两个参数一起使用,这两个参数选择数据属性和函数属性,就像来自TableConnector组件的语句:

...
return connect(mapStateToProps, mapDispatchToProps)(presentationComponent);
...

connect函数可以接受一些高级特性的附加参数,并且可以接收以不同方式表达的参数。在本节中,我将解释使用您已经熟悉的参数的选项,介绍新的参数并演示它们的用法。

映射数据属性

connect函数的第一个参数从存储中为组件的数据属性选择数据。通常,选择器被定义为从商店的getState方法接收值并返回一个对象的函数,该对象的属性对应于属性名。当数据存储发生变化时,选择器函数被调用,由connect函数创建的高阶组件使用shouldComponentUpdate生命周期方法(在第十三章中描述)来查看是否有任何变化的值需要连接器组件更新。

数据值的选择是灵活的,不仅仅是将数据存储属性映射到属性。例如,在TableConnector组件中,我使用选择器函数来映射来自商店不同部分的数据值,如下所示:

...
const mapStateToProps = (storeData) => ({
    products: storeData.modelData[PRODUCTS],
    suppliers: storeData.modelData[SUPPLIERS].map(supp => ({
        ...supp,
        products: supp.products.map(id =>
            storeData.modelData[PRODUCTS].find(p => p.id === Number(id)) || id)
                .map(val => val.name || val)
    }))
})
...

选择器函数也可以用第二个参数表示,该参数用于接收父连接器组件为连接器组件提供的属性。这允许在选择数据时使用组件的属性,并确保当组件的属性发生变化以及数据存储发生变化时,选择器函数将被重新评估。清单 20-21 演示了附加参数的用法。

import { connect } from "react-redux";
import { startEditingProduct, startEditingSupplier } from "./stateActions";
import { deleteProduct, deleteSupplier } from "./modelActionCreators";
import { PRODUCTS, SUPPLIERS } from "./dataTypes";

export const TableConnector = (dataType, presentationComponent) => {

    const mapStateToProps = (storeData, ownProps) => {
        if (!ownProps.needSuppliers) {
            return { products: storeData.modelData[PRODUCTS] };
        } else {
            return {
                suppliers: storeData.modelData[SUPPLIERS].map(supp => ({
                    ...supp,
                    products: supp.products.map(id =>
                        storeData.modelData[PRODUCTS]
                            .find(p => p.id === Number(id)) || id)
                            .map(val => val.name || val)
                    }))
            }
        }
    }

    const mapDispatchToProps = {
        editCallback: dataType === PRODUCTS
            ? startEditingProduct : startEditingSupplier,
        deleteCallback: dataType === PRODUCTS ? deleteProduct : deleteSupplier
    }

    return connect(mapStateToProps, mapDispatchToProps)(presentationComponent);
}

Listing 20-21Using an Additional Selector Argument in the TableConnector.js File in the src/store Folder

创建应用于多个组件的连接器的一个问题是选择了太多的数据,当数据存储中的更改影响一个组件使用而另一个组件不使用的属性时,这可能导致不必要的更新。TableConnector组件是产品和供应商数据表的连接器,但是只有供应商数据需要从数据存储映射suppliers属性。对于产品表来说,这不仅意味着浪费了suppliers属性的计算,而且当没有显示的数据发生变化时,它还会导致更新。

附加参数——通常命名为ownProps—允许通过标准的 React prop 特性定制连接器组件的每个实例。在清单 20-21 中,我使用了ownProps参数,根据应用于连接器组件的名为needSuppliers的属性的值,决定将哪些属性映射到数据存储。如果值是true,则suppliers属性被映射到数据存储器,否则products属性被映射。

在清单 20-22 中,我向由SupplierDisplay组件呈现的ConnectedTable组件添加了needSuppliers属性,这将确保它映射其表示组件所需的数据。由ProductDisplay组件渲染的相应的ConnectedTable组件没有needSuppliers属性,不会从商店接收数据。

...
export const SupplierDisplay = connectFunction(
    class extends Component {

        render() {
            if (this.props.editing) {
                return <ConnectedEditor key={ this.props.selected.id || -1 } />
            } else {
                return <div className="m-2">
                    <ConnectedTable needSuppliers={ true } />
                    <div className="text-center">
                        <button className="btn btn-primary m-1"
                            onClick={ this.props.createSupplier }>
                                Create Supplier
                        </button>
                    </div>
                </div>
            }
        }
    })
...

Listing 20-22Adding a Prop in the SupplierDisplay.js File in the src Folder

应用的行为没有区别,但是在幕后,通过ConnectedTable组件连接到数据存储的每个表示组件使用不同的属性。

过早优化的危险

在发现性能问题之前,不要过于担心优化更新。几乎所有的优化都会增加项目的复杂性,您可能会发现未优化的代码带来的性能损失是不可察觉的,或者不足以成为一个需要担心的问题。试图优化可能不存在的问题很容易陷入困境,更好的方法是尽可能编写最清晰、最简单的代码,然后优化不符合您要求的部分。

映射功能属性

正如我在第十九章中所解释的,第二个connect参数在函数属性之间映射,可以被指定为一个对象或者一个函数。当提供一个对象时,该对象的每个属性的值被假定为一个动作创建者函数,并被自动包装在dispatch方法中,映射到一个函数 prop。当提供一个函数时,该函数被赋予dispatch方法,并负责使用它来创建函数属性映射。

小费

您可以省略connect函数的第二个参数,其中dispatch方法被映射到一个 prop,也称为dispatch,它允许组件创建动作并直接调度它们。

如果您指定了一个函数,那么您也可以选择接收连接器组件 props,如前一节所述。这允许高级组件从其父组件接收关于映射到数据存储的功能属性集的指示。在清单 20-23 中,我使用了一个函数来配置函数属性,并定义了第二个参数来接收组件的属性。

import { connect } from "react-redux";
import { startEditingProduct, startEditingSupplier } from "./stateActions";
import { deleteProduct, deleteSupplier } from "./modelActionCreators";
import { PRODUCTS, SUPPLIERS } from "./dataTypes";

export const TableConnector = (dataType, presentationComponent) => {

    const mapStateToProps = (storeData, ownProps) => {
        if (!ownProps.needSuppliers) {
            return { products: storeData.modelData[PRODUCTS] };
        } else {
            return {
                suppliers: storeData.modelData[SUPPLIERS].map(supp => ({
                    ...supp,
                    products: supp.products.map(id =>
                        storeData.modelData[PRODUCTS]
                            .find(p => p.id === Number(id)) || id)
                            .map(val => val.name || val)
                    }))
            }
        }
    }

    const mapDispatchToProps = (dispatch, ownProps) => {
        if (!ownProps.needSuppliers) {
            return {
                editCallback: (...args) => dispatch(startEditingProduct(...args)),
                deleteCallback: (...args) => dispatch(deleteProduct(...args))
            }
        } else {
            return {
                editCallback: (...args) => dispatch(startEditingSupplier(...args)),
                deleteCallback: (...args) => dispatch(deleteSupplier(...args))
            }
        }
    }

    return connect(mapStateToProps, mapDispatchToProps)(presentationComponent);
}

Listing 20-23Using Props in the TableConnector.js File in the src/store Folder

合并属性

connect函数接受第三个参数,该参数用于在将属性传递给表示组件之前组成属性。这个参数称为mergeProps,是一个函数,它接收映射的数据属性、函数属性和连接的组件属性,并返回一个对象,将它们合并到用作表示组件属性的对象中。

默认情况下,属性从从父属性接收的属性开始组成,然后与数据属性和功能属性组合。这意味着从父节点接收的属性将被具有相同名称的映射数据属性替换,并且如果存在具有相同名称的映射函数属性,则两者都将被替换。mergeProps函数可用于在名称冲突时更改优先级,以及绑定动作,以便使用从父节点接收的值作为属性来调度它们。清单 20-24 展示了如何使用mergeProps参数显式合并属性。

import { connect } from "react-redux";
import { endEditing } from "./stateActions";
import { PRODUCTS, SUPPLIERS  } from "./dataTypes";
import { saveAndEndEditing } from "./multiActionCreators";

export const EditorConnector = (dataType, presentationComponent) => {

    const mapStateToProps = (storeData) => ({
        editing: storeData.stateData.editing
            && storeData.stateData.selectedType === dataType,
        product: (storeData.modelData[PRODUCTS]
            .find(p => p.id === storeData.stateData.selectedId)) || {},
        supplier:(storeData.modelData[SUPPLIERS]
            .find(s => s.id === storeData.stateData.selectedId)) || {}
    })

    const mapDispatchToProps = {
        cancelCallback: endEditing,
        saveCallback: (data) => saveAndEndEditing(data, dataType)
    }

    const mergeProps = (dataProps, functionProps, ownProps) =>
        ({ ...dataProps, ...functionProps, ...ownProps })

    return connect(mapStateToProps, mapDispatchToProps,
        mergeProps)(presentationComponent);
}

Listing 20-24Merging Props in the EditorConnector.js File in the src/store Folder

清单 20-24 中的mergeProps函数组合了每个属性对象的属性。属性是按照指定的顺序从对象中复制的,这意味着函数最后从ownProps中复制,也意味着当有相同名称的属性时,将使用从父对象接收的属性。

设置连接选项

connect方法的最后一个参数通常被命名为options,它是一个用于配置到数据存储的连接的对象。options对象可以用属性定义,属性名称如表 20-4 所示。

表 20-4

选项对象属性名称

|

名字

|

描述

| | --- | --- | | pure | 默认情况下,连接器组件仅在其自身属性发生变化或数据存储中的某个选定值发生变化时才会更新,这允许由connect创建的高阶组件(HOC)在属性或数据未发生变化时阻止更新。将该属性设置为false表示连接器组件可能依赖于其他数据,并且特设不会试图阻止更新。该属性的默认值为true。 | | areStatePropsEqual | 当pure属性为true时,该函数用于覆盖mapStateToProps值的默认相等比较,以最小化更新。 | | areOwnPropsEqual | 当pure属性为true时,该函数用于覆盖mapDispatchToProps值的默认相等比较,以最小化更新。 | | areMergedPropsEqual | 当pure属性为true时,该函数用于覆盖mergeProps值的默认相等比较,以最小化更新。 | | areStatesEqual | 当pure属性为true时,该函数用于覆盖整个组件状态的默认相等比较,以最小化更新。 |

摘要

在本章中,我描述了 Redux 和 React-Redux 包提供的 API,并演示了如何使用它们。我向您展示了如何使用 Redux API 将组件直接连接到数据存储,如何增强数据存储及其 reducers,以及如何定义中间件组件。我还演示了使用 React-Redux 包时可用的高级选项,该包可用于管理组件到数据存储的连接。在下一章中,我将向示例应用介绍 URL 路由。