十一、从 React 类组件迁移

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

在本章中,我们将首先使用 React 类组件实现 ToDo 应用。在下一步中,我们将学习如何将现有的 React 类组件应用迁移到挂钩。在实践中看到使用挂钩的函数组件和类组件之间的差异将加深我们对使用这两种解决方案的权衡的理解。此外,在本章结束时,我们将能够将现有的 React 应用迁移到挂钩。

本章将介绍以下主题:

  • 使用类组件处理状态
  • 将应用从类组件迁移到挂钩
  • 了解类组件与挂钩之间的权衡

技术要求

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

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

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

http://bit.ly/2Mm9yoC

Please note that it is highly recommended that you write the code on your own. Do not simply run the code examples that have been provided. It is important that you write the code yourself in order to be able to learn and understand properly. However, if you run into any issues, you can always refer to the code example.

现在,让我们从这一章开始。

使用类组件处理状态

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

设计应用结构

正如我们之前使用博客应用所做的那样,我们将从思考应用的基本结构开始。对于此应用,我们需要以下功能:

  • 标题
  • 添加新待办事项的方法
  • 在列表中显示所有待办事项的方法
  • todo 项目的筛选器

从模型开始总是一个好主意。那么,让我们开始:

  1. 我们首先为 ToDo 应用绘制一个界面模型:

Mock-up of our ToDo app

  1. 接下来,我们将以与博客应用类似的方式定义基本组件:

Defining fundamental components in our app mock-up

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

Defining container components in our app mock-up

如我们所见,我们需要以下组件:

  • App
  • Header
  • AddTodo
  • 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组件定义了我们应用的基本结构。它将由标题、添加新 todo 项目的方法、todo 项目列表和筛选器组成。

定义组件

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

  • Header
  • AddTodo
  • 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组件。现在,我们将在此组件中静态定义两个 todo 项。

让我们开始定义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方法中,我们静态地定义了两个 todo 项:
        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. 最后,我们将呈现一个包含一个checkbox、一个title和一个buttondiv元素来删除该项:
        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 state、lifecycle 和 handler 方法实现动态代码。

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

  • 定义一个模拟 API
  • 定义一个StateContext
  • 使App组件动态化
  • 使AddTodo组件动态化
  • 使TodoList组件动态化
  • 使TodoItem组件动态化
  • 使TodoFilter组件动态化

让我们开始吧。

定义 API 代码

首先,我们将定义一个 API 来获取 todo 项。在我们的例子中,我们只是在短暂的延迟之后返回一个 todo 项数组。

让我们开始实现模拟 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)
    )

现在,我们有了一个函数,通过在延迟100ms 后返回数组,模拟从 API 获取 todo 项。

定义 StateContext

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

现在让我们开始实施StateContext

  1. 创建一个新的src/StateContext.js文件。
  2. 导入React,如下所示:
import React from 'react'
  1. 现在,定义StateContext并设置一个空数组作为回退值:
const StateContext = React.createContext([])
  1. 最后,导出StateContext
export default StateContext

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

使应用组件动态化

我们现在将通过添加获取、添加、切换、筛选和删除 todo 项的功能,使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来设置初始状态。最初没有 todo 项目,filter值设置为'all'
        this.state = { todos: [], filteredTodos: [], filter: 'all' }
    }
  1. 然后,我们定义了componentDidMount生命周期方法,该方法将在组件首次呈现时获取 todo 项:
    componentDidMount () {
        this.fetchTodos()
    }
  1. 现在,我们将定义实际的fetchTodos方法,在我们的例子中,它只是设置状态,因为我们不会将这个简单的应用连接到后端。我们还将调用this.filterTodos()以便在获取 TODO 后更新filteredTodos数组:
    fetchTodos () {
        fetchAPITodos().then((todos) => {
            this.setState({ todos })
            this.filterTodos()
        })
    }
  1. 接下来,我们定义addTodo方法,该方法创建一个新项目,并将其添加到状态数组中,类似于我们在博客应用中使用挂钩所做的操作:
    addTodo (title) {
        const { todos } = this.state

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

        this.setState({ todos: [ newTodo, ...todos ] })
        this.filterTodos()
    }
  1. 然后定义toggleTodo方法,使用map函数查找并修改某个 todo 项:
    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函数查找并删除某个 todo 项:
    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应用于我们的 todo 项目:
    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)
        }))
    }

