React组件化开发:从零构建一个TodoList应用

58 阅读8分钟

前言

在现代前端开发中,React以其组件化思想和数据驱动理念成为了最受欢迎的框架之一。今天,我将带大家通过一个TodoList应用的开发过程,深入理解React的核心概念和开发模式。不同于传统的DOM操作,React让我们能够专注于业务逻辑,而非繁琐的界面更新。

ps:今天讲的主要是逻辑,应用页面可能丑点,多担待一些,完成后jym可以根据自己的喜好做点好看的渲染。

一、项目初始化与工程化

首先,我们需要使用Vite来搭建React项目。Vite是新一代的前端构建工具,相比传统的Webpack,它提供了更快的启动速度和热更新体验。

npm create vite@latest todoListComponent --template react
cd todoListComponent
npm install
npm run dev

Vite就像建筑工地上的塔吊和搅拌机,为我们处理了项目构建、模块打包、热更新等工程化问题,让我们可以专注于业务代码的编写。

二、组件化思想解析

什么是组件?

组件是React应用的基本构建块,它将相关的HTML、CSS和JavaScript逻辑组合在一起,形成一个独立的、可复用的功能单元。就像乐高积木一样,我们可以通过组合不同的组件来构建复杂的用户界面。

为什么需要组件化?

  1. 关注点分离:每个组件只关注自己的功能和样式
  2. 可复用性:相同的组件可以在不同地方重复使用
  3. 维护性:组件独立,修改一个组件不会影响其他部分
  4. 协作开发:不同开发者可以同时开发不同组件

完整项目展示:

屏幕录制 2025-06-08 124044.gif

额外小知识

在讲解应用之前我想先分享一下我学习时遇到的小bug,或者应该叫useState()的机制。

以下是原码,App 只调用这个组件:

// 内置的hooks 函数
import{useState} from 'react'//导入useState钩子函数,用于管理组件状态
import './Todo.css'
import TodoFrom from './TodoFrom'
import Todos from './Todos'
function TodoList(){
    const [title,setTitle]=useState('Todo List')
    const [todos,setTodos]=useState([
        {
            id:1,
            text:'1',
            completed:false
        }
    ])

     setTimeout(()=>{
         setTodos([...todos,{id:2,text:'2',completed:false}])
         setTitle('Todo List 2')
     },3000)
    return(
        <div className='container'>
            <h1 className='title'>{ title } </h1>
            {    
                todos.map(todo=>(
                    <li>{todo.text}</li>
                )) 
            } 
        </div>
    )
}

export default TodoList;// 导出组件

屏幕录制 2025-06-08 124629.gif

为什么会这样呢?我们问问Trae:

image.png

原来组件每次更新或渲染时会重新执行整个函数体,setTimeout要慎用。

好,你每次都会重新执行函数是吧:

// 内置的hooks 函数
import{useState} from 'react'//导入useState钩子函数,用于管理组件状态
import './Todo.css'
import TodoFrom from './TodoFrom'
import Todos from './Todos'
function TodoList(){
   
    const [num,setNum] = useState(0)

    return(
        <div className='container'>
            <p>num:{num}</p>
            <button onClick={()=>setNum(num+1)}>+</button>
        </div>
    )
}

export default TodoList;// 导出组件

???什么意思,我的num初始值不是0吗?你怎么加到2的?如果每次刷新不应该一直是0+1吗?

image.png

再次询问Trae:

image.png

哦,原来useState 的初始值仅在组件首次渲染时生效,后续渲染会保留状态的最新值,所以 num 不会在重新渲染时变回初始值 0

理解完成以上内容,我们再看看下面代码能发现什么?

// 内置的hooks 函数
import{useState} from 'react'//导入useState钩子函数,用于管理组件状态
import './Todo.css'
import TodoFrom from './TodoFrom'
import Todos from './Todos'
function TodoList(){
   
   const [num,setNum] = useState(0)
    const now = function(){
        setTimeout(()=>{
            console.log(num) 
        },3000)
    }

    return(
        <div className='container'>
            <p>num:{num}</p>
            <div>
                <button onClick={() => setNum(num + 1)}>+</button>
            </div>
            
            <button onClick={now}>nowState</button>
        </div>
    )
}

export default TodoList;// 导出组件

这里我是先点击+,再点击nowState,3 秒后输出1,确实没问题。但是我第二次是先点击nowState再点击+,3 秒后输出还是1,也就是说num确实更新了,但是更新的东西这次的setTimeout捕获不到。

得出结论:React 的状态更新是异步的,当调用 setNum 时,组件不会立即重新渲染,状态也不会马上更新。这意味着在 setTimeout 回调函数执行时,它获取到的 num 是旧状态值。

image.png

小结:react确实难学,一个useState就有这么多细化的知识点,而且我还没有全部探索完成。让我们继续构建应用吧。

三、TodoList组件拆分

让我们看看如何将一个TodoList应用拆分为合理的组件结构:

src/
├── components/
│   ├── TodoForm.jsx  // 输入表单组件
│   ├── TodoList.jsx  // 主容器组件
│   └── Todos.jsx     // 列表展示组件
├── App.jsx           // 根组件
└── Todo.css          // 样式文件

1. TodoList主组件

作为容器组件,它管理着整个应用的状态和数据流:

import { useState } from 'react'
import './Todo.css'
import TodoForm from './TodoForm'
import Todos from './Todos'

