20、Migrating a React Todo App from Class Components to Hooks

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
  1. Open the new Chapter13_1 folder in VS Code.
  2. Delete the current src/App.jsx file, 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'
  1. Define the App component as a class component with a render method:
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
  1. Create a new src/Header.jsx file.
  2. Import React and define the class component with a render method:
import React from 'react'
export class Header extends React.Component {
  render() {
    return <h1>ToDo</h1>
  }
}
AddTodo Component
  1. Create a new src/AddTodo.jsx file.
  2. 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
  1. Create a new src/TodoList.jsx file.
  2. Import React and the TodoItem component:
import React from 'react'
import { TodoItem } from './TodoItem.jsx'
  1. 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
  1. Create a new src/TodoItem.jsx file.
  2. 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
  1. Create a new src/TodoFilter.jsx file.
  2. Import React and define a TodoFilterItem class component:
import React from 'react'
export class TodoFilterItem extends React.Component {
  render() {
    const { name } = this.props
    return <button type='button'>{name}</button>
  }
}
  1. Define the actual TodoFilter component, which renders three TodoFilterItem components:
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'
  1. 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'
  1. Modify the App class 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'
  1. Set the contextType to the StateContext :
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>
    )
  }
}
  1. Add a handleFilter method to the TodoFilterItem class 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.

【四轴飞行器】非线性三自由度四轴飞行器模拟器研究(Matlab代码实现)内容概要:本文围绕非线性三自由度四轴飞行器的建模与仿真展开,重点介绍了基于Matlab的飞行器动力学模型构建与控制系统设计方法。通过对四轴飞行器非线性运动方程的推导,建立其在三维空间中的姿态与位置动态模型,并采用数值仿真手段实现飞行器在复杂环境下的行为模拟。文中详细阐述了系统状态方程的构建、控制输入设计以及仿真参数设置,并结合具体代码实现展示了如何对飞行器进行稳定控制与轨迹跟踪。此外,文章还提到了多种优化与控制策略的应用背景,如模型预测控制、PID控制等,突出了Matlab工具在无人机系统仿真中的强大功能。; 适合人群:具备一定自动控制理论基础和Matlab编程能力的高校学生、科研人员及从事无人机系统开发的工程师;尤其适合从事飞行器建模、控制算法研究及相关领域研究的专业人士。; 使用场景及目标:①用于四轴飞行器非线性动力学建模的教学与科研实践;②为无人机控制系统设计(如姿态控制、轨迹跟踪)提供仿真验证平台;③支持高级控制算法(如MPC、LQR、PID)的研究与对比分析; 阅读建议:建议读者结合文中提到的Matlab代码与仿真模型,动手实践飞行器建模与控制流程,重点关注动力学方程的实现与控制器参数调优,同时可拓展至多自由度或复杂环境下的飞行仿真研究。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值