从 React 类组件迁移到函数组件
1. 准备工作
在开始迁移之前,需要为迁移后的项目创建一个新文件夹:
1. 复制
Chapter13_1
文件夹到新的
Chapter13_2
文件夹:
$ cp -R Chapter13_1 Chapter13_2
-
在 VS Code 中打开新的
Chapter13_2文件夹。
接下来要迁移的组件有:
- TodoItem
- TodoList
- TodoFilterItem
- TodoFilter
- AddTodo
- App
2. 迁移 TodoItem 组件
TodoItem 组件是最简单的迁移组件之一,它不使用任何状态或副作用,因此可以直接将其转换为函数组件。具体步骤如下:
1. 编辑
src/TodoItem.jsx
并删除所有现有代码。
2. 定义一个函数组件,该组件接受
title
、
completed
、
id
、
toggleTodo
和
removeTodo
这些属性:
export function TodoItem({ title, completed, id, toggleTodo,
removeTodo }) {
- 定义两个处理函数,用于切换和移除待办事项:
function handleToggle() {
toggleTodo(id)
}
function handleRemove() {
removeTodo(id)
}
- 最后,渲染组件:
return (
<div style={{ width: '400px', height: '25px' }}>
<input type='checkbox' checked={completed}
onChange={handleToggle} />
{title}
<button type='button' style={{ float: 'right' }}
onClick={handleRemove}>
x
</button>
</div>
)
}
函数组件不需要重新绑定
this
,也不需要定义构造函数,并且可以在函数定义中直接解构所有属性。
3. 迁移 TodoList 组件
TodoList 组件会渲染多个 TodoItem 组件,这里使用了上下文,因此可以使用上下文钩子(Context Hook)。迁移步骤如下:
1. 编辑
src/TodoList.jsx
并删除所有现有代码。
2. 导入所需内容:
import { useContext } from 'react'
import { StateContext } from './StateContext.js'
import { TodoItem } from './TodoItem.jsx'
- 定义一个函数组件,这里不解构属性,而是直接获取整个对象:
export function TodoList(props) {
- 定义上下文钩子:
const items = useContext(StateContext)
- 最后,返回渲染项的列表:
return items.map((item) => <TodoItem {...item} {...props}
key={item.id} />)
}
使用钩子处理上下文更加直接,只需调用一个函数(上下文钩子)并使用返回值即可。
4. 迁移 TodoFilter 组件
TodoFilter 组件不使用任何钩子,但要将
TodoFilterItem
和
TodoFilter
类组件替换为两个函数组件。步骤如下:
1. 编辑
src/TodoFilter.jsx
并删除所有现有代码。
2. 定义
TodoFilterItem
组件:
export function TodoFilterItem({ name, filterTodos, filter = 'all'
}) {
function handleFilter() {
filterTodos(name)
}
return (
<button type='button' disabled={filter === name}
onClick={handleFilter}>
{name}
</button>
)
}
-
定义
TodoFilter组件:
export function TodoFilter(props) {
return (
<div>
<TodoFilterItem {...props} name='all' />
<TodoFilterItem {...props} name='active' />
<TodoFilterItem {...props} name='completed' />
</div>
)
}
TodoFilter 组件静态渲染三个 TodoFilterItem 组件,用于在显示所有、活动或已完成的待办事项之间切换过滤器。
5. 迁移 AddTodo 组件
对于 AddTodo 组件,将使用状态钩子(State Hook)来处理输入字段的状态。迁移步骤如下:
1. 编辑
src/AddTodo.jsx
并删除所有现有代码。
2. 导入
useState
函数:
import { useState } from 'react'
-
定义
AddTodo组件,该组件接受一个addTodo函数作为属性:
export function AddTodo({ addTodo }) {
- 为输入字段的状态定义一个状态钩子:
const [input, setInput] = useState('')
- 定义处理函数:
function handleInput(e) {
setInput(e.target.value)
}
function handleSubmit(e) {
e.preventDefault()
if (input) {
addTodo(input)
setInput('')
}
}
- 最后,渲染输入字段和按钮:
return (
<form onSubmit={handleSubmit}>
<input
type='text'
placeholder='enter new task...'
style={{ width: '350px' }}
value={input}
onChange={handleInput}
/>
<input
type='submit'
style={{ float: 'right' }}
value='add'
disabled={!input}
/>
</form>
)
}
使用状态钩子使状态管理更加简单,可以为每个状态值定义单独的值和设置函数,而不需要处理单个状态对象,代码也更加简洁。
6. 迁移状态管理和 App 组件
6.1 定义动作
应用将接受五个动作:
| 动作 | 描述 | 示例 |
| ---- | ---- | ---- |
| LOAD_TODOS | 加载新的待办事项列表 |
{ type: 'LOAD_TODOS', todos: [] }
|
| ADD_TODO | 插入新的待办事项 |
{ type: 'ADD_TODO', title: 'Test todo app' }
|
| TOGGLE_TODO | 切换待办事项的完成状态 |
{ type: 'TOGGLE_TODO', id: 'xxx' }
|
| REMOVE_TODO | 移除待办事项 |
{ type: 'REMOVE_TODO', id: 'xxx' }
|
| FILTER_TODOS | 过滤待办事项 |
{ type: 'FILTER_TODOS', filter: 'completed' }
|
6.2 定义减速器
需要一个应用减速器和两个子减速器:一个用于待办事项,一个用于过滤器。
-
定义过滤器减速器
:
1. 创建一个新的
src/reducers.js
文件。
2. 在该文件中定义
filterReducer
函数,用于处理
FILTER_TODOS
动作并相应地设置过滤器值:
function filterReducer(state, action) {
if (action.type === 'FILTER_TODOS') {
return action.filter
}
return state
}
-
定义待办事项减速器
:
编辑src/reducers.js并定义todosReducer函数,处理LOAD_TODOS、ADD_TODO、TOGGLE_TODO和REMOVE_TODO动作:
function todosReducer(state, action) {
switch (action.type) {
case 'LOAD_TODOS':
return action.todos
case 'ADD_TODO': {
const newTodo = { id: Date.now(), title: action.title,
completed: false }
return [newTodo, ...state]
}
case 'TOGGLE_TODO': {
return state.map((item) => {
if (item.id === action.id) {
return { ...item, completed: !item.completed }
}
return item
})
}
case 'REMOVE_TODO': {
return state.filter((item) => item.id !== action.id)
}
default:
return state
}
}
-
定义应用减速器
:
编辑src/reducers.js并定义和导出appReducer函数,将两个减速器组合成一个:
export function appReducer(state, action) {
return {
todos: todosReducer(state.todos, action),
filter: filterReducer(state.filter, action),
}
}
6.3 迁移 App 组件
-
编辑
src/App.jsx并删除所有现有代码。 - 导入所需内容:
import { useReducer, useEffect, useMemo } from 'react'
import { Header } from './Header.jsx'
import { AddTodo } from './AddTodo.jsx'
import { TodoList } from './TodoList.jsx'
import { TodoFilter } from './TodoFilter.jsx'
import { StateContext } from './StateContext.js'
import { fetchTodos } from './api.js'
import { appReducer } from './reducers.js'
- 定义函数组件,不接受任何属性:
export function App() {
-
使用
appReducer函数定义一个 Reducer 钩子:
const [state, dispatch] = useReducer(appReducer, { todos: [],
filter: 'all' })
- 解构状态对象,方便后续访问:
const { todos, filter } = state
- 使用 Memo 钩子实现过滤机制:
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter((item) => item.completed === false)
case 'completed':
return todos.filter((item) => item.completed === true)
case 'all':
default:
return todos
}
}, [todos, filter])
-
定义一个 Effect 钩子,从模拟 API 获取待办事项并分发
LOAD_TODOS动作:
useEffect(() => {
async function loadTodos() {
const todos = await fetchTodos()
dispatch({ type: 'LOAD_TODOS', todos })
}
void loadTodos()
}, [])
- 定义各种函数,用于分发动作并更改应用状态:
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 })
}
- 最后,渲染待办应用所需的所有组件:
return (
<StateContext.Provider value={filteredTodos}>
<div style={{ width: '400px' }}>
<Header />
<AddTodo addTodo={addTodo} />
<hr />
<TodoList toggleTodo={toggleTodo} removeTodo={removeTodo} />
<hr />
<TodoFilter filter={filter} filterTodos={filterTodos} />
</div>
</StateContext.Provider>
)
}
- 应用完全迁移后,启动开发服务器并验证一切是否正常工作:
$ npm run dev
使用 Reducer 钩子处理复杂的状态更改使代码更加简洁和易于维护,应用现在已完全迁移到 Hooks,并且功能与之前相同。
7. React 类组件与 React Hooks 的权衡
使用带有 Hooks 的函数组件有很多优点:
- 更易于理解和测试,因为它们只是 JavaScript 函数,而不是复杂的 React 构造。
- 可以将状态更改逻辑重构到单独的
reducers.js
文件中,从而将其与 App 组件解耦,更易于重构和测试。
- 不需要处理构造函数、令人困惑的
this
上下文、多次解构相同的值,处理上下文、属性和状态时也没有魔法操作。
- 不需要定义
componentDidMount
和
componentDidUpdate
来在属性更改时重新获取数据。
- 鼓励创建小而简单的组件,更易于重构、测试,代码量更少,对初学者更友好,更具声明性,并且 React 服务器组件(RSCs)只能是函数组件。
然而,类组件在以下情况下也可以适用:
- 当遵循某些约定时。
- 当使用箭头函数避免
this
重新绑定时。
- 由于团队已有相关知识,可能更容易理解。
- 许多项目仍然使用类,在遗留代码库中可能仍需要使用类组件。
总之,是否使用 Hooks 取决于个人偏好,但 Hooks 相比类组件有很多优势。如果是启动新项目,建议使用 Hooks;如果是处理现有项目,可以考虑将某些组件重构为基于 Hooks 的组件,但不要立即将所有项目迁移到 Hooks,因为重构可能会引入新的错误,最好是在适当的时候逐步替换旧的类组件。
从 React 类组件迁移到函数组件(续)
8. 迁移总结
在前面的内容中,我们完成了从 React 类组件到使用 Hooks 的函数组件的迁移过程。整个迁移流程可以用以下 mermaid 流程图表示:
graph LR
A[准备工作] --> B[迁移 TodoItem 组件]
B --> C[迁移 TodoList 组件]
C --> D[迁移 TodoFilter 组件]
D --> E[迁移 AddTodo 组件]
E --> F[迁移状态管理和 App 组件]
F --> G[验证迁移结果]
通过这个迁移过程,我们将一个原本基于类组件构建的待办事项应用,逐步转换为使用 Hooks 的函数组件应用。在迁移过程中,我们分别对不同的组件进行了处理,具体步骤总结如下:
| 组件名称 | 迁移关键步骤 | 使用的 Hooks |
| ---- | ---- | ---- |
| TodoItem | 转换为函数组件,定义处理函数 | 无 |
| TodoList | 使用上下文钩子获取数据并渲染 | useContext |
| TodoFilter | 替换为函数组件,静态渲染子组件 | 无 |
| AddTodo | 使用状态钩子处理输入字段状态 | useState |
| App | 使用 Reducer 钩子管理状态,Effect 钩子获取数据,Memo 钩子优化过滤逻辑 | useReducer、useEffect、useMemo |
9. 深入理解 Hooks 的优势
在迁移过程中,我们深刻体会到了 Hooks 带来的诸多优势。下面我们通过对比类组件和函数组件在处理常见任务时的代码差异,进一步说明 Hooks 的优势。
9.1 状态管理
在类组件中,状态管理通常需要在构造函数中初始化状态,并且在需要更新状态时使用
this.setState
方法。例如:
class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
increment = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
而在函数组件中,使用
useState
钩子可以更简洁地管理状态:
import { useState } from 'react';
function FunctionComponent() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
可以看到,函数组件使用
useState
钩子避免了构造函数的使用,并且代码更加简洁明了。
9.2 副作用处理
在类组件中,副作用处理通常需要在
componentDidMount
、
componentDidUpdate
和
componentWillUnmount
生命周期方法中进行。例如:
class ClassComponent extends React.Component {
componentDidMount() {
console.log('Component mounted');
}
componentDidUpdate() {
console.log('Component updated');
}
componentWillUnmount() {
console.log('Component will unmount');
}
render() {
return <div>Class Component</div>;
}
}
而在函数组件中,使用
useEffect
钩子可以更方便地处理副作用:
import { useEffect } from 'react';
function FunctionComponent() {
useEffect(() => {
console.log('Component mounted or updated');
return () => {
console.log('Component will unmount');
};
}, []);
return <div>Function Component</div>;
}
useEffect
钩子将副作用处理逻辑集中在一起,并且可以通过依赖项数组来控制副作用的执行时机,代码更加简洁和易于维护。
10. 迁移的最佳实践
在将现有项目从类组件迁移到函数组件时,为了避免引入新的错误,提高迁移的效率和质量,我们可以遵循以下最佳实践:
-
逐步迁移
:不要一次性将整个项目迁移到 Hooks,而是选择一些相对独立的组件进行迁移,逐步验证迁移结果,确保项目的稳定性。例如,可以先从一些简单的展示组件开始迁移,如
TodoItem
组件。
-
编写测试用例
:在迁移之前,为要迁移的组件编写完善的测试用例,确保在迁移过程中不会破坏组件的原有功能。可以使用 Jest 和 React Testing Library 等工具进行测试。
-
代码审查
:在迁移完成后,进行严格的代码审查,确保代码符合项目的编码规范和设计原则。同时,审查代码中是否存在潜在的问题,如内存泄漏、副作用处理不当等。
-
文档更新
:及时更新项目的文档,包括组件的使用说明、状态管理逻辑等,确保团队成员能够理解和维护迁移后的代码。
11. 未来展望
随着 React 生态系统的不断发展,Hooks 已经成为 React 开发的主流方式。未来,我们可以期待更多基于 Hooks 的优秀库和工具的出现,进一步提高开发效率和代码质量。同时,React 团队也在不断改进和完善 Hooks,为开发者提供更好的开发体验。
对于开发者来说,掌握 Hooks 的使用是必不可少的技能。通过使用 Hooks,我们可以编写更加简洁、易于维护和测试的代码,提高开发效率和项目的可维护性。在实际项目中,我们应该积极采用 Hooks,并结合项目的实际需求,灵活运用各种 Hooks 来解决问题。
总之,从 React 类组件迁移到使用 Hooks 的函数组件是一个值得尝试的过程,它将为我们带来更好的开发体验和项目质量。希望通过本文的介绍,你能够更好地理解和掌握 React Hooks 的使用,在实际开发中发挥出它们的优势。
超级会员免费看
1337

被折叠的 条评论
为什么被折叠?



