十一、从 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 。
请查看以下视频以查看代码的运行情况:
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 项目的筛选器
从模型开始总是一个好主意。那么,让我们开始:
- 我们首先为 ToDo 应用绘制一个界面模型:
Mock-up of our ToDo app
- 接下来,我们将以与博客应用类似的方式定义基本组件:
Defining fundamental components in our app mock-up
- 现在我们可以定义容器组件:
Defining container components in our app mock-up
如我们所见,我们需要以下组件:
App
Header
AddTodo
TodoList
TodoItem
TodoFilter (+ TodoFilterItem)
TodoList
组件使用TodoItem
组件,该组件用于显示一个项目,带有一个要完成的复选框和一个删除它的按钮。TodoFilter
组件内部使用TodoFilterItem
组件显示各种过滤器。
初始化项目
我们将使用create-react-app
来创建一个新项目。现在让我们初始化项目:
- 运行以下命令:
> npx create-react-app chapter11_1
- 然后,移除
src/App.css
,因为我们不需要它。 - 接下来,编辑
src/index.css
,并调整边距如下:
margin: 20px;
- 最后,删除当前的
src/App.js
文件,因为我们将在下一步创建一个新文件。
现在,我们的项目已经初始化,我们可以开始定义应用结构了。
定义应用结构
我们已经从模型中知道了我们应用的基本结构,所以让我们从定义App
组件开始:
- 创建一个新的
src/App.js
文件。 - 进口
React
和Header
、AddTodo
、TodoList
和TodoFilter
组件:
import React from 'react'
import Header from './Header'
import AddTodo from './AddTodo'
import TodoList from './TodoList'
import TodoFilter from './TodoFilter'
- 现在将
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
组件开始,因为它是所有组件中最简单的组件:
- 创建一个新的
src/Header.js
文件。 - 导入
React
并用render
方法定义类组件:
import React from 'react'
export default class Header extends React.Component {
render () {
return <h1>ToDo</h1>
}
}
现在,我们的应用的Header
组件已经定义。
定义 AddTodo 组件
接下来,我们将定义AddTodo
组件,它呈现一个input
字段和一个按钮。
现在让我们实现AddTodo
组件:
- 创建一个新的
src/AddTodo.js
文件。 - 导入
React
并定义类组件和render
方法:
import React from 'react'
export default class AddTodo extends React.Component {
render () {
return (
- 在
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
组件:
- 创建一个新的
src/TodoList.js
文件。 - 导入
React
和TodoItem
组件:
import React from 'react'
import TodoItem from './TodoItem'
- 然后定义类组件和
render
方法:
export default class TodoList extends React.Component {
render () {
- 在这个
render
方法中,我们静态地定义了两个 todo 项:
const items = [
{ id: 1, title: 'Write React Hooks book', completed: true },
{ id: 2, title: 'Promote book', completed: false }
]
- 最后,我们将使用
map
函数渲染项目:
return items.map(item =>
<TodoItem {...item} key={item.id} />
)
}
}
如我们所见,TodoList
组件呈现TodoItem
组件的列表。
定义 TodoItem 组件
在定义了TodoList
组件之后,我们现在将定义TodoItem
组件,以便呈现单个项目。
让我们开始定义TodoItem
组件:
- 创建一个新的
src/TodoItem.js
组件。 - 导入
React
,定义组件及render
方式:
import React from 'react'
export default class TodoItem extends React.Component {
render () {
- 现在,我们将使用解构来获得
title
和completed
道具:
const { title, completed } = this.props
- 最后,我们将呈现一个包含一个
checkbox
、一个title
和一个button
的div
元素来删除该项:
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
定义另一个组件。
让我们开始定义TodoFilterItem
和TodoFilter
组件:
- 创建一个新的
src/TodoFilter.js
文件。 - 为
TodoFilterItem
定义一个类组件:
class TodoFilterItem extends React.Component {
render () {
- 在这个
render
方法中,我们使用解构来获得name
道具:
const { name } = this.props
- 接下来,我们将为
style
定义一个对象:
const style = {
color: 'blue',
cursor: 'pointer'
}
- 然后,我们返回一个带有过滤器的
name
值的span
元素,并使用定义的style
对象:
return <span style={style}>{name}</span>
}
}
- 最后,我们可以定义实际的
TodoFilter
组件,它将呈现三个TodoFilterItem
组件,如下所示:
export default class TodoFilter extends React.Component {
render () {
return (
<div>
<TodoFilterItem name="all" />{' / '}
<TodoFilterItem name="active" />{' / '}
<TodoFilterItem name="completed" />
</div>
)
}
}
现在,我们有一个组件列出了三种不同的过滤可能性:all
、active
和completed
。
实现动态代码
现在,我们已经定义了所有静态组件,我们的应用应该看起来就像模型一样。下一步是使用 React state、lifecycle 和 handler 方法实现动态代码。
在本节中,我们将执行以下操作:
- 定义一个模拟 API
- 定义一个
StateContext
- 使
App
组件动态化 - 使
AddTodo
组件动态化 - 使
TodoList
组件动态化 - 使
TodoItem
组件动态化 - 使
TodoFilter
组件动态化
让我们开始吧。
定义 API 代码
首先,我们将定义一个 API 来获取 todo 项。在我们的例子中,我们只是在短暂的延迟之后返回一个 todo 项数组。
让我们开始实现模拟 API:
- 创建一个新的
src/api.js
文件。 - 我们将定义一个函数,该函数将根据通用唯一标识符(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())
}
- 然后,我们定义了
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)
)
现在,我们有了一个函数,通过在延迟100
ms 后返回数组,模拟从 API 获取 todo 项。
定义 StateContext
接下来,我们将定义一个上下文,它将保存当前的待办事项列表。我们将把这个上下文称为StateContext
。
现在让我们开始实施StateContext
:
- 创建一个新的
src/StateContext.js
文件。 - 导入
React
,如下所示:
import React from 'react'
- 现在,定义
StateContext
并设置一个空数组作为回退值:
const StateContext = React.createContext([])
- 最后,导出
StateContext
:
export default StateContext
现在,我们有了一个上下文,可以在其中存储待办事项数组。
使应用组件动态化
我们现在将通过添加获取、添加、切换、筛选和删除 todo 项的功能,使App
组件动态化。此外,我们将定义一个StateContext
提供者。
让我们开始将App
组件动态化:
- 在
src/App.js
中,导入StateContext
,在其他导入语句之后:
import StateContext from './StateContext'
- 然后从
src/api.js
文件中导入fetchAPITodos
和generateID
函数:
import { fetchAPITodos, generateID } from './api'
- 接下来,我们将修改我们的
App
类代码,实现一个constructor
,它将设置初始状态:
export default class App extends React.Component {
constructor (props) {
- 在这个
constructor
中,我们需要首先调用super
,以确保父类(React.Component
构造函数被调用,组件被正确初始化:
super(props)
- 现在,我们可以通过设置
this.state
来设置初始状态。最初没有 todo 项目,filter
值设置为'all'
:
this.state = { todos: [], filteredTodos: [], filter: 'all' }
}
- 然后,我们定义了
componentDidMount
生命周期方法,该方法将在组件首次呈现时获取 todo 项:
componentDidMount () {
this.fetchTodos()
}
- 现在,我们将定义实际的
fetchTodos
方法,在我们的例子中,它只是设置状态,因为我们不会将这个简单的应用连接到后端。我们还将调用this.filterTodos()
以便在获取 TODO 后更新filteredTodos
数组:
fetchTodos () {
fetchAPITodos().then((todos) => {
this.setState({ todos })
this.filterTodos()
})
}
- 接下来,我们定义
addTodo
方法,该方法创建一个新项目,并将其添加到状态数组中,类似于我们在博客应用中使用挂钩所做的操作:
addTodo (title) {
const { todos } = this.state
const newTodo = { id: generateID(), title, completed: false }
this.setState({ todos: [ newTodo, ...todos ] })
this.filterTodos()
}
- 然后定义
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()
}
- 现在,我们定义了
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()
}
- 然后,我们定义一种方法,将某个
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
}
}
- 现在我们可以定义
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
.
- 然后,我们调整
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>
)
}
- 最后,我们需要将
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
组件动态化:
- 在
src/AddTodo.js
中,我们首先定义一个constructor
,为input
字段设置初始state
:
export default class AddTodo extends React.Component {
constructor (props) {
super(props)
this.state = {
input: ''
}
}
- 然后,我们在
input
字段中定义了一种处理更改的方法:
handleInput (e) {
this.setState({ input: e.target.value })
}
- 现在,我们将定义一个方法,该方法可以处理添加的新 todo 项:
handleAdd () {
const { input } = this.state
const { addTodo } = this.props
if (input) {
addTodo(input)
this.setState({ input: '' })
}
}
- 接下来,我们可以将状态值和处理程序方法分配给
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>
)
}
- 最后,我们需要调整
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
组件动态化:
- 在
src/TodoList.js
中,我们首先导入StateContext
,在TodoItem
导入声明下方:
import StateContext from './StateContext'
- 然后,我们将
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.
- 现在,我们可以从
this.context
中获取项目,而不是静态地定义它们:
render () {
const items = this.context
- 最后,我们将所有道具传递给
TodoItem
组件,以便我们可以在那里使用removeTodo
和toggleTodo
方法:
return items.map(item =>
<TodoItem {...item} {...this.props} key={item.id} />
)
}
现在,我们的TodoList
组件从StateContext
获取项目,而不是静态地定义它们。
使 TodoItem 组件动态化
现在我们已经将removeTodo
和toggleTodo
方法作为支柱传递给TodoItem
组件,我们可以在那里实现这些特性。
现在让我们将TodoItem
组件动态化:
- 在
src/TodoItem.js
中,我们首先定义toggleTodo
和removeTodo
函数的处理方法:
handleToggle () {
const { toggleTodo, id } = this.props
toggleTodo(id)
}
handleRemove () {
const { removeTodo, id } = this.props
removeTodo(id)
}
- 然后,我们将处理程序方法分别分配给
checkbox
和button
:
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>
)
}
- 最后,我们需要为处理程序方法重新绑定
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
组件动态化:
- 在
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>
)
}
}
- 在
src/TodoFilter.js
中,在TodoFilterItem
类中,我们首先定义一个用于设置过滤器的处理程序方法:
handleFilter () {
const { name, filterTodos } = this.props
filterTodos(name)
}
- 然后我们从
TodoFilter
获得filter
道具:
render () {
const { name, filter = 'all' } = this.props
- 接下来,我们使用
filter
道具在bold
中显示当前选择的过滤器:
const style = {
color: 'blue',
cursor: 'pointer',
fontWeight: (filter === name) ? 'bold' : 'normal'
}
- 然后,我们通过
onClick
将 handler 方法绑定到过滤器项:
return <span style={style} onClick={this.handleFilter}>{name}</span>
}
- 最后,我们为
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
组件:
- 编辑
src/TodoItem.js
并删除类组件代码。现在我们将定义一个函数组件。 - 我们首先定义函数,它接受五个支柱:
title
值、completed
布尔值、id
值、toggleTodo
函数和removeTodo
函数:
export default function TodoItem ({ title, completed, id, toggleTodo, removeTodo }) {
- 接下来,我们定义两个处理程序函数:
function handleToggle () {
toggleTodo(id)
}
function handleRemove () {
removeTodo(id)
}
- 最后,我们返回 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
组件:
- 编辑
src/TodoList.js
并从 React 导入useContext
挂钩:
import React, { useContext } from 'react'
- 删除类组件代码。现在我们将定义一个函数组件。
- 我们首先定义函数的头。在这种情况下,我们不分解道具,而只是将它们存储在一个
props
对象中:
export default function TodoList (props) {
- 现在我们定义上下文挂钩:
const items = useContext(StateContext)
- 最后,我们返回呈现的
items
列表,使用 destructuring 将item
和props
对象传递给它:
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
组件,它不会使用任何挂钩。但是,我们将用两个功能组件替换TodoFilterItem
和TodoFilter
组件:一个用于TodoFilterItem
,另一个用于TodoFilter
组件。
迁移到 OfFilterItem
首先,我们要迁移TodoFilterItem
组件。现在让我们开始迁移组件:
- 编辑
src/TodoFilter.js
并删除类组件代码。现在我们将定义一个函数组件。 - 为
TodoFilterItem
组件定义一个函数,它将接受三个支柱name
值、filterTodos
函数和filter
值:
function TodoFilterItem ({ name, filterTodos, filter = 'all' }) {
- 在此函数中,我们定义了一个用于更改过滤器的处理程序函数:
function handleFilter () {
filterTodos(name)
}
- 接下来,我们为我们的
span
元素定义一个style
对象:
const style = {
color: 'blue',
cursor: 'pointer',
fontWeight: (filter === name) ? 'bold' : 'normal'
}
- 最后,我们返回并呈现
span
元素:
return <span style={style} onClick={handleFilter}>{name}</span>
}
正如我们所看到的,函数组件比相应的类组件需要更少的样板代码。
迁移到过滤器
现在我们已经迁移了TodoFilterItem
组件,我们可以迁移TodoFilter
组件了。现在让我们迁移它:
- 编辑
src/TodoFilter.js
并删除类组件代码。现在我们将定义一个函数组件。 - 为
TodoFilter
组件定义一个函数。在这里,我们不打算对道具使用解构:
export default function TodoFilter (props) {
- 在这个组件中,我们只返回并呈现三个
TodoFilterItem
组件,将props
传递给它们:
return (
<div>
<TodoFilterItem {...props} name="all" />{' / '}
<TodoFilterItem {...props} name="active" />{' / '}
<TodoFilterItem {...props} name="completed" />
</div>
)
}
现在,我们的TodoFilter
组件已经成功迁移。
迁移 AddTodo 组件
接下来,我们将迁移AddTodo
组件。这里,我们将使用一个状态挂钩来处理input
字段状态。
现在让我们迁移AddTodo
组件:
- 编辑
src/AddTodo.js
并调整 import 语句,从 React 导入useState
挂钩:
import React, { useState } from 'react'
- 删除类组件代码。现在我们将定义一个函数组件。
- 首先,我们定义函数,它只接受一个属性
addTodo
函数:
export default function AddTodo ({ addTodo }) {
- 接下来,我们为
input
字段状态定义一个状态挂钩:
const [ input, setInput ] = useState('')
- 现在我们可以为
input
字段和 add 按钮定义处理函数:
function handleInput (e) {
setInput(e.target.value)
}
function handleAdd () {
if (input) {
addTodo(input)
setInput('')
}
}
- 最后,我们返回并呈现
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
值的减速机开始。现在让我们定义过滤器缩减器:
- 新建
src/reducers.js
文件,从src/api.js
文件导入generateID
功能:
import { generateID } from './api'
- 在
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_TODOS
、ADD_TODO
、TOGGLE_TODO
和REMOVE_TODO
动作。
现在我们来定义todosReducer
函数:
- 在
src/reducers.js
文件中,定义一个新函数,该函数将处理以下操作:
function todosReducer (state, action) {
switch (action.type) {
- 对于
FETCH_TODOS
动作,我们只需将当前状态替换为新的todos
数组:
case 'FETCH_TODOS':
return action.todos
- 对于
ADD_TODO
操作,我们将在当前状态数组的开头插入一个新项:
case 'ADD_TODO':
const newTodo = {
id: generateID(),
title: action.title,
completed: false
}
return [ newTodo, ...state ]
- 对于
TOGGLE_TODO
操作,我们将使用map
函数更新单个 todo 项:
case 'TOGGLE_TODO':
return state.map(t => {
if (t.id === action.id) {
return { ...t, completed: !t.completed }
}
return t
}, [])
- 对于
REMOVE_TODO
操作,我们将使用filter
函数删除单个 todo 项:
case 'REMOVE_TODO':
return state.filter(t => {
if (t.id === action.id) {
return false
}
return true
})
- 默认情况下(对于所有其他操作),我们只返回当前的
state
:
default:
return state
}
}
现在定义了 todos 简化器,我们可以处理FETCH_TODOS
、ADD_TODO
、TOGGLE_TODO
和REMOVE_TODO
动作。
定义应用缩减器
最后,我们需要为我们的应用状态将其他简化器组合成一个简化器。现在我们来定义appReducer
函数:
- 在
src/reducers.js
文件中,为appReducer
定义一个新函数:
export default function appReducer (state, action) {
- 在这个函数中,我们返回一个对象,其中包含来自其他还原器的值。我们只需将子状态和动作传递给其他简化器:
return {
todos: todosReducer(state.todos, action),
filter: filterReducer(state.filter, action)
}
}
现在,我们的简化器组合在一起。所以,我们只有一个state
对象和一个dispatch
函数。
迁移组件
现在我们已经定义了简化器,可以开始迁移App
组件了。现在让我们迁移它:
- 编辑
src/App.js
并将导入语句调整为从React
导入useReducer
、useEffect
、useMemo
:
import React, { useReducer, useEffect, useMemo } from 'react'
- 从
src/reducers.js
导入appReducer
功能:
import appReducer from './reducers'
-
删除类组件代码。现在我们将定义一个函数组件。
-
首先,我们定义函数,它不接受任何道具:
export default function App () {
- 现在,我们使用
appReducer
函数定义简化器挂钩:
const [ state, dispatch ] = useReducer(appReducer, { todos: [], filter: 'all' })
- 接下来,我们定义一个 Effect Hook,它将通过 API 函数获取
todos
,然后发送FETCH_TODOS
动作:
useEffect(() => {
fetchAPITodos().then((todos) =>
dispatch({ type: 'FETCH_TODOS', 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 ])
- 现在,我们定义了将要分派操作和更改状态的各种函数:
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 })
}
- 最后,我们返回并呈现 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
重新绑定) - 无需反复分解相同的值
- 在处理上下文、道具和状态时没有魔力
- 如果我们想在道具改变时重新获取数据,则不需要定义
componentDidMount
和componentDidUpdate
此外,功能组件具有以下优点:
- 鼓励制造小而简单的组件
- 更容易重构
- 更容易测试
- 需要更少的代码
- 对于初学者来说更容易理解
- 更具声明性
但是,在以下情况下,类组件可以正常工作:
- 在遵守某些惯例时。
- 使用最新的 JavaScript 功能时,避免
this
重新绑定。 - 由于现有知识,团队可能更容易理解。
- 许多项目仍然使用类。对于库来说,这不是一个问题,因为它们可以与函数组件一起很好地工作。不过,在工作中,您可能需要使用类。
- 不会很快从 React 中删除(根据 React 团队)。
最后,这是一个偏好的问题,但是挂钩确实比类有很多优势!如果你要开始一个新的项目,一定要用挂钩。如果您正在处理现有项目,考虑将某些组件重构为基于钩的组件是否有意义,以便使它们更简单。然而,您不应该立即将所有项目移植到挂钩,因为重构总是会引入新的 bug。采用挂钩的最佳方法是在适当的时候,缓慢但肯定地用基于挂钩的函数组件替换旧类组件。例如,如果您已经在重构一个组件,您可以重构它以使用挂钩!
总结
在本章中,我们首先使用 React 类组件构建了一个 ToDo 应用。我们从设计应用结构开始,然后实现静态组件,最后使其成为动态组件。在下一节中,我们学习了如何使用类组件将现有项目迁移到使用挂钩的功能组件。最后,我们了解了类组件的权衡,何时应该使用类组件或挂钩,以及如何将现有项目迁移到挂钩。
我们现在已经在实践中看到了 React 类组件与带有挂钩的函数组件的区别。挂钩使我们的代码更加简洁,更易于阅读和维护。我们还了解到,我们应该逐步将我们的组件从类组件迁移到带有挂钩的功能组件,而不需要立即迁移整个应用。
在下一章中,我们将学习如何使用 Redux 处理状态,使用 Redux 与仅使用带有挂钩的功能组件的权衡,如何使用带有挂钩的 Redux,以及如何将现有的 Redux 应用迁移到基于挂钩的设置。
问题
为了回顾我们在本章学到的知识,请尝试回答以下问题:
- React 类组件是如何定义的?
- 在类组件中使用
constructor
时,我们需要调用什么?为什么? - 如何设置类组件的初始状态?
- 如何更改类组件的状态?
- 为什么我们需要用类组件方法重新绑定
this
上下文? - 我们如何重新绑定
this
上下文? - 如何将 React 上下文用于类组件?
- 在迁移到挂钩时,我们可以用什么替换状态管理?
- 使用挂钩和类组件的权衡是什么?
- 应在何时以及如何将现有项目迁移到挂钩?
进一步阅读
如果您对我们在本章中所学概念的更多信息感兴趣,请阅读以下阅读材料:
- ES6 等级:https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Classes
- React 类组件:https://www.robinwieruch.de/react-component-types/#react-类别组件
版权属于:月萌API www.moonapi.com,转载请注明出处