面向 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 应用
我们要实现:添加、删除、切换完成、过滤(全部/未完成/已完成)、本地持久化、统计。涵盖:useState、useEffect、自定义 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,试着自己给它加功能,巩固所学:
- 编辑功能:双击某项进入编辑态(练受控组件 + 状态切换);
- 拖拽排序 :引入
dnd-kit(练第三方库集成); - 接后端:用 TanStack Query 替换 localStorage(练服务端状态,第 11 篇);
- 多页面:加一个"统计页",用 React Router(第 10 篇);
- 主题切换:用 Context 做全局暗色模式(第 8 篇);
- 开启 React Compiler:体验自动优化(第 12 篇)。
五、给 Vue 开发者的最后建议
- 别用 Vue 思维硬套:接受"重渲染 + 不可变 + 手动依赖"这套心智,很多"别扭"会消失。
- 善用 TypeScript:React + TS 是现代标配,类型能帮你少踩很多 props/state 的坑。
- 必装
eslint-plugin-react-hooks:它替代了 Vue 自动依赖追踪,是你最好的搭档。 - 先写正确,再谈优化 :别一上来就
useMemo/useCallback满天飞,React 19 Compiler 会帮你。 - 多读官方文档 :react.dev 的新版教程质量极高,很多例子就是"从直觉出发"讲的。
- 善用生态: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 就真正是你的了。祝编码愉快 🚀