React-挂钩学习手册-四-

51 阅读1小时+

React 挂钩学习手册(四)

原文:zh.annas-archive.org/md5/0d61b163bb6c28fa00edc962fdaa2667

译者:飞龙

协议:CC BY-NC-SA 4.0

第三部分:集成和迁移

在书的最后部分,我们将学习如何将现有的状态管理解决方案与 Hooks 结合使用。此外,我们将演示如何将 React 类组件以及现有的 Redux 和 MobX 应用程序迁移到 Hooks。

在本节中,我们将涵盖以下章节:

  • 第十一章,从 React 类组件迁移

  • 第十二章,Redux 和 Hooks

  • 第十三章,MobX 和 Hooks

第十一章:从 React 类组件迁移

在上一章中,我们学习了如何通过从现有代码中提取自定义 Hooks 来构建我们自己的 Hooks。然后,我们在博客应用程序中使用了我们自己的 Hooks,并学习了本地 Hooks 和 Hooks 之间的交互。最后,我们学习了如何使用 React Hooks 测试库为 Hooks 编写测试,并为我们的自定义 Hooks 实现了测试。

在本章中,我们将首先使用 React 类组件实现一个待办事项应用程序。接下来,我们将学习如何将现有的 React 类组件应用程序迁移到 Hooks。在实践中看到使用 Hooks 的函数组件和类组件之间的差异将加深我们对使用任一解决方案的权衡的理解。此外,到本章结束时,我们将能够将现有的 React 应用程序迁移到 Hooks。

本章将涵盖以下主题:

  • 使用类组件处理状态

  • 从类组件迁移应用程序到 Hooks

  • 了解类组件与 Hooks 的权衡

技术要求

应该已经安装了相当新的 Node.js 版本(v11.12.0 或更高版本)。还需要安装 Node.js 的npm包管理器。

本章的代码可以在 GitHub 存储库中找到:github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter11

观看以下视频以查看代码的实际操作:

bit.ly/2Mm9yoC

请注意,强烈建议您自己编写代码。不要简单地运行提供的代码示例。重要的是您自己编写代码,以便能够正确学习和理解。但是,如果遇到任何问题,您可以随时参考代码示例。

现在,让我们开始本章。

使用类组件处理状态

在我们开始从类组件迁移到 Hooks 之前,我们将使用 React 类组件创建一个小型的待办事项列表应用程序。在下一节中,我们将使用 Hooks 将这些类组件转换为函数组件。最后,我们将比较这两种解决方案。

设计应用程序结构

与我们之前在博客应用程序中所做的一样,我们将首先考虑我们应用程序的基本结构。对于这个应用程序,我们将需要以下功能:

  • 一个标题

  • 添加新待办事项的方法

  • 以列表的方式显示所有待办事项

  • 待办事项的筛选器

从模拟开始总是一个好主意。所以,让我们开始吧:

  1. 我们首先绘制一个 ToDo 应用程序界面的模拟:

我们的 ToDo 应用程序的模拟

  1. 接下来,我们以类似的方式定义基本组件,就像我们在博客应用程序中所做的那样:

在我们应用程序的模拟中定义基本组件

  1. 现在我们可以定义容器组件:

在我们应用程序的模拟中定义容器组件

正如我们所看到的,我们将需要以下组件:

  • App

  • 标题

  • 添加待办事项

  • TodoList

  • TodoItem

  • TodoFilter(+ TodoFilterItem)

TodoList组件使用TodoItem组件,用于显示一个带有复选框完成和删除按钮的项目。TodoFilter组件内部使用TodoFilterItem组件来显示各种筛选器。

初始化项目

我们将使用create-react-app来创建一个新项目。让我们现在初始化项目:

  1. 运行以下命令:
> npx create-react-app chapter11_1
  1. 然后,删除src/App.css,因为我们不需要它。

  2. 接下来,编辑src/index.css,并调整边距如下:

    margin: 20px;
  1. 最后,删除当前的src/App.js文件,因为我们将在下一步创建一个新的文件。

现在,我们的项目已经初始化,我们可以开始定义应用程序结构。

定义应用程序结构

我们已经从模拟中知道了我们的应用程序的基本结构,所以让我们从定义App组件开始:

  1. 创建一个新的src/App.js文件。

  2. 导入ReactHeaderAddTodoTodoListTodoFilter组件:

import React from 'react'

import Header from './Header'
import AddTodo from './AddTodo'
import TodoList from './TodoList'
import TodoFilter from './TodoFilter'
  1. 现在将App组件定义为类组件。现在,我们只会定义render方法:
export default class App extends React.Component {
    render () {
        return (
            <div style={{ width: 400 }}>
                <Header />
                <AddTodo />
                <hr />
                <TodoList />
                <hr />
                <TodoFilter />
            </div>
        )
    }
}

App组件定义了我们应用程序的基本结构。它将包括一个标题,一种添加新待办事项的方法,待办事项列表和一个筛选器。

定义组件

现在,我们将定义组件作为静态组件。在本章的后面,我们将为它们实现动态功能。现在,我们将实现以下静态组件:

  • 标题

  • 添加待办事项

  • TodoList

  • TodoItem

  • TodoFilter

现在让我们开始实现这些组件。

定义标题组件

我们将从Header组件开始,因为它是所有组件中最简单的:

  1. 创建一个新的src/Header.js文件。

  2. 导入React并定义带有render方法的类组件:

import React from 'react'

export default class Header extends React.Component {
    render () {
        return <h1>ToDo</h1>
    }
}

现在,我们已经定义了应用程序的Header组件。

定义 AddTodo 组件

接下来,我们将定义AddTodo组件,它渲染一个input字段和一个按钮。

现在让我们实现AddTodo组件:

  1. 创建一个新的src/AddTodo.js文件。

  2. 导入React并定义类组件和render方法:

import React from 'react'

export default class AddTodo extends React.Component {
    render () {
        return (
  1. render方法中,我们返回一个包含input字段和添加按钮的form
            <form>
                <input type="text" placeholder="enter new task..." style={{ width: 350, height: 15 }} />
                <input type="submit" style={{ float: 'right', marginTop: 2 }} value="add" />
            </form>
        )
    }
}

正如我们所看到的,AddTodo组件由一个input字段和一个按钮组成。

定义 TodoList 组件

现在,我们定义了TodoList组件,它将使用TodoItem组件。目前,我们将在这个组件中静态地定义两个待办事项。

让我们开始定义TodoList组件:

  1. 创建一个新的src/TodoList.js文件。

  2. 导入ReactTodoItem组件:

import React from 'react'

import TodoItem from './TodoItem'
  1. 然后,定义类组件和render方法:
export default class TodoList extends React.Component {
    render () {
  1. 在这个render方法中,我们静态地定义了两个待办事项:
        const items = [
            { id: 1, title: 'Write React Hooks book', completed: true },
            { id: 2, title: 'Promote book', completed: false }
        ]
  1. 最后,我们将使用map函数来渲染项目:
        return items.map(item =>
            <TodoItem {...item} key={item.id} />
        )
    }
}

正如我们所看到的,TodoList组件渲染了一系列TodoItem组件。

定义 TodoItem 组件

在定义了TodoList组件之后,我们现在将定义TodoItem组件,以便渲染单个项目。

让我们开始定义TodoItem组件:

  1. 创建一个新的src/TodoItem.js组件。

  2. 导入React,并定义组件,以及render方法:

import React from 'react'

export default class TodoItem extends React.Component {
    render () {
  1. 现在,我们将使用解构来获取titlecompleted属性:
        const { title, completed } = this.props
  1. 最后,我们将渲染一个包含checkboxtitlebuttondiv元素来删除项目:
        return (
            <div style={{ width: 400, height: 25 }}>
                <input type="checkbox" checked={completed} />
                {title}
                <button style={{ float: 'right' }}>x</button>
            </div>
        )
    }
}

TodoItem组件由一个复选框、title和一个删除项目的button组成。

定义 TodoFilter 组件

最后,我们将定义TodoFilter组件。在同一个文件中,我们将为TodoFilterItem定义另一个组件。

让我们开始定义TodoFilterItemTodoFilter组件:

  1. 创建一个新的src/TodoFilter.js文件。

  2. TodoFilterItem定义一个类组件:

class TodoFilterItem extends React.Component {
    render () {
  1. 在这个render方法中,我们使用解构来获取name属性:
        const { name } = this.props
  1. 接下来,我们将为style定义一个对象:
        const style = {
            color: 'blue',
            cursor: 'pointer'
        }
  1. 然后,我们返回一个带有过滤器name值的span元素,并使用定义的style对象:
        return <span style={style}>{name}</span>
    }
}
  1. 最后,我们可以定义实际的TodoFilter组件,它将呈现三个TodoFilterItem组件,如下所示:
export default class TodoFilter extends React.Component {
    render () {
        return (
            <div>
                <TodoFilterItem name="all" />{' / '}
                <TodoFilterItem name="active" />{' / '}
                <TodoFilterItem name="completed" />
            </div>
        )
    }
}

现在,我们有一个列出三种不同过滤可能性的组件:allactivecompleted

实现动态代码

现在我们已经定义了所有静态组件,我们的应用程序应该看起来像模拟一样。下一步是使用 React 状态、生命周期和处理程序方法实现动态代码。

在本节中,我们将执行以下操作:

  • 定义模拟 API

  • 定义一个StateContext

  • 使App组件动态化

  • 使AddTodo组件动态化

  • 使TodoList组件动态化

  • 使TodoItem组件动态化

  • 使TodoFilter组件动态化

让我们开始。

定义 API 代码

首先,我们将定义一个 API,用于获取待办事项。在我们的情况下,我们将简单地在短暂延迟后返回一个待办事项数组。

让我们开始实现模拟 API:

  1. 创建一个新的src/api.js文件。

  2. 我们将定义一个函数,根据通用唯一标识符UUID)函数生成我们待办事项的随机 ID:

export const generateID = () => {
    const S4 = () =>(((1+Math.random())*0x10000)|0).toString(16).substring(1)
    return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4())
}
  1. 然后,我们定义了fetchAPITodos函数,它返回一个Promise,在短暂延迟后解析:
export const fetchAPITodos = () =>
    new Promise((resolve) =>
        setTimeout(() => resolve([
            { id: generateID(), title: 'Write React Hooks book', completed: true },
            { id: generateID(), title: 'Promote book', completed: false }
        ]), 100)
    )

现在,我们有一个函数,模拟从 API 获取待办事项,通过在100毫秒延迟后返回一个数组。

定义 StateContext

