在上一章中,我们学习了如何通过从现有代码中提取自定义 Hook 来构建我们自己的 Hook。然后,我们在博客应用程序中使用了我们自己的 Hook,并了解了本地 Hook 和 Hook 之间的交互。最后,我们学习了如何使用 React 的 Hook 测试库为 Hook 编写测试,并为我们的自定义 Hook 实现了测试。
在本章中,我们将首先使用 React 类组件实现一个 ToDo 应用程序。在下一步中,我们将学习如何将现有的 React 类组件应用程序迁移到 Hook。在实践中看到使用 Hook 的函数组件和类组件之间的差异将加深我们对使用任一解决方案的权衡的理解。此外,在本章结束时,我们将能够将现有的 React 应用程序迁移到 Hook。
本章将介绍以下主题:
- 使用类组件处理状态
- 将应用程序从类组件迁移到 Hook
- 了解类组件与 Hook 的权衡
技术要求
应该已经安装了相当新版本的 Node.js(vll.l2.0 或更高版本)。Node.js 的 npm
包管理器也需要安装。
本章的代码可以在 GitHub 存储库中找到:https://github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter11。
观看以下视频,了解代码的实际应用:
请注意,强烈建议您自己编写代码。不要简单地运行已提供的代码示例。为了正确学习和理解,自己编写代码很重要。但是,如果遇到任何问题,始终可以参考代码示例。
使用类组件处理状态
在我们开始从类组件迁移到 Hook 之前,我们将使用 React 类组件创建一个小型的 ToDo 列表应用程序。在下一节中,我们将使用 Hook 将这些类组件转换为函数组件。最后,我们将比较这两种解决方案。
设计应用程序结构
正如我们之前对博客应用程序所做的那样,我们将从考虑应用程序的基本结构开始。对于此应用程序,我们将需要以下功能:
- 页眉
- 添加新待办事项的方法
- 一种在列表中显示所有待办事项的方法
- 待办事项的筛选器
从模型开始总是一个好主意。那么,让我们开始吧:
- 我们首先为我们的 ToDo 应用程序绘制一个界面模型:
我们的待办事项应用程序的模型
- 接下来,我们定义基本组件,其方式类似于我们使用博客应用程序的方式:
![ch11-fundamental-comp.png](/uploads/2105/ch11-fundamental-comp.png
在我们的应用程序模型中定义基本组件
- 现在我们可以定义容器组件:
在我们的应用程序模型中定义容器组件
如我们所见,我们将需要以下组件:
- 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
组件定义了我们应用程序的基本结构。它将包含一个标题、一种添加新待办事项的方法、一个待办事项列表和一个过滤器。
定义组件
现在,我们将组件定义为静态组件。在本章的后面,我们将为它们实现动态功能。现在,我们将实现以下静态组件:
- Header
- AddTodo
- TodoList
- TodoItem
- TodoFilter
现在让我们开始实现组件。
定义 Header 组件
我们将从 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
组件。现在,我们将在此组件中静态定义两个待办事项。
让我们开始定义 TodoList
组件:
- 创建一个新的
src/TodoList.js
文件。 - 导入
React
和TodoItem
组件:
import React from 'react'
import TodoItem from './TodoItem'
- 然后,定义类组件和
render
方法:
export default class TodoList extends React.Component {
render () {
- 在这个
render
方法中,我们静态地定义两个待办事项:
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
props:
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 () {
- 在这个渲染方法中,我们使用解构来获取
name
prop:
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 状态、生命周期和处理方法实现动态代码。
在本节中,我们将执行以下操作:
- 定义模拟 API
- 定义
StateContext
- 使
App
组件动态化 - 使
AddTodo
组件动态化 - 使
TodoList
组件动态化 - 使
TodoItem
组件动态化 - 使
TodoFilter
组件动态化
让我们开始吧。
定义 API 代码
首先,我们将定义一个获取待办事项的 API。在我们的例子中,我们只是在短暂的延迟后返回一系列待办事项。
让我们开始实现模拟 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
毫秒的延迟后返回一个数组来模拟从 API 获取待办事项。
定义 StateContext
接下来,我们将定义一个保留当前的待办事项列表的 context。我们将该 context 称为 StateContext
。
现在让我们开始实现 StateContext
:
- 创建一个新的
src/StateContext.js
文件。 - 导入
React
,如下所示:
import React from 'react'
- 现在,定义
StateContext
并设置一个空数组作为备用值:
const StateContext = React.createContext([])
- 最后,导出
StateContext
:
export default StateContext
现在,我们有一个可以存储待办事项数组的 context。
使 App 组件动态化
我们现在将通过添加获取、添加、切换、过滤和删除待办事项的功能,使 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
来设置初始状态。最初,将没有待办事项,filter
的值将设置为all
:
this.state = { todos: [], filteredTodos: [], filter: 'all' }
}
- 然后,我们定义
componentDidMount
生命周期方法,该方法将在组件首次渲染时获取待办事项:
componentDidMount () {
this.fetchTodos()
}
- 现在,我们将定义实际的
fetchTodos
方法,在我们的例子中,它只是设置状态,因为我们不打算将这个简单的应用程序连接到后端。我们还将调用this.filterTodos()
,以便在获取 todos 后更新filteredTodos
数组:
fetchTodos () {
fetchAPITodos().then((todos) => {
this.setState({ todos })
this.filterTodos()
})
}
- 接下来,我们定义
addTodo
方法,该方法创建一个新项,并将其添加到状态数组中,类似于我们在博客应用中使用 Hook 所做的:
addTodo (title) {
const { todos } = this.state
const newTodo = { id: generateID(), title, completed: false }
this.setState({ todos: [ newTodo, ...todos ] })
this.filterTodos()
}
- 然后,我们定义
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()
}
- 现在,我们定义
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()
}
- 然后,我们定义一个方法来对待办事项应用特定的
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
}
}
- 现在,我们可以定义
filterTodos
方法,它将调用applyFilter
方法,并更新filteredTodos
数组和filter
值:
filterTodos(filterArg) {
this.setState(({ todos, filter }) => ({
filter: filterArg || filter,
filteredTodos: this.applyFilter(todos, filterArg || filter)
}))
}
我们正在使用filterTodos
,以便在添加/删除项目以及更改过滤器后重新过滤待办事项。为了使这两个功能正常工作,我们需要检查filter
参数filterArg
是否已传递。如果没有,我们从state
回退到当前filter
参数。
- 然后,我们调整
render
方法,以便使用 state 为StateContext
提供值,并将某些方法传递给组件:
render () {
const { filter, filteredTodos } = this.state
return (
<Statecontext.Provider value={filteredTodos}>
- 最后,我们需要将
this
重新绑定到类,以便我们可以将方法传递给我们的组件,而无需更改this
context。按如下方式调整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
contex。
使 AddTodo 组件动态化
在使我们的 App
组件动态化之后,是时候将所有其他组件动态化为 wel1 了。我们将从顶部开始,使用 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 })
}
- 现在,我们将定义一个可以处理正在添加的新待办事项的方法:
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
中获取待办事项。
现在让我们使 TodoList
组件动态化:
- 在
src/TodoList.js
中,我们首先导入StateContext
,位于TodoItem
导入语句下方:
import StateContext from './StateContext'
- 然后,我们将
contextType
设置为StateContext
,这将允许我们通过this.context
访问 context:
export default class TodoList extends React.Component {
static contextType = Statecontext
对于类组件,如果我们想使用多个 context,我们必须使用 StateContext.Consumer
组件,如下所示:
<StateContext.Consumer>{value => <div>State is:{value}</div>}</StateContext.Consumer>
可以想象,使用这样的多个 context 将导致一个非常深的组件树(包装器地狱),并且我们的代码将难以阅读和重构。
- 现在,我们可以从
this.context
获取项目,而不是静态定义它们:
render () {
const items = this.context
- 最后,我们将所有 props 传递给
TodoItem
组件,以便我们可以在那里使用removeTodo
和toggleTodo
方法:
return items.map(item =>
<TodoItem {...item} {...this.props} key={item.id} />
)
}
现在,我们的 TodoList
组件从 StateContext
中获取项目,而不是静态定义它们。
使 TodoItem 组件动态化
现在我们已经将 removeTodo
和 toggleTodo
方法作为 props 传递给了 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
context。创建一个新的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
组件动态化:
- 在
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>
)
}
}
- 在
src/TodoFilter.js
的TodoFilterItem
类中,我们首先定义一个用于设置过滤器的处理方法:
handleFilter () {
const { name, filterTodos } = this.props filterTodos(name)
}
- 然后我们从
TodoFilter
中获取filter
prop:
render () {
const { name, filter = 'all' } = this.props
- 接下来,在以下代码块第
4
行中,我们使用filter
prop 以bold
(加粗)显示当前选择的过滤器:
const style = {
color: 'blue',
cursor: 'pointer',
fontWeight: filter === name ? 'bold' : 'normal',
}
- 然后,我们通过
onClick
将处理方法绑定到筛选项:
return <span style={style} onClick={this.handleFilter}>{name}
</span>
}
- 最后,我们为
TodoFilterItem
类创建一个新的constructor
,并重新绑定this
context。
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
(如果它没有自动打开)。
(节选)
评论 (0)