We are using filterTodos in order to re-filter todos after adding/removing items, as well as changing the filter. To allow both functionalities to work correctly, we need to check whether the filter argument, filterArg, was passed. If not, we fall back to the current filter argument from the state.

  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. 现在,我们将定义一个方法,该方法可以处理添加的新 todo 项:
    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获取 todo 项。

现在让我们将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

With class components, if we want to use multiple contexts, we have to use the StateContext.Consumer component, as follows: <StateContext.Consumer>{value => <div>State is: {value}</div>}</StateContext.Consumer>.

As you can imagine, using multiple contexts like this, will result in a very deep component tree (wrapper hell), and our code will be hard to read and refactor.

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

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

使 TodoItem 组件动态化

现在我们已经将removeTodotoggleTodo方法作为支柱传递给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方法动态筛选 todo 项目列表。

让我们开始将TodoFilter组件动态化:

  1. src/TodoFilter.js中,在TodoFilter类中,我们将所有道具传递给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道具:
    render () {
        const { name, filter = 'all' } = this.props
  1. 接下来,我们使用filter道具在bold中显示当前选择的过滤器:
        const style = {
            color: 'blue',
            cursor: 'pointer',
            fontWeight: (filter === name) ? 'bold' : 'normal'
        }
  1. 然后,我们通过onClick将 handler 方法绑定到过滤器项:
        return <span style={style} onClick={this.handleFilter}>{name}</span>
    }
  1. 最后,我们为TodoFilterItem类创建一个新的constructor,并重新绑定 handler 方法的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 挂钩。我们将展示如何迁移副作用,例如在组件装载时获取 TODO,以及用于输入的状态管理。

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

  • TodoItem
  • TodoList
  • TodoFilterItem
  • TodoFilter
  • AddTodo
  • App

迁移 TodoItem 组件

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

让我们开始迁移TodoItem组件:

  1. 编辑src/TodoItem.js并删除类组件代码。现在我们将定义一个函数组件。
  2. 我们首先定义函数,它接受五个支柱: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>
    )
}

Try to keep your function components small, and combine them by creating new function components that wrap them. It is always a good idea to have many small components, rather than one large component. They are much easier to maintain, reuse, and refactor.

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

迁移 TodoList 组件

接下来,我们将迁移TodoList组件,它封装TodoItem组件。这里,我们使用上下文,这意味着我们现在可以使用上下文挂钩。

现在让我们迁移TodoList组件:

  1. 编辑src/TodoList.js并从 React 导入useContext挂钩:
import React, { useContext } from 'react'
  1. 删除类组件代码。现在我们将定义一个函数组件。
  2. 我们首先定义函数的头。在这种情况下,我们不分解道具,而只是将它们存储在一个props对象中:
export default function TodoList (props) {
  1. 现在我们定义上下文挂钩:
    const items = useContext(StateContext)
  1. 最后,我们返回呈现的items列表,使用 destructuring 将itemprops对象传递给它:
    return items.map(item =>
        <TodoItem {...item} {...props} key={item.id} />
    )
}

We define the key prop last, in order to avoid overwriting it with the destructuring of the item and props objects.

正如我们所看到的,使用带有挂钩的上下文要简单得多。我们可以简单地调用一个函数,并使用返回值。使用多个上下文时,没有神奇的this.context赋值或包装地狱!

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

迁移 TodoFilter 组件

接下来是TodoFilter组件,它不会使用任何挂钩。但是,我们将用两个功能组件替换TodoFilterItemTodoFilter组件:一个用于TodoFilterItem,另一个用于TodoFilter组件。

迁移到 OfFilterItem

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

  1. 编辑src/TodoFilter.js并删除类组件代码。现在我们将定义一个函数组件。
  2. TodoFilterItem组件定义一个函数,它将接受三个支柱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>
}

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

迁移到过滤器

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

  1. 编辑src/TodoFilter.js并删除类组件代码。现在我们将定义一个函数组件。
  2. TodoFilter组件定义一个函数。在这里,我们不打算对道具使用解构:
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组件。这里,我们将使用一个状态挂钩来处理input字段状态。

现在让我们迁移AddTodo组件:

  1. 编辑src/AddTodo.js并调整 import 语句,从 React 导入useState挂钩:
import React, { useState } from 'react'
  1. 删除类组件代码。现在我们将定义一个函数组件。
  2. 首先,我们定义函数,它只接受一个属性addTodo函数:
export default function AddTodo ({ addTodo }) {
  1. 接下来,我们为input字段状态定义一个状态挂钩:
    const [ input, setInput ] = useState('')
  1. 现在我们可以为input字段和 add 按钮定义处理函数:
    function handleInput (e) {
        setInput(e.target.value)
    }

    function handleAdd () {
        if (input) {
            addTodo(input)
            setInput('')
        }
    }
  1. 最后,我们返回并呈现input字段和 add 按钮:
    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>
    )
}

正如我们所看到的,使用状态挂钩使状态管理更加简单。我们可以为每个状态值定义单独的值和 setter 函数,而不必处理状态对象。此外,我们不需要一直从this.state解构。因此,我们的代码更加简洁明了。

迁移应用组件

最后,剩下要做的就是迁移App组件。然后,我们的整个 ToDo 应用将被迁移到 React Hooks。在这里,我们将使用一个 Reducer 挂钩来管理状态,一个 Effect 挂钩来在组件挂载时获取 TODO,以及一个 Memo 挂钩来存储过滤后的 TODO 列表。

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

  • 定义操作
  • 定义简化器
  • 迁移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' }

在定义动作之后,我们可以继续定义简化器。

定义简化器

我们现在要为我们的州定义简化器。我们需要一个应用简化器和两个子简化器:一个用于 TODO,一个用于过滤器。

过滤后的 TODO 列表将由App组件动态计算。我们可以稍后使用 Memo 挂钩缓存结果,避免对过滤后的 todos 列表进行不必要的重新计算。

定义过滤器简化器

我们将从定义filter值的减速机开始。现在让我们定义过滤器缩减器:

  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 简化器

接下来,我们将为 todo 项定义一个函数。在这里,我们将处理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函数更新单个 todo 项:
        case 'TOGGLE_TODO':
            return state.map(t => {
                if (t.id === action.id) {
                    return { ...t, completed: !t.completed }
                }
                return t
            }, [])
  1. 对于REMOVE_TODO操作,我们将使用filter函数删除单个 todo 项:
        case 'REMOVE_TODO':
            return state.filter(t => {
                if (t.id === action.id) {
                    return false
                }
                return true
            })
  1. 默认情况下(对于所有其他操作),我们只返回当前的state
        default:
            return state
    }
}

现在定义了 todos 简化器,我们可以处理FETCH_TODOSADD_TODOTOGGLE_TODOREMOVE_TODO动作。

定义应用缩减器

最后,我们需要为我们的应用状态将其他简化器组合成一个简化器。现在我们来定义appReducer函数:

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

现在,我们的简化器组合在一起。所以,我们只有一个state对象和一个dispatch函数。

迁移组件

现在我们已经定义了简化器,可以开始迁移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. 首先,我们定义函数,它不接受任何道具:

export default function App () {
  1. 现在,我们使用appReducer函数定义简化器挂钩:
    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. 然后,我们使用备忘录挂钩实现过滤机制,以优化性能,避免在没有任何变化时重新计算过滤后的待办事项列表:
    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>
    )
}

正如我们所看到的,使用减缩器来处理复杂的状态更改使我们的代码更简洁,更易于维护。我们的应用现在已完全迁移到 Hooks!