接下来,我们将定义一个上下文,用于保存我们当前的待办事项列表。我们将称此上下文为StateContext

让我们现在开始实现StateContext

  1. 创建一个新的src/StateContext.js文件。

  2. 导入React,如下所示:

import React from 'react'
  1. 现在,定义StateContext并将空数组设置为回退值:
const StateContext = React.createContext([])
  1. 最后,导出StateContext
export default StateContext

现在,我们有一个上下文,可以在其中存储我们的待办事项数组。

使 App 组件动态化

现在,我们将通过添加功能来获取、添加、切换、过滤和删除待办事项,使App组件动态化。此外,我们将定义一个StateContext提供程序。

让我们开始使App组件动态化:

  1. src/App.js中,在其他导入语句之后导入StateContext
import StateContext from './StateContext'
  1. 然后,从src/api.js文件导入fetchAPITodosgenerateID函数:
import { fetchAPITodos, generateID } from './api'
  1. 接下来,我们将修改我们的App类代码,实现一个constructor,它将设置初始状态:
export default class App extends React.Component {
 constructor (props) {
  1. 在这个constructor中,我们需要首先调用super,以确保父类(React.Component)的构造函数被调用,并且组件得到正确初始化:
        super(props)
  1. 现在,我们可以通过设置this.state来设置初始状态。最初,没有待办事项,filter值将设置为'all'
        this.state = { todos: [], filteredTodos: [], filter: 'all' }
    }
  1. 然后,我们定义componentDidMount生命周期方法,该方法将在组件首次渲染时获取待办事项:
    componentDidMount () {
        this.fetchTodos()
    }
  1. 现在,我们将定义实际的fetchTodos方法,在我们的情况下,它只是设置状态,因为我们不打算将这个简单的应用程序连接到后端。我们还将调用this.filterTodos()来在获取待办事项后更新filteredTodos数组:
    fetchTodos () {
        fetchAPITodos().then((todos) => {
            this.setState({ todos })
            this.filterTodos()
        })
    }
  1. 接下来,我们定义addTodo方法,它创建一个新项目,并将其添加到状态数组中,类似于我们在博客应用中使用 Hooks 所做的操作:
    addTodo (title) {
        const { todos } = this.state

        const newTodo = { id: generateID(), title, completed: false }

        this.setState({ todos: [ newTodo, ...todos ] })
        this.filterTodos()
    }
  1. 然后,我们定义toggleTodo方法,该方法使用map函数来查找和修改特定的待办事项:
    toggleTodo (id) {
        const { todos } = this.state

        const newTodos = todos.map(t => {
            if (t.id === id) {
                return { ...t, completed: !t.completed }
            }
            return t
        }, [])

        this.setState({ todos: newTodos })
        this.filterTodos()
    }
  1. 现在,我们定义removeTodo方法,该方法使用filter函数来查找并删除特定的待办事项:
    removeTodo (id) {
        const { todos } = this.state

        const newTodos = todos.filter(t => {
            if (t.id === id) {
                return false
            }
             return true
        })

        this.setState({ todos: newTodos })
        this.filterTodos()
    }
  1. 然后,我们定义一个方法来对我们的待办事项应用特定的filter
    applyFilter (todos, filter) {
        switch (filter) {
            case 'active':
                return todos.filter(t => t.completed === false)

            case 'completed':
                return todos.filter(t => t.completed === true)

            default:
            case 'all':
                return todos
        }
    }
  1. 现在,我们可以定义filterTodos方法,该方法将调用applyFilter方法,并更新filteredTodos数组和filter值:
    filterTodos (filterArg) {
        this.setState(({ todos, filter }) => ({
            filter: filterArg || filter,
            filteredTodos: this.applyFilter(todos, filterArg || filter)
        }))
    }

我们使用filterTodos来在添加/删除项目以及更改过滤器后重新过滤待办事项,为了使这两个功能都能正常工作,我们需要检查是否传递了filter参数filterArg。如果没有,我们将退回到state中的当前filter参数。

  1. 然后,我们调整render方法,以使用状态为StateContext提供一个值,并将某些方法传递给组件:
    render () {
 const { filter, filteredTodos } = this.state

        return (
 <StateContext.Provider value={filteredTodos}>
                <div style={{ width: 400 }}>
                    <Header />
                    <AddTodo addTodo={this.addTodo} />
                    <hr />
                    <TodoList toggleTodo={this.toggleTodo} removeTodo={this.removeTodo} />
                    <hr />
                    <TodoFilter filter={filter} filterTodos={this.filterTodos} />
                </div>
 </StateContext.Provider>
        )
    }
  1. 最后,我们需要重新绑定this到类,以便我们可以将方法传递给我们的组件,而不会改变this上下文。调整constructor如下:
            constructor () {
                super(props)

                this.state = { todos: [], filteredTodos: [], filter: 
                  'all' }

 this.fetchTodos = this.fetchTodos.bind(this)
 this.addTodo = this.addTodo.bind(this)
 this.toggleTodo = this.toggleTodo.bind(this)
 this.removeTodo = this.removeTodo.bind(this)
 this.filterTodos = this.filterTodos.bind(this)
            }

现在,我们的App组件可以动态地获取、添加、切换、删除和过滤待办事项。正如我们所看到的,当我们使用类组件时,我们需要重新绑定处理程序函数的this上下文到类。

使 AddTodo 组件动态化

在使我们的App组件动态化之后,现在是时候使我们的所有其他组件也动态化了。我们将从顶部开始,从AddTodo组件开始。

现在让AddTodo组件动态化:

  1. src/AddTodo.js中,我们首先定义了一个constructor,它为input字段设置了初始state
export default class AddTodo extends React.Component {
    constructor (props) {
        super(props)

        this.state = {
            input: ''
        }
    }
  1. 然后,我们定义一个处理input字段变化的方法:
    handleInput (e) {
        this.setState({ input: e.target.value })
    }
  1. 现在,我们将定义一个可以处理添加新待办事项的方法:
    handleAdd () {
        const { input } = this.state
        const { addTodo } = this.props

        if (input) {
            addTodo(input)
            this.setState({ input: '' })
        }
    }
  1. 接下来,我们可以将状态值和处理程序方法分配给input字段和按钮:
    render () {
        const { input } = this.state

        return (
            <form onSubmit={e => { e.preventDefault(); this.handleAdd() }}>
                <input
                    type="text"
                    placeholder="enter new task..."
                    style={{ width: 350, height: 15 }}
 value={input}
 onChange={this.handleInput} />
                <input
                    type="submit"
                    style={{ float: 'right', marginTop: 2 }}
 disabled={!input}                    value="add"
                />
            </form>
        )
    }
  1. 最后,我们需要调整constructor以重新绑定所有处理程序方法的this上下文:
    constructor () {
        super(props)

        this.state = {
            input: ''
        }

 this.handleInput = this.handleInput.bind(this)
 this.handleAdd = this.handleAdd.bind(this)
    }

现在,我们的AddTodo组件将在没有输入文本时显示禁用的按钮。激活后,单击按钮将触发从App组件传递下来的handleAdd函数。

使 TodoList 组件动态

我们 ToDo 应用程序中的下一个组件是TodoList组件。在这里,我们只需要从StateContext中获取待办事项。

现在让我们让TodoList组件变得动态起来:

  1. src/TodoList.js中,我们首先导入StateContext,在TodoItem导入语句下面:
import StateContext from './StateContext'
  1. 然后,我们将contextType设置为StateContext,这将允许我们通过this.context访问上下文:
export default class TodoList extends React.Component {
 static contextType = StateContext

使用类组件,如果我们想要使用多个上下文,我们必须使用StateContext.Consumer组件,如下所示:<StateContext.Consumer>{value => <div>State is: {value}</div>}</StateContext.Consumer>

正如你所想象的那样,像这样使用多个上下文将导致非常深的组件树(包装器地狱),我们的代码将很难阅读和重构。

  1. 现在,我们可以从this.context中获取项目,而不是静态定义它们:
    render () {
 const items = this.context
  1. 最后,我们将所有 props 传递给TodoItem组件,以便我们可以在那里使用removeTodotoggleTodo方法:
        return items.map(item =>
            <TodoItem {...item} {...this.props} key={item.id} />
        )
    }

现在,我们的TodoList组件从StateContext中获取项目,而不是静态定义它们。

使 TodoItem 组件动态

现在我们已经将removeTodotoggleTodo方法作为 props 传递给了TodoItem组件,我们可以在那里实现这些功能。

现在让TodoItem组件变得动态起来:

  1. src/TodoItem.js中,我们首先定义了toggleTodoremoveTodo函数的处理程序方法:
    handleToggle () {
        const { toggleTodo, id } = this.props
        toggleTodo(id)
    }

    handleRemove () {
        const { removeTodo, id } = this.props
        removeTodo(id)
    }
  1. 然后,我们分别将处理程序方法分配给checkboxbutton
    render () {
        const { title, completed } = this.props
        return (
            <div style={{ width: 400, height: 25 }}>
                <input type="checkbox" checked={completed} onChange={this.handleToggle} />
                {title}
                <button style={{ float: 'right' }} onClick={this.handleRemove}>x</button>
            </div>
        )
    }
  1. 最后,我们需要重新绑定处理程序方法的this上下文。创建一个新的constructor,如下所示:
export default class TodoItem extends React.Component {
 constructor (props) {
 super(props)

 this.handleToggle = this.handleToggle.bind(this)
 this.handleRemove = this.handleRemove.bind(this)
 }

现在,TodoItem组件触发了切换和删除处理程序函数。

使 TodoFilter 组件动态

最后,我们将使用filterTodos方法动态过滤我们的待办事项列表。

让我们开始使TodoFilter组件动态:

  1. src/TodoFilter.js中,在TodoFilter类中,我们将所有 props 传递给TodoFilterItem组件:
export default class TodoFilter extends React.Component {
    render () {
        return (
            <div>
                <TodoFilterItem {...this.props} name="all" />{' / '}
                <TodoFilterItem {...this.props} name="active" />{' / '}
                <TodoFilterItem {...this.props} name="completed" />
            </div>
        )
    }
}
  1. src/TodoFilter.js中,在TodoFilterItem类中,我们首先定义一个用于设置过滤器的处理方法:
    handleFilter () {
        const { name, filterTodos } = this.props
        filterTodos(name)
    }
  1. 然后,我们从TodoFilter中获取filter prop:
    render () {
        const { name, filter = 'all' } = this.props
  1. 接下来,我们使用filter prop 来以bold显示当前选定的过滤器:
        const style = {
            color: 'blue',
            cursor: 'pointer',
            fontWeight: (filter === name) ? 'bold' : 'normal'
        }
  1. 然后,我们通过onClick将处理方法绑定到过滤器项:
        return <span style={style} onClick={this.handleFilter}>{name}</span>
    }
  1. 最后,我们为TodoFilterItem类创建一个新的constructor,并重新绑定处理方法的this上下文:
class TodoFilterItem extends React.Component {
 constructor (props) {
 super(props)

 this.handleFilter = this.handleFilter.bind(this)
 }

现在,我们的TodoFilter组件触发handleFilter方法以更改过滤器。现在我们的整个应用程序是动态的,我们可以使用其所有功能。

示例代码

示例代码可以在Chapter11/chapter11_1文件夹中找到。

只需运行npm install来安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

从 React 类组件迁移

在使用 React 类组件设置示例项目之后,我们现在将把这个项目迁移到 React Hooks。我们将展示如何迁移副作用,比如在组件挂载时获取待办事项,以及我们用于输入的状态管理。

在本节中,我们将迁移以下组件:

  • TodoItem

  • TodoList

  • TodoFilterItem

  • TodoFilter

  • AddTodo

  • App

迁移 TodoItem 组件

最简单的组件之一要迁移的是TodoItem组件。它不使用任何状态或副作用,因此我们可以简单地将其转换为函数组件。

让我们开始迁移TodoItem组件:

  1. 编辑src/TodoItem.js并删除类组件代码。现在我们将定义一个函数组件。

  2. 我们首先定义一个函数,它接受五个 props——title值、completed布尔值、id值、toggleTodo函数和removeTodo函数:

export default function TodoItem ({ title, completed, id, toggleTodo, removeTodo }) {
  1. 接下来,我们定义我们的两个处理函数:
    function handleToggle () {
        toggleTodo(id)
    }

    function handleRemove () {
        removeTodo(id)
    }
  1. 最后,我们返回 JSX 代码以渲染我们的组件:
    return (
        <div style={{ width: 400, height: 25 }}>
            <input type="checkbox" checked={completed} onChange={handleToggle} />
            {title}
            <button style={{ float: 'right' }} onClick={handleRemove}>x</button>
        </div>
    )
}

尝试保持函数组件的规模较小,并通过创建包装它们的新函数组件来组合它们。拥有许多小组件而不是一个大组件总是一个好主意。它们更容易维护、重用和重构。

正如我们所看到的,函数组件不需要我们重新绑定this,或者根本不需要定义构造函数。此外,我们不需要多次从this.props中解构。我们可以简单地在函数的头部定义所有 props。

迁移 TodoList 组件

接下来,我们要迁移包裹TodoItem组件的TodoList组件。在这里,我们使用了一个上下文,这意味着我们现在可以使用上下文 Hook。

现在让我们迁移TodoList组件:

  1. 编辑src/TodoList.js并从 React 中导入useContext Hook:
import React, { useContext } from 'react'
  1. 移除类组件代码。我们现在要定义一个函数组件。

  2. 我们首先定义函数的头部。在这种情况下,我们不解构 props,而是简单地将它们存储在一个props对象中:

export default function TodoList (props) {
  1. 现在我们定义 Context Hook:
    const items = useContext(StateContext)
  1. 最后,我们返回渲染的items列表,使用解构将itemprops对象传递给它:
    return items.map(item =>
        <TodoItem {...item} {...props} key={item.id} />
    )
}

我们最后定义key属性,以避免在解构itemprops对象时覆盖它。

正如我们所看到的,使用 Hooks 与上下文更加直接。我们可以简单地调用一个函数,并使用返回值。当使用多个上下文时,不需要魔术赋值this.context或包装地狱!

此外,我们可以逐步将组件迁移到 React Hooks,并且我们的应用仍然可以工作。没有必要一次性将所有组件迁移到 Hooks。React 类组件可以很好地与使用 Hooks 的函数组件一起工作。唯一的限制是我们不能在类组件中使用 Hooks。因此,我们需要一次迁移一个完整的组件。

迁移 TodoFilter 组件

接下来是TodoFilter组件,它不会使用任何 Hooks。然而,我们将用两个函数组件替换TodoFilterItemTodoFilter组件:一个用于TodoFilterItem,一个用于TodoFilter组件。

迁移 TodoFilterItem

首先,我们要迁移TodoFilterItem组件。现在让我们开始迁移组件:

  1. 编辑src/TodoFilter.js并移除类组件代码。我们现在要定义一个函数组件。

  2. TodoFilterItem组件定义一个函数,它将接受三个 props——name值,filterTodos函数和filter值:

function TodoFilterItem ({ name, filterTodos, filter = 'all' }) {
  1. 在这个函数中,我们定义了一个处理器函数来改变过滤器:
    function handleFilter () {
        filterTodos(name)
    }
  1. 接下来,我们为我们的span元素定义一个style对象:
    const style = {
        color: 'blue',
        cursor: 'pointer',
        fontWeight: (filter === name) ? 'bold' : 'normal'
    }
  1. 最后,我们返回并渲染span元素:
    return <span style={style} onClick={handleFilter}>{name}</span>
}

正如我们所看到的,函数组件比相应的类组件需要更少的样板代码。

迁移 TodoFilter

现在我们已经迁移了TodoFilterItem组件,我们可以迁移TodoFilter组件。让我们现在迁移它:

  1. 编辑src/TodoFilter.js并删除类组件代码。我们现在要定义一个函数组件。

  2. TodoFilter组件定义一个函数。我们这里不会在 props 上使用解构:

export default function TodoFilter (props) {
  1. 在这个组件中,我们只返回和渲染三个TodoFilterItem组件 - 将 props 传递给它们:
    return (
        <div>
            <TodoFilterItem {...props} name="all" />{' / '}
            <TodoFilterItem {...props} name="active" />{' / '}
            <TodoFilterItem {...props} name="completed" />
        </div>
    )
}

现在,我们的TodoFilter组件已成功迁移。

迁移 AddTodo 组件

接下来,我们将迁移AddTodo组件。在这里,我们将使用 State Hook 来处理input字段状态。

让我们现在迁移AddTodo组件:

  1. 编辑src/AddTodo.js并调整导入语句以从 React 导入useState Hook:
import React, { useState } from 'react'
  1. 删除类组件代码。我们现在要定义一个函数组件。

  2. 首先,我们定义一个函数,它只接受一个 prop - addTodo函数:

export default function AddTodo ({ addTodo }) {
  1. 接下来,我们为input字段状态定义一个 State Hook:
    const [ input, setInput ] = useState('')
  1. 现在,我们可以为input字段和添加按钮定义处理函数:
    function handleInput (e) {
        setInput(e.target.value)
    }

    function handleAdd () {
        if (input) {
            addTodo(input)
            setInput('')
        }
    }
  1. 最后,我们返回并渲染input字段和添加按钮:
    return (
        <form onSubmit={e => { e.preventDefault(); handleAdd() }}>
            <input
                type="text"
                placeholder="enter new task..."
                style={{ width: 350, height: 15 }}
                value={input}
                onChange={handleInput}
            />
            <input
                type="submit"
                style={{ float: 'right', marginTop: 2 }}
                disabled={!input}
                value="add"
            />
        </form>
    )
}

正如我们所看到的,使用 State Hook 使状态管理变得更加简单。我们可以为每个状态值定义一个单独的值和 setter 函数,而不是不断处理一个状态对象。此外,我们不需要一直从this.state中解构。因此,我们的代码更加清晰简洁。

迁移 App 组件

最后,剩下的就是迁移App组件。然后,我们整个待办事项应用程序将被迁移到 React Hooks。在这里,我们将使用 Reducer Hook 来管理状态,Effect Hook 在组件挂载时获取待办事项,以及 Memo Hook 来存储过滤后的待办事项列表。

在本节中,我们将做以下事情:

  • 定义动作

  • 定义 reducers

  • 迁移App组件

定义动作

我们的应用将接受五个动作:

  • FETCH_TODOS:获取新的待办事项列表 - { type: 'FETCH_TODOS', todos: [] }

  • ADD_TODO:插入新的待办事项 - { type: 'ADD_TODO', title: 'Test ToDo app' }

  • TOGGLE_TODO:切换待办事项的completed值 - { type: 'TOGGLE_TODO', id: 'xxx' }

  • REMOVE_TODO:移除一个待办事项—{ type: 'REMOVE_TODO', id: 'xxx' }

  • FILTER_TODOS:过滤待办事项—{ type: 'FILTER_TODOS', filter: 'completed' }

在定义完动作之后,我们可以继续定义 reducers。

定义 reducers

现在我们要为我们的状态定义 reducers。我们需要一个 app reducer 和两个子 reducer:一个用于 todos,一个用于 filter。

过滤后的待办事项列表将由App组件动态计算。我们可以稍后使用 Memo Hook 来缓存结果,避免不必要地重新计算过滤后的待办事项列表。

定义 filter reducer

我们将首先定义filter值的 reducer。现在让我们定义 filter reducer:

  1. 创建一个新的src/reducers.js文件,并从src/api.js文件中导入generateID函数:
import { generateID } from './api'
  1. src/reducers.js文件中,定义一个新函数,它将处理FILTER_TODOS动作,并相应地设置值:
function filterReducer (state, action) {
    if (action.type === 'FILTER_TODOS') {
        return action.filter
    } else {
        return state
    }
}

现在,filterReducer函数已经定义,我们可以正确处理FILTER_TODOS动作了。

定义 todos reducer

接下来,我们将为待办事项定义一个函数。在这里,我们将处理FETCH_TODOSADD_TODOTOGGLE_TODOREMOVE_TODO动作。

现在让我们定义todosReducer函数:

  1. src/reducers.js文件中,定义一个新函数,它将处理这些动作:
function todosReducer (state, action) {
    switch (action.type) {
  1. 对于FETCH_TODOS动作,我们只需用新的todos数组替换当前状态:
        case 'FETCH_TODOS':
            return action.todos
  1. 对于ADD_TODO动作,我们将在当前状态数组的开头插入一个新项目:
        case 'ADD_TODO':
            const newTodo = {
                id: generateID(),
                title: action.title,
                completed: false
            }
            return [ newTodo, ...state ]
  1. 对于TOGGLE_TODO动作,我们将使用map函数来更新单个待办事项:
        case 'TOGGLE_TODO':
            return state.map(t => {
                if (t.id === action.id) {
                    return { ...t, completed: !t.completed }
                }
                return t
            }, [])
  1. 对于REMOVE_TODO动作,我们将使用filter函数来移除单个待办事项:
        case 'REMOVE_TODO':
            return state.filter(t => {
                if (t.id === action.id) {
                    return false
                }
                return true
            })
  1. 默认情况下(对于所有其他动作),我们只需返回当前的state
        default:
            return state
    }
}

现在,todos reducer 已经定义,我们可以处理FETCH_TODOSADD_TODOTOGGLE_TODOREMOVE_TODO动作了。

定义 app reducer

最后,我们需要将其他 reducers 组合成一个单一的 app 状态的 reducer。现在让我们定义appReducer函数:

  1. src/reducers.js文件中,为appReducer定义一个新函数:
export default function appReducer (state, action) {
  1. 在这个函数中,我们返回一个包含其他 reducers 值的对象。我们只需将子状态和动作传递给其他 reducers:
    return {
        todos: todosReducer(state.todos, action),
        filter: filterReducer(state.filter, action)
    }
}

现在,我们的 reducers 已经分组在一起。所以,我们只有一个state对象和一个dispatch函数。

组件迁移

现在我们已经定义了我们的 reducers,我们可以开始迁移App组件。让我们现在迁移它:

  1. 编辑src/App.js并调整导入语句,从React中导入useReduceruseEffectuseMemo
import React, { useReducer, useEffect, useMemo } from 'react'
  1. src/reducers.js中导入appReducer函数:
import appReducer from './reducers'
  1. 删除类组件代码。现在我们要定义一个函数组件。

  2. 首先,我们定义一个不接受任何 props 的函数:

export default function App () {
  1. 现在,我们使用appReducer函数定义一个 Reducer Hook:
    const [ state, dispatch ] = useReducer(appReducer, { todos: [], filter: 'all' })
  1. 接下来,我们定义一个 Effect Hook,它将通过 API 函数获取todos,然后将会派发一个FETCH_TODOS动作:
    useEffect(() => {
        fetchAPITodos().then((todos) =>
            dispatch({ type: 'FETCH_TODOS', todos })
        )
    }, [])
  1. 然后,我们使用 Memo Hook 实现过滤机制,以优化性能并避免在没有变化时重新计算过滤后的 todos 列表:
    const filteredTodos = useMemo(() => {
        const { filter, todos } = state
        switch (filter) {
            case 'active':
                return todos.filter(t => t.completed === false)

            case 'completed':
                return todos.filter(t => t.completed === true)

            default:
            case 'all':
                return todos
        }
    }, [ state ])
  1. 现在,我们定义各种函数,这些函数将派发动作并改变状态:
    function addTodo (title) {
        dispatch({ type: 'ADD_TODO', title })
    }

    function toggleTodo (id) {
        dispatch({ type: 'TOGGLE_TODO', id })
    }

    function removeTodo (id) {
        dispatch({ type: 'REMOVE_TODO', id })
    }

    function filterTodos (filter) {
        dispatch({ type: 'FILTER_TODOS', filter })
    }
  1. 最后,我们返回并渲染所有需要的 ToDo 应用程序组件:
    return (
        <StateContext.Provider value={filteredTodos}>
            <div style={{ width: 400 }}>
                <Header />
                <AddTodo addTodo={addTodo} />
                <hr />
                <TodoList toggleTodo={toggleTodo} removeTodo={removeTodo} />
                <hr />
                <TodoFilter filter={state.filter} filterTodos={filterTodos} />
            </div>
        </StateContext.Provider>
    )
}

正如我们所看到的,使用 reducer 来处理复杂的状态变化使我们的代码更加简洁和易于维护。我们的应用现在完全迁移到了 Hooks!

示例代码

示例代码可以在Chapter11/chapter11_2文件夹中找到。

只需运行npm install来安装所有依赖项,并运行npm start来启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

类组件的权衡

现在我们已经完成了从类组件到 Hooks 的迁移,让我们回顾和总结我们学到的东西。

通过计算代码行数,我们可以看到,总共有 392 行 JavaScript 代码的函数组件与 Hooks 比起需要 430 行 JavaScript 代码的类组件更加简洁。此外,函数组件与 Hooks 更容易理解和测试,因为它们只是使用 JavaScript 函数而不是复杂的 React 构造。此外,我们能够将所有的状态改变逻辑重构到一个单独的reducers.js文件中,从而将其与App组件解耦,并使其更容易重构和测试。这将App.js文件的大小从 109 行减少到 64 行,并在reducers.js文件中增加了 50 行。

我们可以在下表中看到减少的代码行数:

比较:JavaScript 代码行数
类组件使用 Hooks 的函数组件

| 36  ./TodoFilter.js 15  ./TodoList.js

59  ./AddTodo.js

12  ./index.js

7   ./Header.js

5   ./StateContext.js

9   ./App.test.js

135 ./serviceWorker.js

12  ./api.js

109 ./App.js

31  ./TodoItem.js | 25  ./TodoFilter.js 12  ./TodoList.js

42  ./AddTodo.js

12  ./index.js

7   ./Header.js

50  ./reducers.js

5   ./StateContext.js

9   ./App.test.js

135 ./serviceWorker.js

12  ./api.js

64  ./App.js

19  ./TodoItem.js |

430 总计392 总计

使用函数组件和 Hooks,以下几点不需要考虑:

  • 不需要处理构造函数

  • 没有混淆的 this 上下文(this 重新绑定)

  • 不需要一遍又一遍地解构相同的值

  • 在处理上下文、props 和状态时没有魔法

  • 如果我们想在 props 改变时重新获取数据,就不需要定义 componentDidMountcomponentDidUpdate

此外,函数组件具有以下优势:

  • 鼓励创建小而简单的组件

  • 更容易重构

  • 更容易测试

  • 需要更少的代码

  • 对初学者更容易理解

  • 更具声明性

然而,在以下情况下,类组件可能是可以的:

  • 遵循某些约定时。

  • 使用最新的 JavaScript 特性来避免 this 重新绑定。

  • 可能更容易理解,因为团队已经掌握了相关知识。

  • 许多项目仍在使用类。对于库来说,这不是什么问题,因为它们可以很好地与函数组件一起工作。在工作中,你可能需要使用类。

  • 根据 React 团队的说法,类不会很快从 React 中移除。

最后,这是一个偏好的问题,但是 Hooks 确实比类有很多优势!如果你正在开始一个新项目,一定要选择 Hooks。如果你正在处理一个现有项目,考虑是否有必要重构某些组件为基于 Hook 的组件,以使它们更简单。然而,你不应该立即将所有项目都转换为 Hooks,因为重构总是可能引入新的 bug。采用 Hooks 的最佳方式是在适当的时候,慢慢但确定地用基于 Hook 的函数组件替换旧的类组件。例如,如果你已经在重构一个组件,你可以将其重构为使用 Hooks!

总结

在本章中,我们首先使用 React 类组件构建了一个 ToDo 应用程序。我们首先设计了应用程序结构,然后实现了静态组件,最后使它们变得动态。在接下来的部分,我们学习了如何将使用类组件的现有项目迁移到使用 Hooks 的函数组件。最后,我们学习了类组件的权衡,何时应该使用类组件或 Hooks,以及如何迁移现有项目到 Hooks。

我们现在已经实际看到了 React 类组件与使用 Hooks 的函数组件的不同之处。Hooks 使我们的代码更加简洁,更易于阅读和维护。我们还学到了应该逐步将我们的组件从类组件迁移到使用 Hooks 的函数组件——没有必要立即迁移整个应用程序。

在下一章中,我们将学习如何使用 Redux 处理状态,使用 Redux 与仅使用 Hooks 的函数组件相比的权衡,如何在 Hooks 中使用 Redux,以及如何将现有的 Redux 应用程序迁移到基于 Hook 的设置。

问题

为了总结本章学到的知识,请尝试回答以下问题:

  1. React 类组件是如何定义的?

  2. 在使用类组件的constructor时,我们需要调用什么?为什么?

  3. 我们如何在类组件中设置初始状态?

  4. 我们如何在类组件中改变状态?

  5. 为什么我们需要重新绑定类组件方法的this上下文?

  6. 我们如何重新绑定this上下文?

  7. 我们如何在类组件中使用 React 上下文?

  8. 在迁移到 Hooks 时,我们可以用什么替代状态管理?

  9. 使用 Hooks 与类组件相比有什么权衡之处?

  10. 何时以及如何迁移现有项目到 Hooks?

进一步阅读

如果您对本章学到的概念更多信息感兴趣,请查看以下阅读材料:

第十二章:Redux 和 Hooks

在上一章中,我们学习了关于 React 类组件,以及如何从现有的基于类组件的项目迁移到基于 Hook 的项目。然后,我们了解了两种解决方案之间的权衡,并讨论了现有项目应该何时以及如何迁移。

在本章中,我们将把上一章中创建的 ToDo 应用程序转换为 Redux 应用程序。首先,我们将学习 Redux 是什么,包括 Redux 的三个原则。我们还将学习在应用程序中何时使用 Redux 是有意义的,以及它并不适用于每个应用程序。此外,我们将学习如何使用 Redux 处理状态。之后,我们将学习如何在 Hooks 中使用 Redux,以及如何将现有的 Redux 应用程序迁移到 Hooks。最后,我们将学习 Redux 的权衡,以便能够决定哪种解决方案对于特定用例最合适。通过本章结束时,您将完全了解如何使用 Hooks 编写 Redux 应用程序。

本章将涵盖以下主题:

  • Redux 是什么,以及何时以及为什么应该使用它

  • 使用 Redux 处理状态

  • 使用 Hooks 与 Redux

  • 迁移 Redux 应用程序

  • 学习 Redux 的权衡

技术要求

应该已经安装了相当新的 Node.js 版本(v11.12.0 或更高)。还需要安装 Node.js 的npm包管理器。

本章的代码可以在 GitHub 存储库上找到:github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter12

查看以下视频以查看代码运行情况:

bit.ly/2Mm9yoC

请注意,强烈建议您自己编写代码。不要简单地运行提供的代码示例。重要的是您自己编写代码,以便您能够正确学习和理解。但是,如果遇到任何问题,您可以随时参考代码示例。

现在,让我们开始本章。

Redux 是什么?

正如我们之前学到的,应用程序中有两种状态:

  • 本地 状态:例如,处理输入字段数据

  • 全局 状态:例如,存储当前登录的用户

在本书中,我们使用 State Hook 处理本地状态,使用 Reducer Hook 处理更复杂的状态(通常是全局状态)。

Redux 是一种可以用来处理 React 应用程序中各种状态的解决方案。它提供了一个包含所有应用程序状态的单个状态树对象。这与我们在博客应用程序中使用的 Reducer Hook 类似。传统上,Redux 也经常用于存储本地状态,这使得状态树非常复杂。

Redux 本质上由五个元素组成:

  • 存储:包含状态的对象,描述了我们应用程序的完整状态—{ todos: [], filter: 'all' }

  • 动作:描述状态修改的对象—{ type: 'FILTER_TODOS', filter: 'completed' }

  • 动作创建者:创建动作对象的函数—(filter) => ({ type: 'FILTER_TODOS', filter })

  • 减速器:接受当前 state 值和一个 action 对象,并返回一个新状态的函数—(state, action) => { ... }

  • 连接器:将现有组件连接到 Redux 的高阶组件,通过将 Redux 状态和动作创建者注入为 props—connect(mapStateToProps, mapDispatchToProps)(Component)

在 Redux 生命周期中,存储 包含定义 UI 的状态。UI 通过 连接器 连接到 Redux 存储。用户与 UI 的交互触发 动作,然后发送到 减速器减速器 然后更新 存储 中的状态。

我们可以在下图中看到 Redux 生命周期的可视化:

Redux 生命周期的可视化

正如你所看到的,我们已经了解了这些组件中的三个:store(状态树)、动作和减速器。Redux 就像是 Reducer Hook 的更高级版本。不同之处在于,使用 Redux,我们总是将状态分派给单个减速器,因此只改变一个状态。Redux 不应该有多个实例。通过这种限制,我们可以确保整个应用程序状态都包含在一个对象中,这使我们能够仅从 Redux 存储中重建整个应用程序状态。

由于只有一个包含所有状态的存储,我们可以通过在崩溃报告中保存 Redux 存储,或者在调试过程中自动重放某些操作来轻松调试错误的状态,这样我们就不需要手动输入文本和点击按钮,一遍又一遍。此外,Redux 提供了简化我们处理异步请求的中间件,例如从服务器获取数据。现在我们了解了 Redux 是什么,在下一节中,我们将学习 Redux 的三个基本原则。

Redux 的三个原则

Redux 的 API 非常小,实际上只包含少数几个函数。Redux 如此强大的原因在于在使用该库时应用于代码的一套规则。这些规则允许编写可扩展、易于扩展、测试和调试的应用程序。

Redux 基于三个基本原则:

  • 真相的单一来源

  • 只读状态

  • 状态更改通过纯函数处理

真相的单一来源

这个 Redux 原则指出数据应该始终有一个单一的真相来源。这意味着全局数据来自单一的 Redux 存储,本地数据来自,例如,某个 State Hook。每种数据只有一个来源。因此,应用程序变得更容易调试,更不容易出错。

只读状态

使用 Redux,不可能直接修改应用程序状态。只能通过分派动作来改变状态。这个原则使状态变化可预测:如果没有动作发生,应用程序状态将不会改变。此外,动作是逐个处理的,因此我们不必处理竞争条件。最后,动作是纯粹的 JavaScript 对象,这使它们易于序列化、记录、存储或重放。因此,调试和测试 Redux 应用程序变得非常容易。

状态更改通过纯函数处理

纯函数是指,给定相同的输入,将始终返回相同的输出。Redux 中的 Reducer 函数是纯的,因此,给定相同的状态和动作,它们将始终返回相同的新状态。

例如,以下的 reducer 是一个不纯的函数,因为多次使用相同的输入调用该函数会产生不同的输出:

let i = 0
function counterReducer (state, action) {
    if (action.type === 'INCREMENT') {
        i++
    }
    return i
}

console.log(counterReducer(0, { type: 'INCREMENT' })) // prints 1
console.log(counterReducer(0, { type: 'INCREMENT' })) // prints 2

要将这个 reducer 转变为纯函数,我们必须确保它不依赖于外部状态,只使用其参数进行计算:

function counterReducer (state, action) {
    if (action.type === 'INCREMENT') {
        return state + 1
    }
    return state
}

console.log(counterReducer(0, { type: 'INCREMENT' })) // prints 1
console.log(counterReducer(0, { type: 'INCREMENT' })) // prints 1

使用纯函数进行减速器使它们可预测,易于测试和调试。使用 Redux 时,我们需要小心始终返回新状态,而不是修改现有状态。因此,例如,我们不能在数组状态上使用Array.push(),因为它会修改现有数组;我们必须使用Array.concat()来创建一个新数组。对于对象也是一样,我们必须使用 rest/spread 语法来创建新对象,而不是修改现有对象。例如,{ ...state, completed: true }

现在我们已经了解了 Redux 的三个基本原则,我们可以开始在我们的 ToDo 应用程序中实践使用 Redux 来实现状态处理。

使用 Redux 处理状态

使用 Redux 进行状态管理实际上与使用 Reducer Hook 非常相似。我们首先定义状态对象,然后操作,最后是我们的减速器。Redux 中的另一个模式是创建返回操作对象的函数,即所谓的操作创建者。此外,我们需要用Provider组件包装整个应用程序,并连接组件到 Redux 存储,以便能够使用 Redux 状态和操作创建者。

安装 Redux

首先,我们必须安装 Redux,React Redux 和 Redux Thunk。让我们分别看看每个库的作用:

  • Redux 本身只处理 JavaScript 对象,因此它提供存储,处理操作和操作创建者,并处理减速器。

  • React Redux 提供连接器,以便将 Redux 连接到我们的 React 组件。

  • Redux Thunk 是一个中间件,允许我们在 Redux 中处理异步请求。

React中与Redux结合使用Redux可以将全局状态管理转移到Redux,而React处理应用程序的渲染和本地状态:

React 和 Redux 如何一起工作的示例

要安装 Redux 和 React Redux,我们将使用npm。执行以下命令:

> npm install --save redux react-redux redux-thunk

现在所有必需的库都已安装好,我们可以开始设置我们的 Redux 存储。

定义状态、操作和减速器

开发 Redux 应用程序的第一步是定义状态,然后是将改变状态的操作,最后是执行状态修改的减速器函数。在我们的 ToDo 应用程序中,我们已经定义了状态,操作和减速器,以便使用 Reducer Hook。在这里,我们只是简单地回顾了我们在上一章中定义的内容。

状态

我们 ToDo 应用程序的完整状态对象由两个键组成:一个 todo 项目数组和一个字符串,用于指定当前选择的filter值。初始状态如下:

{
    "todos": [
        { "id": 1, "title": "Write React Hooks book", "completed": true },
        { "id": 2, "title": "Promote book", "completed": false }
    ],
    "filter": "all"
}

正如我们所看到的,在 Redux 中,状态对象包含了对我们应用程序重要的所有状态。在这种情况下,应用程序状态由一个todos数组和一个filter字符串组成。

动作

我们的应用程序接受以下五个动作:

  • FETCH_TODOS:获取新的 todo 项目列表——{ type: 'FETCH_TODOS', todos: [] }

  • ADD_TODO:插入新的 todo 项目——{ type: 'ADD_TODO', title: 'Test ToDo app' }

  • TOGGLE_TODO:切换 todo 项目的completed值——{ type: 'TOGGLE_TODO', id: 'xxx' }

  • REMOVE_TODO:移除 todo 项目——{ type: 'REMOVE_TODO', id: 'xxx' }

  • FILTER_TODOS:过滤 todo 项目——{ type: 'FILTER_TODOS', filter: 'completed' }

Reducers

我们定义了三个 reducer——分别用于我们状态的每个部分,并且定义了一个 app reducer 来合并其他两个 reducer。filter reducer 等待FILTER_TODOS动作,然后相应地设置新的过滤器。todos reducer 监听其他与 todo 相关的动作,并通过添加、删除或修改元素来调整 todos 数组。然后 app reducer 合并这两个 reducer,并将动作传递给它们。在定义了创建 Redux 应用所需的所有元素之后,我们现在可以设置 Redux 存储。

设置 Redux 存储

为了最初保持简单,并展示 Redux 的工作原理,我们现在不会使用连接器。我们只是简单地用 Redux 替换了之前由 Reducer Hook 提供的state对象和dispatch函数。

现在让我们设置 Redux 存储:

  1. 编辑src/App.js,并从 Redux 库中导入useState Hook 和createStore函数:
import React, { useState, useEffect, useMemo } from 'react'
import { createStore } from 'redux' 
  1. 在导入语句之后,并在App函数定义之前,我们将初始化 Redux 存储。我们首先定义初始状态:
const initialState = { todos: [], filter: 'all' }
  1. 接下来,我们将使用createStore函数来定义 Redux 存储,通过使用现有的appReducer函数并传递initialState对象:
const store = createStore(appReducer, initialState)

请注意,在 Redux 中,通过将其传递给createStore来初始化状态并不是最佳实践。然而,在 Reducer Hook 中,我们需要以这种方式进行。在 Redux 中,我们通常通过在 reducer 函数中设置默认值来初始化状态。我们将在本章后面学习更多关于通过 Redux reducer 初始化状态的内容。

  1. 现在,我们可以从存储中获取dispatch函数:
const { dispatch } = store
  1. 下一步是在App函数中删除以下 Reducer Hook 定义:
    const [ state, dispatch ] = useReducer(appReducer, { todos: [], filter: 'all' })

它被一个简单的 State Hook 替换,它将存储我们的 Redux 状态:

    const [ state, setState ] = useState(initialState)
  1. 最后,我们定义一个 Effect Hook,以便将 State Hook 与 Redux 存储状态同步:
    useEffect(() => {
        const unsubscribe = store.subscribe(() => setState(store.getState()))
        return unsubscribe
    }, [])

正如我们所看到的,应用程序仍然以与以前完全相同的方式运行。Redux 的工作方式与 Reducer Hook 非常相似,但具有更多的功能。然而,在如何定义动作和减速器方面有轻微的差异,我们将在接下来的章节中学习。

示例代码

示例代码可以在Chapter12/chapter12_1文件夹中找到。

只需运行npm install以安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

定义动作类型

创建完整的 Redux 应用程序的第一步是定义所谓的动作类型。它们将用于在动作创建者中创建动作,并在减速器中处理动作。这里的想法是避免在定义或比较动作的type属性时出现拼写错误。

现在让我们定义动作类型:

  1. 创建一个新的src/actionTypes.js文件。

  2. 在新创建的文件中定义并导出以下常量:

export const FETCH_TODOS = 'FETCH_TODOS'
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const REMOVE_TODO = 'REMOVE_TODO'
export const FILTER_TODOS = 'FILTER_TODOS'

现在我们已经定义了我们的动作类型,我们可以开始在动作创建者和减速器中使用它们。

定义动作创建者

在定义动作类型之后,我们需要定义动作本身。这样做,我们将定义返回动作对象的函数。这些函数被称为动作创建者,有两种类型:

  • 同步 动作创建者:这些只是返回一个动作对象

  • 异步 动作创建者:这些返回一个async函数,稍后将调度一个动作

我们将首先定义同步动作创建者,然后我们将学习如何定义异步动作创建者。

定义同步动作创建者

我们已经在src/App.js中早些时候定义了动作创建者函数。现在我们可以从我们的App组件中复制它们,确保我们调整type属性以使用动作类型常量,而不是静态字符串。

现在让我们定义同步动作创建者:

  1. 创建一个新的src/actions.js文件。

  2. 导入我们将需要创建动作的所有动作类型:

import {
    ADD_TODO, TOGGLE_TODO, REMOVE_TODO, FILTER_TODOS
} from './actionTypes'
  1. 现在,我们可以定义并导出我们的动作创建者函数:
export function addTodo (title) {
    return { type: ADD_TODO, title }
}

export function toggleTodo (id) {
    return { type: TOGGLE_TODO, id }
}

export function removeTodo (id) {
    return { type: REMOVE_TODO, id }
}

export function filterTodos (filter) {
    return { type: FILTER_TODOS, filter }
}

正如我们所看到的,同步动作创建者只是创建并返回动作对象。

定义异步动作创建者

下一步是为fetchTodos动作定义一个异步动作创建者。在这里,我们将使用async/await结构。

现在我们将使用async函数来定义fetchTodos动作创建者:

  1. src/actions.js中,首先导入FETCH_TODOS动作类型和fetchAPITodos函数:
import {
    FETCH_TODOS, ADD_TODO, TOGGLE_TODO, REMOVE_TODO, FILTER_TODOS
} from './actionTypes'
import { fetchAPITodos } from './api'
  1. 然后,定义一个新的动作创建者函数,该函数将返回一个获取dispatch函数作为参数的async函数:
export function fetchTodos () {
    return async (dispatch) => {
  1. 在这个async函数中,我们现在将调用 API 函数,并dispatch我们的动作:
        const todos = await fetchAPITodos()
        dispatch({ type: FETCH_TODOS, todos })
    }
}

正如我们所看到的,异步动作创建者返回一个函数,该函数将在以后的时间调度动作。

调整存储

为了我们能够在 Redux 中使用异步动作创建者函数,我们需要加载redux-thunk中间件。此中间件检查动作创建者是否返回一个函数,而不是一个普通对象,如果是这种情况,它会执行该函数,并将dispatch函数作为参数传递给它。

现在让我们调整存储以允许异步动作创建者:

  1. 创建一个新的src/configureStore.js文件。

  2. 从 Redux 中导入createStoreapplyMiddleware函数:

import { createStore, applyMiddleware } from 'redux'
  1. 接下来,导入thunk中间件和appReducer函数:
import thunk from 'redux-thunk'

import appReducer from './reducers'
  1. 现在,我们可以定义存储并将thunk中间件应用于它:
const store = createStore(appReducer, applyMiddleware(thunk))
  1. 最后,我们导出store
export default store

使用redux-thunk中间件,我们现在可以调度稍后调度动作的函数,这意味着我们的异步动作创建者现在将正常工作。

调整 reducers

如前所述,Redux reducers 与 Reducer Hooks 不同,它们具有某些约定:

  • 每个 reducer 需要通过在函数定义中定义默认值来设置其初始状态

  • 每个 reducer 需要返回未处理动作的当前状态

现在我们将调整现有的 reducers,使它们遵循这些约定。第二个约定已经实现了,因为我们之前定义了一个单一的应用程序 reducer,以避免有多个 dispatch 函数。

在 Redux reducers 中设置初始状态

因此,我们将专注于第一个约定-通过在函数参数中定义默认值来设置初始状态,如下所示:

  1. 编辑src/reducers.js并从 Redux 中导入combineReducers函数:
import { combineReducers } from 'redux'
  1. 然后,将filterReducer重命名为filter,并设置默认值:
function filter (state = 'all', action) {
  1. 接下来,在todosReducer中进行相同的编辑和重复相同的过程:
function todos (state = [], action) {
  1. 最后,我们将使用combineReducers函数来创建我们的appReducer函数。现在我们可以这样做,而不是手动创建函数:
const appReducer = combineReducers({ todos, filter })
export default appReducer

正如我们所看到的,Redux reducers 非常类似于 Reducer Hooks。Redux 甚至提供了一个函数,允许我们将多个 reducer 函数组合成一个单一的应用 reducer!

连接组件

现在是时候介绍连接器和容器组件了。在 Redux 中,我们可以使用connect高阶组件将现有的组件连接到 Redux,通过将状态和动作创建者作为 props 注入到它们中。

Redux 定义了两种不同类型的组件:

  • 表示性 组件:就像我们一直定义的 React 组件

  • 容器 组件:连接表示性组件到 Redux 的 React 组件

容器组件使用连接器将 Redux 连接到表示性组件。这个连接器接受两个函数:

  • mapStateToProps(state): 获取当前的 Redux 状态,并返回一个要传递给组件的 props 对象;用于将状态传递给组件

  • mapDispatchToProps(dispatch): 从 Redux 存储中获取dispatch函数,并返回一个要传递给组件的 props 对象;用于将动作创建者传递给组件

我们现在将为现有的表示性组件定义容器组件:

  1. 首先,为所有我们的表示性组件创建一个新的src/components/文件夹。

  2. 然后,我们将所有现有的组件文件复制到src/components/文件夹,并调整以下文件的导入语句:AddTodo.jsApp.jsHeader.jsTodoFilter.jsTodoItem.jsTodoList.js

连接 AddTodo 组件

我们现在将开始连接我们的组件到 Redux 存储。表示性组件可以保持与以前相同。我们只创建新的组件—容器组件—将表示性组件包装起来,并向它们传递特定的 props。

现在让我们连接AddTodo组件:

  1. 为所有我们的容器组件创建一个新的src/containers/文件夹。

  2. 创建一个新的src/containers/ConnectedAddTodo.js文件。

  3. 在这个文件中,我们从react-redux中导入connect函数,从redux中导入bindActionCreators函数:

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
  1. 接下来,我们导入addTodo动作创建者和AddTodo组件:
import { addTodo } from '../actions'
import AddTodo from '../components/AddTodo'
  1. 现在,我们将定义mapStateToProps函数。由于这个组件不涉及 Redux 中的任何状态,我们可以在这里简单地返回一个空对象:
function mapStateToProps (state) {
    return {}
}
  1. 然后,我们定义mapDispatchToProps函数。在这里,我们使用bindActionCreators将动作创建者与dispatch函数包装起来:
function mapDispatchToProps (dispatch) {
    return bindActionCreators({ addTodo }, dispatch)
}

这段代码本质上与手动包装动作创建者是一样的,如下所示:

function mapDispatchToProps (dispatch) {
    return {
        addTodo: (...args) => dispatch(addTodo(...args))
    }
}
  1. 最后,我们使用connect函数将AddTodo组件连接到 Redux:
export default connect(mapStateToProps, mapDispatchToProps)(AddTodo)

现在,我们的AddTodo组件成功连接到了 Redux 存储。

连接 TodoItem 组件

接下来,我们将连接TodoItem组件,以便在下一步中在TodoList组件中使用它。

现在让我们连接TodoItem组件:

  1. 创建一个新的src/containers/ConnectedTodoItem.js文件。

  2. 在这个文件中,我们从react-redux中导入connect函数,从redux中导入bindActionCreators函数:

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
  1. 接下来,我们导入toggleTodoremoveTodo动作创建者,以及TodoItem组件:
import { toggleTodo, removeTodo } from '../actions'
import TodoItem from '../components/TodoItem'
  1. 同样,我们只从mapStateToProps中返回一个空对象:
function mapStateToProps (state) {
    return {}
}
  1. 这一次,我们将两个动作创建者绑定到dispatch函数:
function mapDispatchToProps (dispatch) {
    return bindActionCreators({ toggleTodo, removeTodo }, dispatch)
}
  1. 最后,我们连接组件,并导出它:
export default connect(mapStateToProps, mapDispatchToProps)(TodoItem)

现在,我们的TodoItem组件成功连接到了 Redux 存储。

连接 TodoList 组件

连接TodoItem组件之后,我们现在可以在TodoList组件中使用ConnectedTodoItem组件。

现在让我们连接TodoList组件:

  1. 编辑src/components/TodoList.js,并调整导入语句如下:
import ConnectedTodoItem from '../containers/ConnectedTodoItem'
  1. 然后,将从函数返回的组件重命名为ConnectedTodoItem
    return filteredTodos.map(item =>
        <ConnectedTodoItem {...item} key={item.id} />
    )
  1. 现在,创建一个新的src/containers/ConnectedTodoList.js文件。

  2. 在这个文件中,我们只从react-redux中导入connect函数,因为这一次我们不打算绑定动作创建者:

import { connect } from 'react-redux'
  1. 接下来,我们导入TodoList组件:
import TodoList from '../components/TodoList'
  1. 现在,我们定义mapStateToProps函数。这一次,我们使用解构从state对象中获取todosfilter,然后返回它们:
function mapStateToProps (state) {
    const { filter, todos } = state
    return { filter, todos }
}
  1. 接下来,我们定义mapDispatchToProps函数,在这里我们只返回一个空对象,因为我们不打算将任何动作创建者传递给TodoList组件:
function mapDispatchToProps (dispatch) {
    return {}
}
  1. 最后,我们连接并导出连接的TodoList组件:
export default connect(mapStateToProps, mapDispatchToProps)(TodoList)

现在,我们的TodoList组件成功连接到了 Redux 存储。

调整 TodoList 组件

现在我们已经连接了TodoList组件,我们可以将App组件中的过滤逻辑移动到TodoList组件中,如下所示:

  1. src/components/TodoList.js中导入useMemo Hook:
import  React,  {  useMemo  }  from  'react'
  1. 编辑src/components/App.js,并删除以下代码:
    const filteredTodos = useMemo(() => {
        const { filter, todos } = state
        switch (filter) {
            case 'active':
                return todos.filter(t => t.completed === false)

            case 'completed':
                return todos.filter(t => t.completed === true)

            default:
            case 'all':
                return todos
        }
    }, [ state ])
  1. 现在,编辑src/components/TodoList.js,并在这里添加filteredTodos代码。请注意,我们从状态对象中删除了解构,因为组件已经以 props 接收了filtertodos值。我们还相应地调整了依赖数组:
    const filteredTodos = useMemo(() => {
        switch (filter) {
            case 'active':
                return todos.filter(t => t.completed === false)

            case 'completed':
                return todos.filter(t => t.completed === true)

            default:
            case 'all':
                return todos
        }
    }, [ filter, todos ])

现在,我们的过滤逻辑在TodoList组件中,而不是App组件中。让我们继续连接其余的组件。

连接TodoFilter组件

接下来是TodoFilter组件。在这里,我们将使用mapStateToPropsmapDispatchToProps

现在让我们连接TodoFilter组件:

  1. 创建一个新的src/containers/ConnectedTodoFilter.js文件。

  2. 在这个文件中,我们从react-redux中导入connect函数,从redux中导入bindActionCreators函数:

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
  1. 接下来,我们导入filterTodos动作创建者和TodoFilter组件:
import { filterTodos } from '../actions'
import TodoFilter from '../components/TodoFilter'
  1. 我们使用解构从我们的state对象中获取filter,然后返回它:
function mapStateToProps (state) {
    const { filter } = state
    return { filter }
}
  1. 接下来,我们绑定并返回filterTodos动作创建者:
function mapDispatchToProps (dispatch) {
    return bindActionCreators({ filterTodos }, dispatch)
}
  1. 最后,我们连接组件并导出它:
export default connect(mapStateToProps, mapDispatchToProps)(TodoFilter)

现在,我们的TodoFilter组件已成功连接到 Redux 存储。

连接 App 组件

现在唯一还需要连接的组件是App组件。在这里,我们将注入fetchTodos动作创建者,并更新组件以使用所有其他组件的连接版本。

现在让我们连接App组件:

  1. 编辑src/components/App.js,并调整以下导入语句:
import ConnectedAddTodo from '../containers/ConnectedAddTodo'
import ConnectedTodoList from '../containers/ConnectedTodoList'
import ConnectedTodoFilter from '../containers/ConnectedTodoFilter'
  1. 还要调整从函数返回的以下组件:
            return (
                <div style={{ width: 400 }}>
                    <Header />
                    <ConnectedAddTodo />
                    <hr />
                    <ConnectedTodoList />
                    <hr />
                    <ConnectedTodoFilter />
                </div>
            )
  1. 现在,我们可以创建连接的组件。创建一个新的src/containers/ConnectedApp.js文件。

  2. 在这个新创建的文件中,我们从react-redux中导入connect函数,从redux中导入bindActionCreators函数:

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
  1. 接下来,我们导入fetchTodos动作创建者和App组件:
import { fetchTodos } from '../actions'
import App from '../components/App'
  1. 我们已经在其他组件中处理了状态的各个部分,因此无需将任何状态注入我们的App组件中:
function mapStateToProps (state) {
    return {}
}
  1. 然后,我们绑定并返回fetchTodos动作创建者:
function mapDispatchToProps (dispatch) {
    return bindActionCreators({ fetchTodos }, dispatch)
}
  1. 最后,我们连接App组件并导出它:
export default connect(mapStateToProps, mapDispatchToProps)(App)

现在,我们的App组件已成功连接到 Redux 存储。

设置 Provider 组件

最后,我们必须设置一个Provider组件,它将为 Redux 存储提供上下文,这将被连接器使用。

现在让我们设置Provider组件:

  1. 编辑src/index.js,并从react-redux中导入Provider组件:
import { Provider } from 'react-redux'
  1. 现在,从containers文件夹中导入ConnectedApp组件,并导入由configureStore.js创建的 Redux 存储:
import ConnectedApp from './containers/ConnectedApp'
import store from './configureStore'
  1. 最后,通过将ConnectedApp组件与Provider组件包装起来,调整ReactDOM.render的第一个参数,如下所示:
ReactDOM.render(
 <Provider store={store}>
 <ConnectedApp />
 </Provider>,
    document.getElementById('root')
)

现在,我们的应用程序将以与以前相同的方式工作,但一切都连接到 Redux 存储!正如我们所看到的,Redux 需要比仅仅使用 React 更多的样板代码,但它带来了许多优势:

  • 更容易处理异步操作(使用redux-thunk中间件)

  • 集中的操作处理(无需在组件中定义操作创建者)

  • 用于绑定操作创建者和组合减速器的有用函数

  • 减少错误的可能性(例如,通过使用操作类型,我们可以确保没有拼写错误)

然而,也有以下缺点:

  • 需要大量的样板代码(操作类型、操作创建者和连接组件)

  • 在单独的文件中映射状态/操作创建者(而不是在需要它们的组件中)

第一点既是优点又是缺点;操作类型和操作创建者确实需要更多的样板代码,但它们也使得以后更容易更新与操作相关的代码。第二点,以及连接组件所需的样板代码,可以通过使用 Hooks 来连接我们的组件到 Redux 来解决。我们将在本章的下一节中使用 Redux 与 Hooks。

示例代码

示例代码可以在Chapter12/chapter12_2文件夹中找到。

只需运行npm install来安装所有依赖项,然后运行npm start来启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

使用 Hooks 与 Redux

将我们的待办事项应用程序转换为基于 Redux 的应用程序后,我们现在使用高阶组件而不是 Hooks 来访问 Redux 状态和动作创建者。这是开发 Redux 应用程序的传统方式。但是,在 Redux 的最新版本中,可以使用 Hooks 代替高阶组件!我们现在将用 Hooks 替换现有的连接器。

即使使用了 Hooks,Provider组件仍然需要,以便向其他组件提供 Redux 存储。当从connect()重构到 Hooks 时,存储和提供程序的定义可以保持不变。

最新版本的 React Redux 提供了各种 Hooks 作为connect()高阶组件的替代方案。使用这些 Hooks,您可以订阅 Redux 存储,并且无需包装组件即可分派动作。

使用 dispatch Hook

useDispatch Hook 返回 Redux 存储提供的dispatch函数的引用。它可以用于分派从动作创建者返回的动作。其 API 如下所示:

const dispatch = useDispatch()

现在,我们将使用 Dispatch Hook 来替换现有的容器组件。

您不需要一次性迁移整个 Redux 应用程序以使用 Hooks。可以有选择性地重构某些组件,这意味着它们将使用 Hooks,同时仍然使用connect()来处理其他组件。

学会如何使用 Dispatch Hook 后,让我们继续迁移现有组件,使它们使用 Dispatch Hook。

使用 Hooks 为 AddTodo 组件

现在我们已经了解了 Dispatch Hook,让我们通过在AddTodo组件中实现它来看看它的作用。

现在让我们将AddTodo组件迁移到 Hooks:

  1. 首先删除src/containers/ConnectedAddTodo.js文件。

  2. 现在,编辑src/components/AddTodo.js文件并从react-redux中导入useDispatch Hook:

import { useDispatch } from 'react-redux'
  1. 另外,导入addTodo动作创建者:
import { addTodo } from '../actions'
  1. 现在,我们可以从函数定义中删除 props:
export default function AddTodo () {
  1. 然后,定义 Dispatch Hook:
    const dispatch = useDispatch()
  1. 最后,调整处理程序函数并调用dispatch()
    function handleAdd () {
        if (input) {
            dispatch(addTodo(input))
            setInput('')
        }
    }
  1. 现在,唯一剩下的事情就是在src/components/App.js中用AddTodo组件替换ConnectedAddTodo组件。首先,调整导入语句:
import AddTodo from './AddTodo'
  1. 然后,调整渲染的组件:
    return (
        <div style={{ width: 400 }}>
            <Header />
            <AddTodo />

如您所见,我们的应用程序仍然以与以前相同的方式工作,但现在我们正在使用 Hooks 来连接组件到 Redux!

使用 Hooks 为 App 组件

接下来,我们将更新我们的App组件,以便直接调度fetchTodos动作。现在让我们将App组件迁移到 Hooks:

  1. 首先删除src/containers/ConnectedApp.js文件。

  2. 现在,编辑src/components/App.js文件,并从react-redux导入useDispatch Hook:

import { useDispatch } from 'react-redux'
  1. 另外,导入fetchTodos动作创建者:
import { fetchTodos } from '../actions'
  1. 现在,我们可以从函数定义中删除 props:
export default function App () {
  1. 然后,定义 Dispatch Hook:
    const dispatch = useDispatch()
  1. 最后,调整 Effect Hook 并调用dispatch()
    useEffect(() => {
        dispatch(fetchTodos())
    }, [ dispatch ])
  1. 现在,剩下的就是在src/index.js中用App组件替换ConnectedApp组件。首先,调整导入语句:
import App from './components/App'
  1. 然后,调整渲染的组件:
ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

正如我们所看到的,使用 Hooks 比定义一个单独的容器组件更简单和更简洁。

使用 Hooks 为 TodoItem 组件

现在,我们将升级TodoItem组件以使用 Hooks。让我们现在迁移它:

  1. 首先删除src/containers/ConnectedTodoItem.js文件。

  2. 现在,编辑src/components/TodoItem.js文件,并从react-redux导入useDispatch Hook:

import { useDispatch } from 'react-redux'
  1. 另外,导入toggleTodoremoveTodo动作创建者:
import { toggleTodo, removeTodo } from '../actions'
  1. 现在,我们可以从函数定义中删除与动作创建者相关的 props。新代码应如下所示:
export default function TodoItem ({ title, completed, id }) {
  1. 然后,定义 Dispatch Hook:
    const dispatch = useDispatch()
  1. 最后,调整处理函数以调用dispatch()
    function handleToggle () {
        dispatch(toggleTodo(id))
    }

    function handleRemove () {
        dispatch(removeTodo(id))
    }
  1. 现在,剩下的就是在src/components/TodoList.js中用TodoItem组件替换ConnectedTodoItem组件。首先,调整导入语句:
import TodoItem from './TodoItem'
  1. 然后,调整渲染的组件:
    return filteredTodos.map(item =>
        <TodoItem {...item} key={item.id} />
    )

现在,TodoItem组件使用 Hooks 而不是容器组件。接下来,我们将学习有关 Selector Hook 的内容。

使用 Selector Hook

Redux 提供的另一个非常重要的 Hook 是 Selector Hook。它允许我们通过定义选择器函数从 Redux 存储状态中获取数据。该 Hook 的 API 如下:

const result = useSelector(selectorFn, equalityFn)

selectorFn是一个类似于mapStateToProps函数的函数。它将完整的状态对象作为唯一参数。当组件渲染时,选择器函数将被执行,以及当动作被调度时(并且状态与先前状态不同)。

重要的是要注意,从一个选择器 Hook 返回多个状态部分的对象将在每次分派动作时强制重新渲染。如果需要请求存储中的多个值,我们可以这样做:

  • 使用多个 Selector Hooks,每个返回状态对象中的单个字段

  • 使用reselect或类似的库创建一个记忆化选择器(我们将在下一节中介绍)

  • 使用react-redux中的shallowEqual函数作为equalityFn

现在,我们将在我们的 ToDo 应用程序中实现选择器 Hook,特别是在TodoListTodoFilter组件中。

使用 Hooks 为 TodoList 组件

首先,我们将实现一个选择器 Hook 来获取TodoList组件的所有todos,如下所示:

  1. 首先删除src/containers/ConnectedTodoList.js文件。

  2. 现在,编辑src/components/TodoList.js文件,并从react-redux中导入useSelector Hook:

import { useSelector } from 'react-redux'
  1. 现在,我们可以从函数定义中删除所有的 props:
export default function TodoList () {
  1. 然后,我们定义两个 Selector Hooks,一个用于filter值,一个用于todos值:
    const filter = useSelector(state => state.filter)
    const todos = useSelector(state => state.todos)
  1. 现在,剩下的就是在src/components/App.js中用TodoList组件替换ConnectedTodoList组件。首先,调整导入语句:
import TodoList from './TodoList'
  1. 然后,调整渲染的组件:
    return (
        <div style={{ width: 400 }}>
            <Header />
            <AddTodo />
            <hr />
            <TodoList />

组件的其余部分可以保持不变,因为我们存储状态部分的值与以前的名称相同。

使用 Hooks 为 TodoFilter 组件

最后,我们将在TodoFilter组件中实现选择器和 Dispatch Hooks,因为我们需要突出显示当前的过滤器(来自选择器 Hook 的状态)并分派一个动作来改变过滤器(Dispatch Hook)。

现在让我们为TodoFilter组件实现 Hooks:

  1. 首先,删除src/containers/ConnectedTodoFilter.js文件。

  2. 我们还可以删除src/containers/文件夹,因为现在它是空的。

  3. 现在,编辑src/components/TodoFilter.js文件,并从react-redux中导入useSelectoruseDispatch Hooks:

import { useSelector, useDispatch } from 'react-redux'
  1. 另外,导入filterTodos动作创建者:
import { filterTodos } from '../actions'
  1. 现在,我们可以从函数定义中删除所有的 props:
export default function TodoFilter () {
  1. 然后,定义 Dispatch 和 Selector Hooks:
    const dispatch = useDispatch()
    const filter = useSelector(state => state.filter)
  1. 最后,调整处理函数以调用dispatch()
    function handleFilter () {
        dispatch(filterTodos(name))
    }
  1. 现在,剩下的就是在src/components/App.js中用TodoFilter组件替换ConnectedTodoFilter组件。首先,调整导入语句:
import TodoFilter from './TodoFilter'
  1. 然后,调整渲染的组件:
    return (
        <div style={{ width: 400 }}>
            <Header />
            <AddTodo />
            <hr />
            <TodoList />
            <hr />
            <TodoFilter />
        </div>
    )

现在,我们的 Redux 应用程序完全使用 Hooks 而不是容器组件!

示例代码

示例代码可以在Chapter12/chapter12_3文件夹中找到。

只需运行npm install以安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

创建可重用的选择器

在定义选择器时,我们一直以来都是在每次组件渲染时创建选择器的新实例。这是可以的,如果选择器函数不执行任何复杂操作并且不维护内部状态。否则,我们需要使用可重用的选择器,现在我们将学习有关它们的知识。

设置 reselect

为了创建可重用的选择器,我们可以使用reselect库中的createSelector函数。首先,我们必须通过npm安装该库。执行以下命令:

> npm install --save reselect

现在,reselect库已经安装,我们可以使用它来创建可重用的选择器。

对仅依赖于状态的选择器进行记忆化

如果我们想要记忆化选择器,并且选择器仅依赖于状态(而不是 props),我们可以在组件外部声明选择器,如下所示:

  1. 编辑src/components/TodoList.js文件,并从reselect中导入createSelector函数:
import { createSelector } from 'reselect'
  1. 然后,在组件定义之前,我们为状态的todosfilter部分定义选择器:
const todosSelector = state => state.todos
const filterSelector = state => state.filter

如果选择器被许多组件使用,将它们放在单独的selectors.js文件中并从那里导入可能是有意义的。例如,我们可以将filterSelector放在一个单独的文件中,然后在TodoList.js以及TodoFilter.js中导入它。

  1. 现在,在定义组件之前,我们为过滤后的 todos 定义一个选择器,如下所示:
const selectFilteredTodos = createSelector(
  1. 首先,我们指定要重用的另外两个选择器:
    todosSelector,
    filterSelector,
  1. 现在,我们指定一个过滤选择器,从useMemo Hook 中复制代码:
    (todos, filter) => {
        switch (filter) {
            case 'active':
                return todos.filter(t => t.completed === false)

            case 'completed':
                return todos.filter(t => t.completed === true)

            default:
            case 'all':
                return todos
        }
    }
)
  1. 最后,在选择器钩子中使用我们定义的选择器:
export default function TodoList () {
    const filteredTodos = useSelector(selectFilteredTodos)

现在我们已经为过滤后的 todos 定义了一个可重用的选择器,过滤 todos 的结果将被记忆化,如果状态没有改变,将不会重新计算。

示例代码

示例代码可以在Chapter12/chapter12_4文件夹中找到。

只需运行npm install以安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。

使用 store Hook

React Redux 还提供了一个useStore Hook,它返回对 Redux 存储本身的引用。这是传递给Provider组件的相同store对象。其 API 如下:

const store = useStore()

最好的做法是避免直接使用 Store Hook。通常更有意义的是使用 Dispatch 或 Selector Hooks。但是,也有特殊情况,比如替换 reducer,可能需要使用这个 Hook。

在本节中,我们已经学会了如何在现有的 Redux 应用程序中用 Hooks 替换连接器。现在,我们将学习一种策略,可以有效地将现有的 Redux 应用程序迁移到 Hooks。

迁移 Redux 应用程序

在一些 Redux 应用程序中,本地状态也存储在 Redux 状态树中。在其他情况下,React 类组件状态用于存储本地状态。在任何情况下,迁移现有的 Redux 应用程序的方法如下:

  • 用状态钩子替换简单的本地状态,比如输入字段的值

  • 用 Reducer Hooks 替换复杂的本地状态

  • 全局状态(在多个组件中使用的状态)存储在 Redux 存储中

在上一章中,我们已经学会了如何迁移 React 类组件。在上一节中,我们学会了如何从 Redux 连接器迁移到使用 Selector 和 Dispatch Hooks。现在,我们将展示一个将 Redux 本地状态迁移到基于 Hook 的方法的示例。

假设我们现有的待办事项应用程序将输入字段状态存储在 Redux 中,如下所示:

{
    "todos": [],
    "filter": "all",
    "newTodo": ""
}

现在,每当输入文本时,我们需要分派一个动作,通过调用所有的 reducer 来计算新的状态,然后更新 Redux 存储状态。可以想象,如果有很多输入字段,这可能会导致性能问题。 我们应该使用状态钩子来存储这个本地状态,因为它只在一个组件内部使用。在我们的示例应用程序中,我们在实现AddTodo组件时已经正确地做到了这一点。

现在我们已经学会了如何将现有的 Redux 应用程序迁移到 Hooks,我们可以继续讨论 Redux 的权衡。

Redux 的权衡

总结一下,让我们总结一下在 Web 应用程序中使用 Redux 的利弊。首先,让我们从积极的方面开始:

  • 提供了一定的项目结构,使我们可以轻松地扩展和修改代码

  • 我们的代码中出错的可能性较少

  • 比使用 React Context 进行状态管理性能更好

  • 使 App 组件更简单(将状态管理和操作创建者卸载到 Redux)

Redux 是处理复杂状态变化和在许多组件中使用的状态的较大项目的完美选择。

但是,使用 Redux 也有缺点:

  • 需要编写样板代码

  • 项目结构变得更加复杂

  • Redux 需要一个包装组件(Provider)来连接应用程序到存储

因此,Redux 不应该用于简单的项目。在这些情况下,Reducer Hook 可能就足够了。使用 Reducer Hook,我们无需包装组件来连接我们的应用程序到状态存储。此外,如果我们使用多个 Reducer Hook,向特定的 reducer 发送操作稍微更有效,而不是全局应用程序 reducer。然而,缺点在于必须处理多个 dispatch 函数,并保持各种状态同步。我们也不能使用中间件,包括对 Reducer Hook 的异步操作的支持。如果状态变化复杂但仅局限于某个组件,可能使用 Reducer Hook 是有意义的,但如果状态在多个组件中使用,或者对整个应用程序都很重要,我们应该将其存储在 Redux 中。

如果您的组件不执行以下操作,则可能不需要 Redux:

  • 使用网络

  • 保存或加载状态

  • 与其他非子组件共享状态

在这种情况下,使用状态或 Reducer Hook 而不是 Redux 是有意义的。

总结

在本章中,我们首先学习了 Redux 是什么,以及何时以及为什么应该使用它。然后,我们学习了 Redux 的三个原则。接下来,我们在实践中使用 Redux 处理我们的 ToDo 应用程序中的状态。我们还学习了同步和异步操作创建者。然后,我们学习了如何使用 Hooks 使用 Redux,以及如何将现有的 Redux 应用程序迁移到基于 Hook 的解决方案。最后,我们了解了使用 Redux 和 Reducer Hook 的权衡。

在下一章中,我们将学习如何使用 MobX 处理状态。我们将学习 MobX 是什么,以及如何以传统方式与 React 一起使用它。然后,我们将学习如何使用 Hooks 使用 MobX,并且我们还将了解如何将现有的 MobX 应用程序迁移到基于 Hook 的解决方案。

问题

为了总结本章学到的内容,请尝试回答以下问题:

  1. Redux 应该用于哪种状态?

  2. Redux 由哪些元素组成?

  3. Redux 的三大原则是什么?

  4. 为什么我们要定义动作类型?

  5. 我们如何将组件连接到 Redux?

  6. 我们可以使用哪些 Hooks 与 Redux?

  7. 为什么我们应该创建可重用的选择器?

  8. 我们如何迁移 Redux 应用程序?

  9. Redux 的权衡是什么?

  10. 我们什么时候应该使用 Redux?

进一步阅读

如果您对本章学习的概念更多信息感兴趣,请查看以下阅读材料: