从「组件化思维」到「单向数据流」一文吃透 React 入门核心 技术栈React 19·Vite·JavaScript (JSX) 目标通过一个 Todo List 应用理解 React 最重要的几个心智模型 目录 一、组件化思维把界面拆成积木 二、先画组件树再写代码⚛️ 三、JSX用 JavaScript 写界面 四、useState让组件「记住」状态 五、单向数据流props 向下事件向上️ 六、从零搭建项目 七、完整代码逐行拆解 八、数据流全景图 九、三条必须刻进脑子里的规则 一、组件化思维把界面拆成积木写 React 的第一步不是打开编辑器敲代码而是把界面拆成一块块积木。看看我们的 Todo 应用它长这样┌───────────────────────────────────────┐ │ My Todo List ← 标题 │ ├───────────────────────────────────────┤ │ [ 输入框............ ] [Add] ← 输入区 │ ├───────────────────────────────────────┤ │ ☑ 吃饭 │ │ ☐ 睡觉 │ │ ☑ 打豆豆 [删除] ← 列表区 │ ├───────────────────────────────────────┤ │ Total:3 Active:1 Completed:2 │ │ [Clear Completed] ← 统计区 │ └───────────────────────────────────────┘肉眼可见地能拆成4 块独立零件零件职责对应文件App顶层容器持有所有数据App.jsx⌨️TodoInput输入框 添加按钮TodoInput.jsxTodoList渲染列表、勾选、删除TodoList.jsxTodoStats统计数字 清除已完成TodoStats.jsx组件化的本质每个组件只关心自己那一亩三分地像搭乐高一样组合起来。好处是——可复用、可测试、可维护还特别适合Vibe Coding你告诉 AI 要哪些积木它帮你拼。 二、先画组件树再写代码组件之间不是平铺的而是一棵树。画清楚这棵树代码结构就出来了┌──────────┐ │ App │ ← 树根唯一持有 todos 数据 └────┬─────┘ ┌──────────────┼────────────────┐ ▼ ▼ ▼ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ TodoInput │ │ TodoList │ │ TodoStats │ │ 子组件① │ │ 子组件② │ │ 子组件③ │ └────────────┘ └────────────┘ └────────────┘这棵树决定了项目的目录结构todos/ ├── index.html # 页面骨架只有 div idroot ├── package.json # 依赖清单 ├── vite.config.js # Vite 配置 └── src/ ├── main.jsx # 入口把 App 挂到 #root ├── App.jsx # 根组件父组件 ├── App.css # 全局样式 ├── index.css # 设计变量CSS 变量 主题 └── components/ ├── Todoinput.jsx # 输入组件 ├── TodoList.jsx # 列表组件 └── TodoStats.jsx # 统计组件先规划组件树再动手是高效开发的秘诀。组件树的形状直接决定了「数据该放哪、怎么流动」。⚛️ 三、JSX用 JavaScript 写界面React 里写界面用的是JSX——一种「看起来像 HTML其实是 JavaScript」的语法糖。// 这就是 JSXHTML 直接写在 JS 里还可以内嵌表达式 const name 打豆豆; const el h1今天要{name} /h1; // {} 里可以塞任何 JS 表达式JSX 的几条小规则️标签必须闭合input /、img /这类自闭合标签也要写斜杠。必须有唯一根节点返回多个兄弟节点时用.../这种Fragment片段包起来不生成多余 DOM。属性改用驼峰class→classNameonclick→onClick。注释写法不同{/* 这是 JSX 注释 */}。 四、useState让组件「记住」状态React 的响应式靠的是「状态」state。而useState就是 React 官方给的「记忆钩子」。import { useState } from react; // 解构出两个东西[当前值, 修改函数] const [inputValue, setInputValue] useState(); // ↑ 读 ↑ 写 ↑ 初始值两条铁律永远不要直接改状态——inputValue xxx是无效的必须调用setInputValue(xxx)。永远返回全新的数据—— React 靠「新旧引用是否变化」来判断要不要重新渲染所以要map/filter/展开运算符造一份新数组而不是原地push。看App.jsx里的例子const toggleTodo (id) { // ✅ 用 map 生成一个「全新的数组」返回新对象绝不原地修改 setTodos(todos.map(todo todo.id id ? { ...todo, completed: !todo.completed } : todo )); }; README 里提到的「懒执行」和useEffect是 useState 的进阶用法懒执行懒初始化useState(expensiveCalc)每次都算而useState(() expensiveCalc())只在首次渲染算一次适合昂贵计算。useEffect处理「副作用」如请求数据、操作 DOM、订阅在渲染后执行。 五、单向数据流props 向下事件向上这是本文最重要的概念。React 数据流是单向的数据props向下 传递 ┌──────────────────────┐ │ App 父组件 │ ← 唯一的「数据源」 todos └──────────────────────┘ ↑ ↑ 修改请求事件 修改请求事件 ┌──────────┐ ┌──────────┐ │ 子组件① │ │ 子组件② │ ← 只读 props不碰父组件数据 └──────────┘ └──────────┘核心规则这条必须背下来子组件不可以直接修改父组件的数据状态。✅ 只能通过props 传入的自定义事件「通知」父组件父组件修改后子组件自动更新。这就是 README 里写的「props 向父组件申请修改状态」——子组件「打报告」父组件「做决定」。拆解一个完整的往返 ① 父组件把数据和「操作函数」一起传下去TodoList todos{todos} // 传数据 onToggle{toggleTodo} // 传「事件回调」 onDelete{deleteTodo} /② 子组件只读数据用户操作时调用回调// TodoList.jsx const TodoList ({ todos, onToggle, onDelete }) ( ul {todos.map(todo ( li key{todo.id} className{todo.completed ? completed : } {/* 勾选不自己改而是通知父组件 */} input typecheckbox checked{todo.completed} onChange{() onToggle(todo.id)} // 打报告 / span{todo.text}/span {/* 删除同样打报告 */} button onClick{() onDelete(todo.id)}删除/button /li ))} /ul );③ 父组件收到报告修改自己的状态子组件自动刷新。为什么要这么绕因为「数据和界面是统一的」——所有数据只在父组件这一处存谁改都必须走同一扇门界面才不会出现「数据对不上」的诡异 bug。这就是单一数据源Single Source of Truth。️ 六、从零搭建项目用 Vite 一键创建秒级启动# 1. 创建项目Vite 脚手架npmcreate vitelatest todos ----templatereact# 2. 进入目录、装依赖cdtodosnpminstall# 3. 启动开发服务器 npmrun devpackage.json里的关键脚本{scripts:{dev:vite,// 开发热更新build:vite build,// 打包产出可部署的静态文件preview:vite preview,// 预览打包结果lint:eslint .// 代码规范检查}}入口文件main.jsx—— 一切的起点import { StrictMode } from react; import { createRoot } from react-dom/client; import ./index.css; import App from ./App.jsx; // 把 App / 挂载到 index.html 里的 div idroot createRoot(document.getElementById(root)).render( StrictMode App / /StrictMode, );StrictMode是 React 的「严格模式」开发时帮你发现潜在问题比如不纯的渲染生产环境无影响。 七、完整代码逐行拆解1️⃣ App.jsx —— 大脑 import { useState } from react; import TodoInput from ./components/Todoinput; import TodoList from ./components/TodoList; import TodoStats from ./components/TodoStats; import ./App.css; const App () { // 唯一的「数据源」todos 数组父组件独占 const [todos, setTodos] useState([ { id: 1, text: 吃饭, completed: false }, { id: 2, text: 睡觉, completed: false }, { id: 3, text: 打豆豆, completed: true }, ]); // ➕ 添加新项放最前面用展开运算符生成全新数组 const addTodo (text) { if (text.trim() ) return; // 空内容直接忽略 setTodos([ { id: Date.now(), text, completed: false }, // id 用时间戳保证唯一 ...todos, ]); }; // ☑️ 切换完成状态map 出一个新数组只改目标项 const toggleTodo (id) { setTodos(todos.map(todo todo.id id ? { ...todo, completed: !todo.completed } : todo )); }; // ️ 删除filter 保留非目标项 const deleteTodo (id) { setTodos(todos.filter(todo todo.id ! id)); }; // 清除已完成 const clearCompleted () { setTodos(todos.filter(todo !todo.completed)); }; // 派生数据不需要 useState直接从 todos 算出来 const activeCount todos.filter(todo !todo.completed).length; const completedCount todos.length - activeCount; return ( div h1My Todo List/h1 {/* ⬇️ 数据 回调一起传给子组件 */} TodoInput onAdd{addTodo} / TodoList todos{todos} onToggle{toggleTodo} onDelete{deleteTodo} / TodoStats total{todos.length} active{activeCount} completed{completedCount} onClearCompleted{clearCompleted} / /div ); }; export default App;派生数据是 React 的重要技巧activeCount/completedCount不用额外useState存直接从todos计算即可——单一数据源永远不冗余。2️⃣ TodoInput.jsx —— 输入组件 ⌨️import { useState } from react; // 接收父组件传来的 onAdd 回调 const TodoInput ({ onAdd }) { // 私有状态输入框的文字只属于这个组件自己 const [inputValue, setInputValue] useState(); const handleSubmit (e) { e.preventDefault(); // 阻止表单默认刷新页面 onAdd(inputValue); // 把文字「报告」给父组件 setInputValue(); // 清空输入框 }; return ( form classNametodo-input onSubmit{handleSubmit} {/* 受控组件value 绑 stateonChange 同步 state */} input typetext value{inputValue} onChange{(e) setInputValue(e.target.value)} placeholderWhat needs to be done? autoFocus / button typesubmitAdd/button /form ); }; export default TodoInput; 这个输入框是典型的**「受控组件」Controlled Component**value由 state 决定onChange更新 state形成一个闭环。输入框的「私有状态」和父组件的「共享状态」泾渭分明——共享状态只有父组件持有。3️⃣ TodoList.jsx —— 列表组件 // 纯展示组件只读 props所有修改都通过回调上报 const TodoList ({ todos, onToggle, onDelete }) { return ( ul classNametodo-list {todos.length 0 ? ( li classNameemptyNo todos yet!/li // 空状态兜底 ) : ( todos.map(todo ( li key{todo.id} // ⚠️ 列表必须给 key className{todo.completed ? completed : } label input typecheckbox checked{todo.completed} onChange{() onToggle(todo.id)} // 上报切换 / span{todo.text}/span /label button onClick{() onDelete(todo.id)}删除/button /li )) )} /ul ); }; export default TodoList;⚠️key是列表渲染的必需品React 靠它区分每一项缺少或用下标index会导致错乱。这里用唯一的id正确 ✅。4️⃣ TodoStats.jsx —— 统计组件 // 接收统计数字 清除回调 const TodoStats ({ total, active, completed, onClearCompleted }) { return ( div classNametodo-stats p Total:{total} Active:{active} Completed:{completed} /p {/* 条件渲染只有存在已完成项时才显示按钮 */} {completed 0 ( button onClick{onClearCompleted} // 上报清除 classNameclear-completed Clear Completed /button )} /div ); }; export default TodoStats;{completed 0 button/}是 React 最常用的条件渲染写法左边为真才渲染右边。5️⃣ 样式 —— 让完成项「划掉」/* App.css */*{margin:0;padding:0;}/* 已完成的 todo 加删除线 */.completed{text-decoration:line-through;}index.css用CSS 变量 深色模式做了一套设计系统:root{--text:#6b6375;--bg:#fff;--accent:#aa3bff;/* 主题紫色 */--border:#e5e4e7;/* ... 更多变量 */color-scheme:light dark;/* 声明支持明暗主题 */}/* 跟随系统深色模式自动切换 */media(prefers-color-scheme:dark){:root{--text:#9ca3af;--bg:#16171d;--accent:#c084fc;/* ... */}} 八、数据流全景图把整件事串起来一个 Todo 的「一生」是这样的用户输入 学习 React → 敲回车 │ ▼ TodoInputonAdd(学习 React) 子组件打报告 │ ▼ App.addTodosetTodos([新项, ...旧项]) 父组件更新状态唯一数据源 │ ▼ React 检测到状态变化 → 重新渲染 App 及其子树 │ ├─▶ TodoList 收到新 todos → 列表多了一项 ✅ └─▶ TodoStats 收到新 total → 数字 1 ✅一句话总结状态在父组件事件往上走数据往下流改动永远走 setXxx。 九、三条必须刻进脑子里的规则子组件不改父组件状态—— 只能通过 props 里的回调函数「申请」由父组件执行。状态不可变更新—— 永远用map/filter/...造新数据绝不原地改。单一数据源—— 共享数据只存一份在父组件派生数据直接算不额外复制。 结语一个看似简单的 Todo List其实浓缩了 React 最核心的心智模型组件化—— 拆积木组件树—— 先规划后编码⚛️JSX—— 用 JS 写界面useState—— 状态即记忆单向数据流—— props 向下、事件向上掌握了这些你已经拿到了进入 React 世界的钥匙 ️。下一步可以试着给这个应用加上useEffect把 todos 存进 localStorage实现刷新不丢数据或者用useReducer把四个操作函数收敛成一个 reducer——那又是另一段精彩的旅程了。 本文代码来自本项目源码核心概念参考项目README.md的梳理。