示例代码

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

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

类组件的权衡

现在我们已经完成了从类组件到挂钩的迁移,让我们修改并总结一下所学内容。

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

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

| 对比:JavaScript 代码行 | | 类组件 | 带挂钩的功能部件 | | 36  ./TodoFilter.js 15  ./TodoList.js 59  ./AddTodo.js 12  ./index.js 7   ./Header.js

9   ./App.test.js

109 ./App.js 31  ./TodoItem.js | 25  ./TodoFilter.js 12  ./TodoList.js 42  ./AddTodo.js 12  ./index.js

50  ./reducers.js 5   ./StateContext.js 135 ./serviceWorker.js 12  ./api.js 64  ./App.js 19  ./TodoItem.js | | 430 total | 392 total |

对于功能组件和挂钩,不需要考虑以下几点:

  • 不需要与施工人员打交道
  • 无混淆的this上下文(this重新绑定)
  • 无需反复分解相同的值
  • 在处理上下文、道具和状态时没有魔力
  • 如果我们想在道具改变时重新获取数据,则不需要定义componentDidMountcomponentDidUpdate

此外,功能组件具有以下优点:

  • 鼓励制造小而简单的组件
  • 更容易重构
  • 更容易测试
  • 需要更少的代码
  • 对于初学者来说更容易理解
  • 更具声明性

但是,在以下情况下,类组件可以正常工作:

  • 在遵守某些惯例时。
  • 使用最新的 JavaScript 功能时,避免this重新绑定。
  • 由于现有知识,团队可能更容易理解。
  • 许多项目仍然使用类。对于库来说,这不是一个问题,因为它们可以与函数组件一起很好地工作。不过,在工作中,您可能需要使用类。
  • 不会很快从 React 中删除(根据 React 团队)。

最后,这是一个偏好的问题,但是挂钩确实比类有很多优势!如果你要开始一个新的项目,一定要用挂钩。如果您正在处理现有项目,考虑将某些组件重构为基于钩的组件是否有意义,以便使它们更简单。然而,您不应该立即将所有项目移植到挂钩,因为重构总是会引入新的 bug。采用挂钩的最佳方法是在适当的时候,缓慢但肯定地用基于挂钩的函数组件替换旧类组件。例如,如果您已经在重构一个组件,您可以重构它以使用挂钩!

总结

在本章中,我们首先使用 React 类组件构建了一个 ToDo 应用。我们从设计应用结构开始,然后实现静态组件,最后使其成为动态组件。在下一节中,我们学习了如何使用类组件将现有项目迁移到使用挂钩的功能组件。最后,我们了解了类组件的权衡,何时应该使用类组件或挂钩,以及如何将现有项目迁移到挂钩。

我们现在已经在实践中看到了 React 类组件与带有挂钩的函数组件的区别。挂钩使我们的代码更加简洁,更易于阅读和维护。我们还了解到,我们应该逐步将我们的组件从类组件迁移到带有挂钩的功能组件,而不需要立即迁移整个应用。

在下一章中,我们将学习如何使用 Redux 处理状态,使用 Redux 与仅使用带有挂钩的功能组件的权衡,如何使用带有挂钩的 Redux,以及如何将现有的 Redux 应用迁移到基于挂钩的设置。

问题

为了回顾我们在本章学到的知识,请尝试回答以下问题:

  1. React 类组件是如何定义的?
  2. 在类组件中使用constructor时,我们需要调用什么?为什么?
  3. 如何设置类组件的初始状态?
  4. 如何更改类组件的状态?
  5. 为什么我们需要用类组件方法重新绑定this上下文?
  6. 我们如何重新绑定this上下文?
  7. 如何将 React 上下文用于类组件?
  8. 在迁移到挂钩时,我们可以用什么替换状态管理?
  9. 使用挂钩和类组件的权衡是什么?
  10. 应在何时以及如何将现有项目迁移到挂钩?

进一步阅读

如果您对我们在本章中所学概念的更多信息感兴趣,请阅读以下阅读材料: