从 Vue3 到 React · 快速上手系列第 13 篇:工程化与综合实战(Vite + Todo 应用)

面向 Vue3 开发者的 React 快速上手系列 · 收官

理论讲完了,这一篇动手。我们用你熟悉的 Vite 从零搭一个 React 项目,写一个功能完整的 Todo 应用,把前 11 篇的知识点全部串起来。写完这个,你就具备独立开发 React 应用的能力了。


一、用 Vite 创建 React 项目

Vite 你已经很熟了,创建 React 项目命令几乎一样:

bash 复制代码
# 创建项目(选 React + TypeScript 模板)
npm create vite@latest my-react-app -- --template react-ts
# 或纯 JS:--template react

cd my-react-app
npm install
npm run dev

项目结构对照

perl 复制代码
my-react-app/
├── index.html            # 入口 HTML(和 Vue 项目一样)
├── vite.config.ts        # Vite 配置(和 Vue 一样)
├── src/
│   ├── main.tsx          # 入口,挂载根组件(对标 Vue 的 main.ts)
│   ├── App.tsx           # 根组件(对标 App.vue)
│   ├── components/       # 组件目录
│   └── hooks/            # 自定义 Hook(对标 composables/)
└── package.json

入口文件对比

js 复制代码
// Vue3 main.ts
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
tsx 复制代码
// React main.tsx
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)

二、实战:Todo 应用

我们要实现:添加、删除、切换完成、过滤(全部/未完成/已完成)、本地持久化、统计。涵盖:useStateuseEffect、自定义 Hook、受控表单、条件/列表渲染、组件通信、不可变更新------全系列精华。

第 1 步:自定义 Hook useTodos(对标 Composable)

把所有 Todo 逻辑抽进一个 Hook,组件只负责渲染。这正是第 9 篇的思路。

tsx 复制代码
// src/hooks/useTodos.ts
import { useState, useEffect } from 'react'

export interface Todo {
  id: number
  text: string
  done: boolean
}

export function useTodos() {
  // 惰性初始化:从 localStorage 读取(第 6 篇技巧)
  const [todos, setTodos] = useState<Todo[]>(() => {
    const saved = localStorage.getItem('todos')
    return saved ? JSON.parse(saved) : []
  })

  // 持久化:todos 变化时写入 localStorage(第 5 篇 useEffect)
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos))
  }, [todos])

  // 增:不可变更新(第 6 篇)
  const addTodo = (text: string) => {
    if (!text.trim()) return
    setTodos(prev => [...prev, { id: Date.now(), text: text.trim(), done: false }])
  }

  // 删
  const removeTodo = (id: number) => {
    setTodos(prev => prev.filter(t => t.id !== id))
  }

  // 切换完成
  const toggleTodo = (id: number) => {
    setTodos(prev => prev.map(t =>
      t.id === id ? { ...t, done: !t.done } : t
    ))
  }

  return { todos, addTodo, removeTodo, toggleTodo }
}

第 2 步:输入组件(受控表单,第 5 篇)

tsx 复制代码
// src/components/TodoInput.tsx
import { useState } from 'react'

interface Props {
  onAdd: (text: string) => void   // 回调 prop:子传父(第 8 篇)
}

export function TodoInput({ onAdd }: Props) {
  const [text, setText] = useState('')

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()        // 阻止表单默认刷新
    onAdd(text)
    setText('')               // 清空输入
  }

  return (
    <form onSubmit={handleSubmit} className="todo-input">
      <input
        value={text}                                   // 受控组件
        onChange={e => setText(e.target.value)}
        placeholder="添加一个待办..."
      />
      <button type="submit">添加</button>
    </form>
  )
}

第 3 步:单项组件(列表项)

tsx 复制代码
// src/components/TodoItem.tsx
import type { Todo } from '../hooks/useTodos'

interface Props {
  todo: Todo
  onToggle: (id: number) => void
  onRemove: (id: number) => void
}

export function TodoItem({ todo, onToggle, onRemove }: Props) {
  return (
    <li className={todo.done ? 'done' : ''}>
      <input
        type="checkbox"
        checked={todo.done}                       // 复选框用 checked
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onRemove(todo.id)}>删除</button>
    </li>
  )
}

第 4 步:根组件 App(组装一切)

tsx 复制代码
// src/App.tsx
import { useState, useMemo } from 'react'
import { useTodos } from './hooks/useTodos'
import { TodoInput } from './components/TodoInput'
import { TodoItem } from './components/TodoItem'

type Filter = 'all' | 'active' | 'done'

export default function App() {
  const { todos, addTodo, removeTodo, toggleTodo } = useTodos()
  const [filter, setFilter] = useState<Filter>('all')

  // 派生:按过滤条件筛选(第 12 篇)
  const visibleTodos = useMemo(() => {
    if (filter === 'active') return todos.filter(t => !t.done)
    if (filter === 'done')   return todos.filter(t => t.done)
    return todos
  }, [todos, filter])

  // 派生:未完成数量
  const remaining = todos.filter(t => !t.done).length

  return (
    <div className="app">
      <h1>我的待办</h1>

      <TodoInput onAdd={addTodo} />

      {/* 过滤按钮 */}
      <div className="filters">
        {(['all', 'active', 'done'] as Filter[]).map(f => (
          <button
            key={f}
            className={filter === f ? 'active' : ''}
            onClick={() => setFilter(f)}
          >
            {f === 'all' ? '全部' : f === 'active' ? '未完成' : '已完成'}
          </button>
        ))}
      </div>

      {/* 列表 + 空状态(第 4 篇条件渲染与列表渲染) */}
      {visibleTodos.length === 0 ? (
        <p className="empty">暂无待办</p>
      ) : (
        <ul className="todo-list">
          {visibleTodos.map(todo => (
            <TodoItem
              key={todo.id}                    // key 别忘
              todo={todo}
              onToggle={toggleTodo}
              onRemove={removeTodo}
            />
          ))}
        </ul>
      )}

      <footer>还有 {remaining} 项未完成</footer>
    </div>
  )
}

第 5 步:一点样式

css 复制代码
/* src/index.css */
.app { max-width: 480px; margin: 40px auto; font-family: system-ui; }
.todo-input { display: flex; gap: 8px; margin-bottom: 16px; }
.todo-input input { flex: 1; padding: 8px; }
.filters { display: flex; gap: 8px; margin-bottom: 12px; }
.filters .active { background: #3b82f6; color: #fff; }
.todo-list { list-style: none; padding: 0; }
.todo-list li { display: flex; align-items: center; gap: 8px; padding: 8px 0; }
.todo-list li.done span { text-decoration: line-through; color: #999; }
.todo-list li button { margin-left: auto; }
.empty { color: #999; text-align: center; }

三、这个应用用到的知识点回顾

知识点 出现位置 对应文章
函数组件 + JSX 所有组件 第 2、3 篇
props + 回调通信 onAdd / onToggle / onRemove 第 3 篇
条件 + 列表渲染 空状态、.map() 第 4 篇
受控表单 TodoInput 第 5 篇
useState + 不可变更新 useTodos 的增删改 第 6 篇
useEffect 持久化 localStorage 同步 第 7 篇
自定义 Hook useTodos 第 9 篇
useMemo 派生列表 visibleTodos 第 12 篇

四、进阶练习建议

学完这个 Todo,试着自己给它加功能,巩固所学:

  1. 编辑功能:双击某项进入编辑态(练受控组件 + 状态切换);
  2. 拖拽排序 :引入 dnd-kit(练第三方库集成);
  3. 接后端:用 TanStack Query 替换 localStorage(练服务端状态,第 11 篇);
  4. 多页面:加一个"统计页",用 React Router(第 10 篇);
  5. 主题切换:用 Context 做全局暗色模式(第 8 篇);
  6. 开启 React Compiler:体验自动优化(第 12 篇)。

五、给 Vue 开发者的最后建议

  1. 别用 Vue 思维硬套:接受"重渲染 + 不可变 + 手动依赖"这套心智,很多"别扭"会消失。
  2. 善用 TypeScript:React + TS 是现代标配,类型能帮你少踩很多 props/state 的坑。
  3. 必装 eslint-plugin-react-hooks:它替代了 Vue 自动依赖追踪,是你最好的搭档。
  4. 先写正确,再谈优化 :别一上来就 useMemo/useCallback 满天飞,React 19 Compiler 会帮你。
  5. 多读官方文档react.dev 的新版教程质量极高,很多例子就是"从直觉出发"讲的。
  6. 善用生态:ahooks/ReactUse(工具)、Zustand(状态)、TanStack Query(请求)、React Router(路由),别重复造轮子。

全系列回顾

# 主题 核心一句话
1 思维转变 Vue 响应式驱动,React 重渲染驱动
2 JSX 在 JS 里写 UI,{ } 放表达式
3 组件与 Props 组件是函数,emit 用回调 prop 替代
4 条件与列表 &&/三元 + .map(),别忘 key
5 事件与表单 受控组件替代 v-model
6 状态管理 useState + 不可变更新,state 是快照
7 副作用 useEffect 一统 watch/生命周期
8 组件通信 Context 对标 provide/inject
9 自定义 Hook 就是 Composable,直觉可平移
10 路由 React Router≈Vue Router
11 状态管理 Zustand≈Pinia
12 并发与性能 别滥用 memo,Compiler 会帮你
13 实战 从 Vite 到完整 Todo

恭喜你完成整个系列! 作为 Vue3 开发者,你已经具备了迁移到 React 的全部核心知识。剩下的就是------动手多写。把上面的 Todo 应用亲手敲一遍,再挑一两个进阶练习做做,React 就真正是你的了。祝编码愉快 🚀

相关推荐
晴殇i1 小时前
用 TRAE Work 搞定客户项目介绍,开发再也不用头疼写汇报 PPT
前端·后端
Cache技术分享1 小时前
488. Java 反射 - 动态创建数组
前端·后端
Synmbrf1 小时前
ECharts 饼图鼠标悬浮后标签 title 消失的问题
前端·javascript
Canace1 小时前
用 AI 写了一天代码,为什么反而更累了?
前端·人工智能·ai编程
Vhen1 小时前
Taro 封装小程序toast提示组件
前端
用户250694921611 小时前
React项目打包为.next文件部署
前端
tryCbest1 小时前
Html设置网站图标
前端·html
半个落月1 小时前
从页面跳转到前端路由:手写一个简单的 HashRouter
前端
做前端的娜娜子1 小时前
移动端上拉加载与下拉刷新实现方案
前端·面试·掘金·金石计划