Migrating a React Todo App from Class Components to Hooks
1. Initial Setup and Component Mapping
To start building our todo app, we first need to map out the container components that will group together the simple components. Here are the components we’ll need:
- App
- Header
- AddTodo
- TodoList
- TodoItem
- TodoFilter (+ TodoFilterItem)
The
TodoList
component uses a
TodoItem
component to show each todo item, with a checkbox to mark it as completed and a button to remove it. The
TodoFilter
component internally uses a
TodoFilterItem
component to display different filters.
Initializing the Project
We’ll use a barebones Vite app to create a new project. Follow these steps:
1. Copy the
Chapter01_3
folder to a new
Chapter13_1
folder:
$ cp -R Chapter01_3 Chapter13_1
-
Open the new
Chapter13_1folder in VS Code. -
Delete the current
src/App.jsxfile, as we’ll create a new one.
Defining the App Structure
Now, let’s define the basic structure of our app by creating the
App
component:
1. Create a new
src/App.jsx
file.
2. Import React and all the container components:
import React from 'react'
import { Header } from './Header.jsx'
import { AddTodo } from './AddTodo.jsx'
import { TodoList } from './TodoList.jsx'
import { TodoFilter } from './TodoFilter.jsx'
-
Define the
Appcomponent as a class component with arendermethod:
export class App extends React.Component {
render() {
return (
<div style={{ width: '400px' }}>
<Header />
<AddTodo />
<hr />
<TodoList />
<hr />
<TodoFilter />
</div>
)
}
}
The
App
component sets up the basic layout of our app, including a header, a way to add new todo items, a list of todo items, and a filter.
Defining the Static Components
Next, we’ll define the following components as static components. We’ll add dynamic functionality to them later.
Header Component
-
Create a new
src/Header.jsxfile. -
Import React and define the class component with a
rendermethod:
import React from 'react'
export class Header extends React.Component {
render() {
return <h1>ToDo</h1>
}
}
AddTodo Component
-
Create a new
src/AddTodo.jsxfile. - Import React and define the class component:
import React from 'react'
export class AddTodo extends React.Component {
render() {
return (
<form>
<input
type='text'
placeholder='enter new task...'
style={{ width: '350px' }}
/>
<input
type='submit'
style={{ float: 'right' }}
value='add'
/>
</form>
)
}
}
TodoList Component
-
Create a new
src/TodoList.jsxfile. -
Import React and the
TodoItemcomponent:
import React from 'react'
import { TodoItem } from './TodoItem.jsx'
- Define the class component:
export class TodoList extends React.Component {
render() {
const items = [
{ id: 1, title: 'Finish React Hooks book', completed: true },
{ id: 2, title: 'Promote the book', completed: false },
]
return items.map((item) => <TodoItem {...item} key={item.id} />)
}
}
TodoItem Component
-
Create a new
src/TodoItem.jsxfile. - Import React and define the class component:
import React from 'react'
export class TodoItem extends React.Component {
render() {
const { title, completed } = this.props
return (
<div style={{ width: '400px', height: '25px' }}>
<input type='checkbox' checked={completed} />
{title}
<button type='button' style={{ float: 'right' }}>x</button>
</div>
)
}
}
TodoFilter Component
-
Create a new
src/TodoFilter.jsxfile. -
Import React and define a
TodoFilterItemclass component:
import React from 'react'
export class TodoFilterItem extends React.Component {
render() {
const { name } = this.props
return <button type='button'>{name}</button>
}
}
-
Define the actual
TodoFiltercomponent, which renders threeTodoFilterItemcomponents:
export class TodoFilter extends React.Component {
render() {
return (
<div>
<TodoFilterItem name='all' />
<TodoFilterItem name='active' />
<TodoFilterItem name='completed' />
</div>
)
}
}
Starting the Static App
After implementing all the static components, we can start the app by running the following command:
$ npm run dev
Open the URL in a browser, and you’ll see that the app looks like our mock-up. However, it’s completely static, and clicking on anything won’t do anything.
2. Implementing Dynamic Code
Now that we have our static components in place, it’s time to make our app dynamic using React state, life cycle, and handler functions.
Defining a Mock API
We’ll start by defining a mock API to fetch todo items. This API will return an array of todo items after a short delay to simulate a network request.
1. Create a new
src/api.js
file.
2. Define and export a function that returns items after a short delay:
const mockItems = [
{ id: 1, title: 'Finish React Hooks book', completed: true },
{ id: 2, title: 'Promote the book', completed: false },
]
export function fetchTodos() {
return new Promise((resolve) => {
setTimeout(() => resolve(mockItems), 100)
})
}
Defining the StateContext
Next, we’ll define a context to keep track of the current list of todos.
1. Create a new
src/StateContext.js
file.
2. Import the
createContext
function from React:
import { createContext } from 'react'
- Define and export a context that contains an empty array:
export const StateContext = createContext([])
Making the App Component Dynamic
We’ll start by making the
App
component dynamic, adding functionality to fetch, add, toggle, filter, and remove todo items.
1. Edit
src/App.jsx
and import the
StateContext
and the
fetchTodos
function:
import { StateContext } from './StateContext.js'
import { fetchTodos } from './api.js'
-
Modify the
Appclass code by adding a constructor to set the initial state:
export class App extends React.Component {
constructor(props) {
super(props)
this.state = { todos: [], filteredTodos: [], filter: 'all' }
this.loadTodos = this.loadTodos.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)
}
componentDidMount() {
this.loadTodos()
}
async loadTodos() {
const todos = await fetchTodos()
this.setState({ todos })
this.filterTodos()
}
addTodo(title) {
const { todos } = this.state
const newTodo = { id: Date.now(), title, completed: false }
this.setState({ todos: [newTodo, ...todos] })
this.filterTodos()
}
toggleTodo(id) {
const { todos } = this.state
const updatedTodos = todos.map(item => {
if (item.id === id) {
return { ...item, completed: !item.completed }
}
return item
})
this.setState({ todos: updatedTodos })
this.filterTodos()
}
removeTodo(id) {
const { todos } = this.state
const updatedTodos = todos.filter((item) => item.id !== id)
this.setState({ todos: updatedTodos })
this.filterTodos()
}
applyFilter(todos, filter) {
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
}
}
filterTodos(filterArg) {
this.setState(({ todos, filter }) => {
const newFilter = filterArg ?? filter
return {
filter: newFilter,
filteredTodos: this.applyFilter(todos, newFilter),
}
})
}
render() {
const { filter, filteredTodos } = this.state
return (
<StateContext.Provider value={filteredTodos}>
<div style={{ width: '400px' }}>
<Header />
<AddTodo addTodo={this.addTodo} />
<hr />
<TodoList toggleTodo={this.toggleTodo} removeTodo={this.removeTodo} />
<hr />
<TodoFilter filter={filter} filterTodos={this.filterTodos} />
</div>
</StateContext.Provider>
)
}
}
Making the AddTodo Component Dynamic
After making the
App
component dynamic, we’ll make the
AddTodo
component dynamic.
1. Edit
src/AddTodo.jsx
and define a constructor to set the initial state for the input field:
export class AddTodo extends React.Component {
constructor(props) {
super(props)
this.state = {
input: '',
}
this.handleInput = this.handleInput.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleInput(e) {
this.setState({ input: e.target.value })
}
handleSubmit(e) {
e.preventDefault()
const { input } = this.state
const { addTodo } = this.props
if (input) {
addTodo(input)
this.setState({ input: '' })
}
}
render() {
const { input } = this.state
return (
<form onSubmit={this.handleSubmit}>
<input
type='text'
placeholder='enter new task...'
style={{ width: '350px' }}
value={input}
onChange={this.handleInput}
/>
<input
type='submit'
style={{ float: 'right' }}
value='add'
disabled={!input}
/>
</form>
)
}
}
Making the TodoList Component Dynamic
The
TodoList
component needs to get the todo items from the
StateContext
.
1. Edit
src/TodoList.jsx
and import the
StateContext
:
import { StateContext } from './StateContext.js'
-
Set the
contextTypeto theStateContext:
export class TodoList extends React.Component {
static contextType = StateContext
render() {
const items = this.context
return items.map((item) => (
<TodoItem {...item} {...this.props} key={item.id} />
))
}
}
Making the TodoItem Component Dynamic
We’ll make the
TodoItem
component dynamic by implementing the
toggleTodo
and
removeTodo
features.
1. Edit
src/TodoItem.jsx
and define handler methods for the
toggleTodo
and
removeTodo
functions:
export class TodoItem extends React.Component {
constructor(props) {
super(props)
this.handleToggle = this.handleToggle.bind(this)
this.handleRemove = this.handleRemove.bind(this)
}
handleToggle() {
const { toggleTodo, id } = this.props
toggleTodo(id)
}
handleRemove() {
const { removeTodo, id } = this.props
removeTodo(id)
}
render() {
const { title, completed } = this.props
return (
<div style={{ width: '400px', height: '25px' }}>
<input
type='checkbox'
checked={completed}
onChange={this.handleToggle}
/>
{title}
<button
type='button'
style={{ float: 'right' }}
onClick={this.handleRemove}
>
x
</button>
</div>
)
}
}
Making the TodoFilter Component Dynamic
Finally, we’ll make the
TodoFilter
component dynamic to filter the todo item list.
1. Edit
src/TodoFilter.jsx
and pass all props down to the
TodoFilterItem
component in the
TodoFilter
class component:
export class TodoFilter extends React.Component {
render() {
return (
<div>
<TodoFilterItem {...this.props} name='all' />
<TodoFilterItem {...this.props} name='active' />
<TodoFilterItem {...this.props} name='completed' />
</div>
)
}
}
-
Add a
handleFiltermethod to theTodoFilterItemclass component:
export class TodoFilterItem extends React.Component {
constructor(props) {
super(props)
this.handleFilter = this.handleFilter.bind(this)
}
handleFilter() {
const { name, filterTodos } = this.props
filterTodos(name)
}
render() {
const { name, filter = 'all' } = this.props
return (
<button type='button' disabled={filter === name} onClick={this.handleFilter}>
{name}
</button>
)
}
}
Starting the Dynamic App
After making all the components dynamic, we can start the app again:
$ npm run dev
Open the URL in a browser, and you can now add, toggle, remove, and filter todos as expected.
Migrating to React Hooks
Now that our app works well with React class components, we can learn how to migrate it to React Hooks. We’ll show how to migrate side effects, such as fetching todos when the component mounts, and migrate the state management to a Hook-based solution. The example code for this migration can be found in the
Chapter13/Chapter13_1
folder. Check the
README.md
file inside the folder for instructions on how to set up and run the example.
3. Understanding the Need for Migration to React Hooks
Before we start the actual migration process, it’s important to understand why we might want to migrate from React class components to React Hooks. Here are some key advantages of using React Hooks:
| Advantages of React Hooks | Explanation |
|---|---|
| Simpler State Management | Hooks allow us to manage state in functional components without the need for a class, reducing boilerplate code. |
| Easier Side - Effect Handling |
With hooks like
useEffect
, we can handle side - effects such as data fetching, subscriptions, and DOM manipulations in a more straightforward way.
|
| Code Reusability | Hooks make it easier to reuse stateful logic across different components. |
| Readability and Maintainability | Functional components with hooks are generally more concise and easier to understand compared to class components. |
Migrating Side Effects with
useEffect
When using class components, we rely on lifecycle methods like
componentDidMount
,
componentDidUpdate
, and
componentWillUnmount
to handle side effects. With React Hooks, we can use the
useEffect
hook to achieve the same functionality.
Let’s take the
loadTodos
functionality in the
App
component as an example. In the class component, we used
componentDidMount
to load todos:
componentDidMount() {
this.loadTodos()
}
async loadTodos() {
const todos = await fetchTodos()
this.setState({ todos })
this.filterTodos()
}
To migrate this to a functional component using hooks, we can use the
useEffect
hook:
import React, { useEffect, useState } from 'react'
import { fetchTodos } from './api.js'
import { StateContext } from './StateContext.js'
const App = () => {
const [todos, setTodos] = useState([])
const [filteredTodos, setFilteredTodos] = useState([])
const [filter, setFilter] = useState('all')
useEffect(() => {
const loadTodos = async () => {
const fetchedTodos = await fetchTodos()
setTodos(fetchedTodos)
filterTodos()
}
loadTodos()
}, [])
const filterTodos = (filterArg) => {
const newFilter = filterArg ?? filter
const filtered = applyFilter(todos, newFilter)
setFilteredTodos(filtered)
setFilter(newFilter)
}
const applyFilter = (todos, filter) => {
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
}
}
return (
<StateContext.Provider value={filteredTodos}>
<div style={{ width: '400px' }}>
{/* Components */}
</div>
</StateContext.Provider>
)
}
export default App
The
useEffect
hook takes a function as its first argument and an array of dependencies as its second argument. In this case, the empty array
[]
means that the effect will only run once, similar to
componentDidMount
in a class component.
Migrating State Management with
useState
In class components, we use
this.state
and
this.setState
to manage state. With React Hooks, we can use the
useState
hook.
Let’s take the
AddTodo
component as an example. In the class component, we had:
export class AddTodo extends React.Component {
constructor(props) {
super(props)
this.state = {
input: '',
}
this.handleInput = this.handleInput.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleInput(e) {
this.setState({ input: e.target.value })
}
handleSubmit(e) {
e.preventDefault()
const { input } = this.state
const { addTodo } = this.props
if (input) {
addTodo(input)
this.setState({ input: '' })
}
}
render() {
const { input } = this.state
return (
<form onSubmit={this.handleSubmit}>
<input
type='text'
placeholder='enter new task...'
style={{ width: '350px' }}
value={input}
onChange={this.handleInput}
/>
<input
type='submit'
style={{ float: 'right' }}
value='add'
disabled={!input}
/>
</form>
)
}
}
To migrate this to a functional component using hooks:
import React, { useState } from 'react'
const AddTodo = ({ addTodo }) => {
const [input, setInput] = useState('')
const handleInput = (e) => {
setInput(e.target.value)
}
const 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>
)
}
export default AddTodo
4. Step - by - Step Migration Process
The migration process from class components to functional components with hooks can be broken down into the following steps:
graph TD;
A[Start with Class Components] --> B[Identify State and Side - Effects];
B --> C[Convert Class Components to Functional Components];
C --> D[Replace State Management with useState];
D --> E[Replace Lifecycle Methods with useEffect];
E --> F[Test and Refactor];
Identify State and Side - Effects
The first step is to identify all the state variables and side - effects in each class component. For example, in the
App
component, we had state variables like
todos
,
filteredTodos
, and
filter
, and side - effects like fetching todos.
Convert Class Components to Functional Components
Change the class component syntax to a functional component syntax. For example, the
App
class component:
export class App extends React.Component {
// Class methods and state
}
Becomes a functional component:
const App = () => {
// Functional component logic
}
Replace State Management with
useState
As shown in the
AddTodo
component example above, replace
this.state
and
this.setState
with the
useState
hook.
Replace Lifecycle Methods with
useEffect
Replace lifecycle methods like
componentDidMount
,
componentDidUpdate
, and
componentWillUnmount
with the
useEffect
hook.
Test and Refactor
After making the necessary changes, thoroughly test the application to ensure that all functionality works as expected. Refactor the code to improve readability and maintainability.
5. Conclusion
Migrating a React todo app from class components to React Hooks can bring many benefits, including simpler state management, easier side - effect handling, and improved code reusability. By following the steps outlined in this guide, you can smoothly transition your existing class - based React application to a more modern and efficient hook - based architecture.
Remember to test your application at each stage of the migration process to catch any issues early. With React Hooks, you can write more concise and maintainable code, making your development process more enjoyable and productive.
超级会员免费看
405

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