function TodoList() {
    const [title, setTitle] = useState('Todo List')
    const [todos, setTodos] = useState([
        {
            id: 1,
            text: '1',
            completed: false
        }
    ])

    const handleAdd = (text) => {
        setTodos([
            ...todos,
            {
                id: todos.length + 1,
                text: text,
                completed: false
            }
        ])
    }

    return (
        <div className='container'>
            <h1 className='title'>{title}</h1>
            <TodoForm onAdd={handleAdd} />
            <Todos todos={todos} />
        </div>
    )
}

export default TodoList

这个组件使用了React的useState钩子来管理三个状态:

  • title:应用标题
  • todos:待办事项列表
  • handleAdd:添加新待办事项的方法

2. TodoForm表单组件

一开始我的TodoFrom代码:

import { useState } from 'react'

function TodoForm() {
    const [text, setText] = useState('')
    
    const handleSubmit = (e) => {
        e.preventDefault()
        console.log(e.target[0].value)
    }

    return (
        <form onSubmit={handleSubmit}>
            <input 
                type="text" 
                placeholder="请输入待办事项:" 
                value={text} 
            />
            <button type="submit">添加</button>
        </form>
    )
}

export default TodoForm

这里我并没有添加onChange处理程序,所以无论我怎么动输入框都无法改变值,我没有让其响应我的输入数据。

image.png

所以找Trae 改改:

import { useState } from 'react'
function TodoFrom() {

    const [text,setText] = useState('2')
    // 处理输入框内容变化的函数
    const handleChange = (e) => {
        setText(e.target.value);
    }
    const handleSubmit = (e) =>{
        // 阻止默认行为
        // 由js 来控制
        e.preventDefault();// event api 阻止默认行为
        console.log(e.target[0].value)
        
    }

    return (
        <form action="http://www.baidu.com" onSubmit={handleSubmit}>
            {/* 添加 onChange 事件处理函数 */}
            <input type="text" placeholder="请输入待办事项:" value={text} onChange={handleChange}/>
            <button type="submit">添加</button>
        </form>
    )
}

export default TodoFrom;

但是还是有问题,它只会在输出面板输出该值,而不是加载到页面上,我总不能让用户自己点击检查来看看加载了没吧 。 屏幕录制 2025-06-08 140736.gif

页面展示不用多说,一定是要和TodoList交互的,那这里就需要用到props了。我们看看Trae的解释:它是一种从父组件向子组件传递信息的机制,让子组件能根据接收到的数据进行不同的渲染或执行相应的逻辑。

哦,所以TodoFrom只管收集数据,剩下的展示交给TodoList即可。

import { useState } from 'react'

function TodoForm(props) {
    const [text, setText] = useState('')
    const onAdd = props.onAdd
    
    const handleSubmit = (e) => {
        e.preventDefault()
        onAdd(text)
        setText('')
    }
    
    const handleChange = (e) => {
        setText(e.target.value)
    }
    
    return (
        <form onSubmit={handleSubmit}>
            <input 
                type="text" 
                placeholder="请输入待办事项:" 
                value={text} 
                onChange={handleChange}
            />
            <button type="submit">添加</button>
        </form>
    )
}

export default TodoForm

3. Todos列表组件

负责渲染待办事项列表:

function Todos(props) {
    const todos = props.todos
    
    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id}>{todo.text}</li>
            ))}
        </ul>
    )
}

export default Todos

四、数据驱动与响应式更新

React最强大的特性之一就是它的响应式数据绑定。我们不再需要手动操作DOM来更新界面,只需要关心数据状态的变化。

数据状态管理

TodoList组件中,我们使用useState来声明和管理状态:

const [todos, setTodos] = useState([
    {
        id: 1,
        text: '1',
        completed: false
    }
])

当调用setTodos更新状态时,React会自动比较新旧状态的差异,并高效地更新DOM。

数据流向

React中的数据流动是单向的,从父组件流向子组件:

  1. TodoList组件通过todos={todos}将数据传递给Todos组件
  2. TodoList组件通过onAdd={handleAdd}将添加方法传递给TodoForm组件
  3. 当用户在TodoForm中输入并提交时,调用onAdd方法更新父组件的状态

这种单向数据流使得应用的状态变化更加可预测和易于调试。

五、组件通信模式

在这个TodoList应用中,我们看到了几种常见的组件通信方式:

  1. 父传子:通过props传递数据(如todos传递给Todos组件)
  2. 子传父:通过回调函数(如onAdd方法传递给TodoForm
  3. 兄弟组件通信:通过共同的父组件(TodoFormTodos通过TodoList通信)

对于更复杂的应用,我们可能会使用Context API或状态管理库如Redux。

六、React开发的核心思维

通过这个TodoList应用的开发,我们可以总结出React开发的几个核心思维:

  1. 组件化思维:将UI拆分为独立、可复用的组件
  2. 数据驱动:关注数据状态而非DOM操作
  3. 单向数据流:数据从父组件流向子组件
  4. 声明式编程:描述"UI应该是什么样子",而非"如何更新UI"

结语

React的组件化开发模式彻底改变了前端开发的方式。通过这个TodoList应用的开发,我们不仅学会了如何拆分组件、管理状态,更重要的是理解了数据驱动的开发理念。记住,在React中,UI只是数据状态的映射,我们只需要关心数据的变化,React会负责高效地更新界面。

希望这篇文章能帮助你入门React开发。如果你有任何问题或想法,欢迎在评论区留言讨论。接下来,你可以尝试为这个TodoList添加更多功能,比如编辑待办事项、添加分类标签等,来进一步巩固你的React技能。