从零搭建 React Todo 应用:掌握组件化、状态管理与数据驱动

文章目录

    • 一、引言
    • [二、React 核心知识](#二、React 核心知识)
      • [2.1 组件化](#2.1 组件化)
      • [2.2 响应式与数据驱动](#2.2 响应式与数据驱动)
      • [2.3 JSX](#2.3 JSX)
      • [2.4 Fragment 组件](#2.4 Fragment 组件)
      • [2.5 Hooks](#2.5 Hooks)
        • [2.5.1 useState](#2.5.1 useState)
        • [2.5.2 懒执行](#2.5.2 懒执行)
        • [2.5.3 useEffect](#2.5.3 useEffect)
      • [2.6 组件通信](#2.6 组件通信)
        • [2.6.1 父子组件通信](#2.6.1 父子组件通信)
        • [2.6.2 组件树](#2.6.2 组件树)
    • 三、开发流程和思路
      • [3.1 组件思想:拿到需求先拆组件](#3.1 组件思想:拿到需求先拆组件)
      • [3.2 规划组件树:目录结构决定协作效率](#3.2 规划组件树:目录结构决定协作效率)
      • [3.3 父子组件通信实战](#3.3 父子组件通信实战)
        • [3.3.1 数据模型设计](#3.3.1 数据模型设计)
        • [3.3.2 TodoInput:输入与新增](#3.3.2 TodoInput:输入与新增)
        • [3.3.3 TodoList:展示、切换与删除](#3.3.3 TodoList:展示、切换与删除)
        • [3.3.4 TodoStatus:统计与批量清除](#3.3.4 TodoStatus:统计与批量清除)
        • [3.3.5 App:状态管理的核心](#3.3.5 App:状态管理的核心)
        • [3.3.6 main.jsx:应用入口](#3.3.6 main.jsx:应用入口)
    • 四、数据流动全景图
    • 五、全文总结
    • 六、核心知识点复盘
    • [七、常见问题 / 避坑指南](#七、常见问题 / 避坑指南)

一、引言

Todo 应用是前端学习路上绕不开的经典练手项目。它麻雀虽小、五脏俱全------涵盖了组件化状态管理数据驱动父子通信等 React 最核心的概念。本文将以一个完整的 Todo 应用为主线,从概念到实战,层层递进地讲解 React 的必备知识。

读完本文,你将能够:

  • 理解 React 组件化的本质
  • 掌握 useState 的用法与"懒执行"机制
  • 搞懂"数据驱动视图"到底是什么意思
  • 独立完成一个带增删改查的 Todo 应用

二、React 核心知识

2.1 组件化

是什么?

组件化就是把一个页面拆成一个个独立、可复用的小块(组件),每个组件负责自己的结构和行为。就像搭乐高------每个积木块各司其职,拼在一起就是一个完整的作品。

为什么要组件化?

  • 可维护:页面出问题时,能快速定位到具体组件,不用在几千行代码里大海捞针。
  • 可复用:一个按钮组件写好了,整个项目到处都能用。
  • 分工协作:不同的人可以并行开发不同组件,互不干扰。

在本文的 Todo 应用中,我们把页面拆成了三个组件:

组件 职责
TodoInput 输入框 + 添加按钮,负责"新增"交互
TodoList 展示列表,负责"展示"和"勾选/删除"
TodoStatus 底部统计栏,负责"计数"和"清除已完成"

核心原则:一个组件只做一件事。 不要把输入框、列表、统计栏写在一个大组件里,否则逻辑纠缠在一起,改一处牵动全身。

2.2 响应式与数据驱动

这两个概念经常一起出现,但各有侧重。

响应式

响应式的意思是:数据变了,界面自动跟着变,不需要手动操作 DOM。

传统 jQuery 的开发模式是"命令式"的------你要手动找到 DOM 节点,然后 .text().html() 去改它。React 完全不同:你只管改数据,React 帮你更新界面。

js 复制代码
// jQuery 时代:手动改 DOM
$('#count').text(5);

// React:改数据,界面自动更新
setCount(5);
数据驱动(单向数据绑定)

数据驱动指界面由数据决定。只要数据一致,渲染出来的界面就一致。

在 React 中,数据流是单向的:

复制代码
数据(state) → 视图(JSX) → 用户操作 → 更新数据 → 视图自动更新

这个单向流动让数据变化可追踪、可预测 ,是 React 应用稳定性的基石。与之对比,Vue 的双向绑定(v-model)虽然写起来方便,但数据流在复杂场景下可能变得难以追踪。

一句话总结:React 中,UI = f(state)。界面就是数据的函数,同样的输入必然产生同样的输出。

2.3 JSX

JSX 看起来像 HTML,但本质上是 JavaScript 的语法糖。

jsx 复制代码
// JSX 写法
<h1 className="title">Hello</h1>

// Babel 编译后等价于:
React.createElement('h1', { className: 'title' }, 'Hello');

关键规则:

  1. 必须有一个根节点(或使用 Fragment,后面会讲)。
  2. {} 里可以写任何 JS 表达式 ,包括三元运算、map、函数调用等。
  3. class 要写成 classNamefor 要写成 htmlFor------因为 classfor 是 JS 的保留字。
  4. 布尔值、nullundefined 不会被渲染,可以利用这个特性做条件渲染。
jsx 复制代码
// 条件渲染示例:completed > 0 时才显示按钮
{completed > 0 && (
  <button onClick={onClearCompleted}>Clear Completed</button>
)}

2.4 Fragment 组件

前面提到 JSX 必须有一个根节点。如果你不想在 DOM 中多套一层 <div>,就用 Fragment:

html 复制代码
// ❌ 多余的 div 包裹,影响 CSS 布局
return (
  <div>
    <h1>Title</h1>
    <p>Content</p>
  </div>
);

// ✅ Fragment 不会在 DOM 中生成任何节点
import { Fragment } from 'react';

return (
  <Fragment>
    <h1>Title</h1>
    <p>Content</p>
  </Fragment>
);

// ✅ 更简洁的短语法(无需 import)
return (
  <>
    <h1>Title</h1>
    <p>Content</p>
  </>
);

在 Todo 应用中,App 组件的 return 就用 <div> 包裹了三个子组件。如果你想用 Fragment 替换,也是完全可以的------这取决于你的 CSS 布局是否依赖外层容器。

2.5 Hooks

Hooks 是 React 16.8 引入的能力,让函数组件也能拥有状态和生命周期。本文重点讲 useState,后续会补充 useEffect 的场景。

2.5.1 useState

useState 是函数组件的"记忆体"------它让组件能记住数据,并且数据变化时自动重新渲染。

js 复制代码
const [state, setState] = useState(初始值);
  • state:当前状态值(只读,不能直接修改)。
  • setState:更新状态的函数(调用它才会触发重新渲染)。
  • 初始值:组件首次渲染时的默认值。

关键规则:

  1. 不可变更新(Immutable Update) :必须用 setState 传入一个全新的值/对象/数组,不能直接修改原状态。
js 复制代码
// ❌ 错误:直接 push 不会触发渲染
todos.push(newTodo);

// ✅ 正确:用展开运算符创建新数组
setTodos([...todos, newTodo]);
  1. 状态更新是异步的 :在同一个事件处理函数中连续调用 setState,React 会批量处理,只触发一次渲染。

  2. 用函数式更新避免闭包陷阱:当新状态依赖旧状态时,推荐传函数而非直接传值。

js 复制代码
// ❌ 连续调用可能读到旧的 count
setCount(count + 1);
setCount(count + 1); // 两次调用都基于同一个旧值

// ✅ 函数式更新确保每次拿到最新值
setCount(prev => prev + 1);
setCount(prev => prev + 1);

在 Todo 应用中,每个增删改操作都严格遵循了不可变原则:

js 复制代码
// 添加:展开旧数组 + 新对象
setTodos([...todos, { id: +Date.now(), text, completed: false }]);

// 修改:map 返回新数组,匹配的项用展开创建新对象
setTodos(todos.map(todo =>
  todo.id === id ? { ...todo, completed: !todo.completed } : todo
));

// 删除:filter 返回新数组
setTodos(todos.filter(todo => todo.id !== id));

为什么 { ...todo, completed: !todo.completed } 没有两个 completed

对象属性名是唯一的。展开 ...todo 先把 completed: false 写入,紧接着 completed: !todo.completed 用新值覆盖 了它。最终只有一个 completed,值是新算出来的那个。这是 React 里切换布尔状态的经典写法。

2.5.2 懒执行

useState 的初始值只在组件首次渲染时 计算一次。如果初始值需要复杂计算(比如从 localStorage 读取并解析 JSON),直接写在 useState() 里会在每次渲染时都执行计算(虽然结果只取第一次)。

js 复制代码
// ❌ 每次渲染都会执行 JSON.parse(浪费性能)
const [todos, setTodos] = useState(
  JSON.parse(localStorage.getItem('todos')) || []
);

// ✅ 传函数:只在首次渲染时执行一次
const [todos, setTodos] = useState(() => {
  const saved = localStorage.getItem('todos');
  return saved ? JSON.parse(saved) : [];
});

这就是 懒初始化(Lazy Initialization) ------传一个函数给 useState,React 只在首次渲染时调用它,后续渲染直接跳过。

适用场景: 初始值计算成本较高(如 localStorage 读写、复杂数据转换)时才需要用懒初始化。像 useState(0)useState('') 这种简单值,完全不需要。

2.5.3 useEffect

虽然本文的 Todo 应用没有用到 useEffect,但这是一个必须了解的核心 Hook。

useEffect 让你在函数组件中执行副作用------即那些不能直接在渲染阶段做的事,比如:请求 API 数据、操作 DOM、设置定时器、订阅事件。

js 复制代码
useEffect(() => {
  // 副作用逻辑(组件渲染后执行)
  console.log('组件挂载或更新了');

  return () => {
    // 清理函数(组件卸载前 / 下次 effect 执行前运行)
    console.log('清理工作');
  };
}, [依赖项]); // 依赖项数组

依赖项数组控制 effect 的触发时机:

依赖项 触发时机
[] 空数组 只在组件挂载 时执行一次(类似 componentDidMount
[a, b] 挂载时 + 每次 ab 变化时执行
不传第二个参数 每次渲染后都执行(很少用,容易造成死循环)

常见踩坑: setState → 组件重渲染 → useEffect 再次触发 → 又 setState → 死循环。务必正确设置依赖项。

2.6 组件通信

2.6.1 父子组件通信

这是 React 中最基础、最重要的通信模式。记住两条铁律:

  1. 父 → 子:通过 props 传递数据。

  2. 子 → 父:子组件不能直接修改父组件的状态。 只能通过父组件传下来的回调函数来"报告"父组件,让父组件自己决定怎么改。

    父组件 App(持有 todos 状态)

    ├── props(数据下发)→ TodoList(展示列表)
    ├── props(数据下发)→ TodoStatus(展示统计)

    └── props(回调下发)→ TodoInput(用户输入后回调 addTodo)

为什么子组件不能直接改父组件的状态?

这是 React 的单向数据流 原则。如果每个子组件都能随意修改父组件的数据,数据变化就变得不可追踪。统一由父组件管理状态,确保了数据流是可预测的

在 Todo 应用中,整个通信关系非常清晰:

jsx 复制代码
// App.jsx ------ 父组件持有状态,下发数据和回调
const App = () => {
  const [todos, setTodos] = useState([...]);

  const addTodo = (text) => { /* 添加逻辑 */ };
  const toggleTodo = (id) => { /* 切换完成状态 */ };
  const deleteTodo = (id) => { /* 删除逻辑 */ };
  const clearCompleted = () => { /* 清除已完成 */ };

  return (
    <div>
      {/* TodoInput 只拿回调,不需要数据 */}
      <TodoInput onAdd={addTodo} />

      {/* TodoList 拿数据 + 回调 */}
      <TodoList
        todos={todos}
        onToggle={toggleTodo}
        onDelete={deleteTodo}
      />

      {/* TodoStatus 拿统计数据 + 回调 */}
      <TodoStatus
        total={todos.length}
        active={activeCount}
        completed={completedCount}
        onClearCompleted={clearCompleted}
      />
    </div>
  );
};
2.6.2 组件树

组件树就是把组件之间的嵌套关系画成一棵树。理解组件树对后续学习很重要,因为:

  • 状态提升:多个子组件需要共享同一份数据时,把状态放到它们最近的共同父组件上。
  • props drilling:当组件层级很深时,props 一层层传递会很痛苦------这是后续学习 Context 或状态管理库的动机。
  • 调试:React DevTools 就是按组件树展示的。

本文 Todo 应用的组件树:

复制代码
App
├── TodoInput
├── TodoList
└── TodoStatus

层级很浅,不需要 Context。当你的组件树长到 5 层、10 层时,自然会感受到为什么要学 Context API。


三、开发流程和思路

3.1 组件思想:拿到需求先拆组件

拿到一个 Todo 应用的需求,不要一上来就写代码。先思考:

  1. 页面上有哪些"块"? → 输入区、列表区、状态区。
  2. 每个块需要什么数据? → 列表需要 todos 数组,状态区需要计数。
  3. 数据存在哪里? → 三个组件都需要 todos,所以状态放在共同父组件 App 中------这叫状态提升

这个"拆 → 分析 → 决定数据归属"的思考流程,就是组件化开发的核心思维。

3.2 规划组件树:目录结构决定协作效率

合理的目录结构不仅让自己思路清晰,在团队协作中(尤其是使用 AI 辅助编码时)也能让 AI 更准确地定位文件、理解项目上下文。

复制代码
src/
├── App.jsx                    # 根组件,管理全局状态
├── App.css                    # 根组件样式
├── main.jsx                   # 应用入口
├── index.css                  # 全局样式
└── components/
    ├── TodoInput.jsx           # 输入组件
    ├── TodoList.jsx            # 列表组件
    └── TodoStatus.jsx          # 状态栏组件

命名建议: 组件文件用 PascalCase(大驼峰),一眼就知道这是组件;工具函数文件用 camelCase(小驼峰)。

3.3 父子组件通信实战

3.3.1 数据模型设计

todos 是一个 JSON 数组,每个元素包含三个字段:

js 复制代码
{
  id: 1,           // 唯一标识,用于 React key 和增删改定位
  text: '吃饭',     // todo 文本内容
  completed: false  // 是否完成
}

这个数据模型是 App 组件的唯一数据源(Single Source of Truth) ,所有子组件展示的内容都派生于此。确保数据和界面完全对应 ------如果界面上显示 3 条,那么 todos.length 也一定是 3,绝不对不上。

3.3.2 TodoInput:输入与新增
javascript 复制代码
// components/TodoInput.jsx
import { useState } from 'react';

const TodoInput = ({ onAdd }) => {
  // 输入框的值是"局部状态"------只有这个组件自己关心
  const [inputValue, setInputValue] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();            // 阻止表单默认提交(页面刷新)
    if (inputValue.trim() === '') return; // 空内容不添加
    onAdd(inputValue);             // 通知父组件"用户要添加这条内容"
    setInputValue('');             // 清空输入框
  };

  return (
    <form className="todo-input" onSubmit={handleSubmit}>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="What needs to be done?"
        autoFocus
      />
      <button type="submit">Add</button>
    </form>
  );
};

export default TodoInput;

关键点解析:

  • inputValue局部状态 ------只有 TodoInput 自己需要,父组件完全不关心用户打了什么字,只关心最终提交的内容。
  • 受控组件模式:<input>value 绑定到 state,onChange 更新 state。React 成为数据的"唯一真实来源",输入框的值完全由 React 控制。
  • e.preventDefault():表单内的按钮默认 type="submit" 会触发页面刷新,必须阻止。
3.3.3 TodoList:展示、切换与删除
javascript 复制代码
// components/TodoList.jsx
const TodoList = ({ todos, onToggle, onDelete }) => {
  return (
    <ul className="todo-list">
      {todos.length === 0 ? (
        <li className="empty">No todos yet!</li>
      ) : (
        todos.map(todo => (
          <li key={todo.id} className={todo.completed ? 'completed' : ''}>
            <label>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => onToggle(todo.id)}
              />
              <span>{todo.text}</span>
            </label>
            <button onClick={() => onDelete(todo.id)}>删除</button>
          </li>
        ))
      )}
    </ul>
  );
};

export default TodoList;

关键点解析:

  • key={todo.id}:React 用 key 来追踪列表中的每一项,提高 diff 效率。永远不要用数组索引当 key,否则在列表增删时可能导致渲染错乱。
  • todo.completed ? 'completed' : '':通过类名切换样式,展示已完成/未完成的不同外观。
  • onChange={() => onToggle(todo.id)}:这里不能用 onChange={onToggle(todo.id)}------后者会在渲染时立即执行,而不是等用户点击。用箭头函数包装一层,延迟到事件触发时才调用。

⚠️ 经典陷阱:箭头函数 {} vs ()

jsx 复制代码
// ❌ 错误:{} 是函数体,没有 return 语句 → map 返回 undefined
todos.map(todo => {
  <li key={todo.id}>{todo.text}</li>
});

// ✅ 正确:() 包裹 JSX,隐式返回
todos.map(todo => (
  <li key={todo.id}>{todo.text}</li>
));

// ✅ 也可以:{} 配合显式 return
todos.map(todo => {
  return <li key={todo.id}>{todo.text}</li>;
});

这是新手写 React 最容易犯的错误之一。当你用 {} 写箭头函数体时,JSX 只是一条"孤立的表达式语句",不会被返回。必须用 () 包起来实现隐式返回,或者写 return

3.3.4 TodoStatus:统计与批量清除
javascript 复制代码
// components/TodoStatus.jsx
const TodoStatus = ({
  total,
  active,
  completed,
  onClearCompleted
}) => {
  return (
    <div className="todo-status">
      <p>Total: {total} | Active: {active} | Completed: {completed}</p>
      {completed > 0 && (
        <button className="clear-btn" onClick={onClearCompleted}>
          Clear Completed
        </button>
      )}
    </div>
  );
};

export default TodoStatus;

关键点解析:

  • {completed > 0 && <button>...</button>}:利用短路求值做条件渲染。只有当 completed > 0 时按钮才显示,否则渲染 false(React 会忽略)。
  • TodoStatus 不接收完整的 todos 数组,只接收它需要的派生数据totalactivecompleted)。这是"最小权限原则"在组件 props 设计中的体现------组件只知道它需要知道的。
3.3.5 App:状态管理的核心
javascript 复制代码
// App.jsx
import { useState } from 'react';
import TodoInput from './components/TodoInput';
import TodoList from './components/TodoList';
import TodoStatus from './components/TodoStatus';
import './App.css';

const App = () => {
  // 所有共享状态由父组件统一管理
  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([...todos, { id: +Date.now(), text, completed: false }]);
  };

  // 切换完成状态:map 返回新数组,匹配项创建新对象
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  // 删除:用 filter 排除指定 id
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // 清除已完成:保留 completed 为 false 的项
  const clearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed));
  };

  // 派生数据:不需要额外的 state,直接从 todos 计算
  const activeCount = todos.filter(todo => !todo.completed).length;
  const completedCount = todos.length - activeCount;

  return (
    <div>
      <h1>My Todo List</h1>
      <TodoInput onAdd={addTodo} />
      <TodoList
        todos={todos}
        onToggle={toggleTodo}
        onDelete={deleteTodo}
      />
      <TodoStatus
        total={todos.length}
        active={activeCount}
        completed={completedCount}
        onClearCompleted={clearCompleted}
      />
    </div>
  );
};

export default App;

核心思路复盘:

  1. 状态提升todos 放在 App 中,因为三个子组件都需要它。
  2. 派生数据不存 stateactiveCountcompletedCount 直接从 todos 计算即可。如果你把它们也存成 state,每次修改 todos 时都要同步更新它们------数据多了一份,bug 也多了一个来源。
  3. 不可变更新贯穿始终mapfilter、展开运算符 ... 都返回新数组/新对象,从来不会直接修改原数据。
3.3.6 main.jsx:应用入口
js 复制代码
// main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

这里主要关注 <StrictMode>------它是 React 的开发模式组件,不会影响生产构建,但在开发时会:

  • 检测意外的副作用(故意执行两次组件渲染函数)。
  • 检测过时的 API 使用。
  • 在控制台给出更明确的警告信息。

如果你在开发时发现 console.log 被打印了两次,别慌------这是 StrictMode 干的,不是 bug。


四、数据流动全景图

把整个应用的数据流动串起来,形成一张清晰的"数据回路":

复制代码
用户输入文本 → TodoInput 调用 onAdd(text)
                  ↓
      App.addTodo 执行 → setTodos 更新状态
                  ↓
      React 检测到 todos 变化 → 重新渲染 App
                  ↓
      App 将新的 todos 通过 props 下发
        ↙          ↓           ↘
  TodoInput    TodoList    TodoStatus
(无影响)   (列表刷新)   (统计刷新)

整个流程是单向的、可预测的 ------数据从 App 流出,经过各个子组件展示,用户操作通过回调回到 AppApp 修改状态后又重新流向子组件。这个循环就是 React 应用的最基本运行模型。


五、全文总结

本文以 Todo 应用为载体,完整走通了 React 开发的一个标准流程:

  1. 先思考后动手:拆组件 → 定数据归属 → 设计通信关系。
  2. 状态提升:多个组件共享的数据放到最近的共同父组件。
  3. 不可变更新 :永远通过 setState 传新值,不直接修改原状态。
  4. 单向数据流:数据从父组件通过 props 流到子组件;子组件通过回调把"意图"传回父组件。

六、核心知识点复盘

知识点 一句话总结
组件化 拆成独立小块,各司其职,可复用可维护
JSX HTML 写在 JS 里,最终编译成 createElement 调用
单向数据流 数据从父到子,不可反向直接修改
useState 函数组件的"记忆",改数据触发重渲染
不可变更新 map/filter/... 创建新值,不动原值
懒初始化 useState(() => heavyCalc()) 避免重复计算
父子通信 父传 props,子传回调,数据始终由父管理
派生数据 能从 state 算出来的值,不要再存一个 state
key 列表渲染的稳定标识,不能用索引
Fragment 不产生 DOM 节点的包裹元素,<>...</>

七、常见问题 / 避坑指南

  1. 为什么我改了数据,界面不更新?

原因几乎都是:直接修改了原状态,没有用 setState。

js 复制代码
// ❌ 直接修改,React 不知道数据变了
todos.push(newTodo);

// ✅ 创建新数组,React 检测到引用变化,触发渲染
setTodos([...todos, newTodo]);

React 用 Object.is 比较新旧状态------数组引用没变,它就认为没变。请务必使用不可变更新的方式。

  1. map 里写了 JSX 但页面上什么也不显示?

检查箭头函数是否用了 {} 而不是 ()

jsx 复制代码
// ❌ {} 没有 return,每个元素返回 undefined
todos.map(todo => { <li>...</li> })

// ✅ () 隐式返回 JSX
todos.map(todo => ( <li>...</li> ))
  1. 用了索引当 key 会怎样?
jsx 复制代码
// ❌ 用索引当 key
todos.map((todo, index) => <li key={index}>...</li>)

列表增删时,索引会变化,React 可能复用了错误的 DOM 节点,导致输入框内容错位、动画异常等问题。始终用稳定且唯一的值当 key (如 todo.id)。

  1. setState 后立即读取 state 还是旧值
js 复制代码
const [count, setCount] = useState(0);
const handleClick = () => {
  setCount(count + 1);
  console.log(count); // 还是 0!不是 1!
};

状态更新是异步的,在新一次渲染之前,count 的值不会变。如果需要基于新值做操作,用函数式更新或 useEffect 监听。

  1. 表单提交时页面刷新了

检查是否在 handleSubmit 中调用了 e.preventDefault()。表单内按钮的默认 typesubmit,点击会触发页面刷新。

  1. StrictMode 下 console.log 打印两次

正常现象。StrictMode 在开发模式下会故意重复调用组件函数来帮你发现副作用问题,打包到生产环境后不会有这个行为。


本文基于 React 18 + Vite 构建的 Todo 应用实战编写,完整覆盖了组件化、状态管理、数据驱动和父子通信等核心概念,适合 React 初学者作为系统性学习参考。

相关推荐
Hilaku1 小时前
工作 5 年后,决定你薪资上限的究竟是什么?
前端·javascript·程序员
Revolution611 小时前
页面更新后为什么出现 Loading chunk failed:旧页面如何请求了已删除的构建产物
前端·面试·前端工程化
JavaGuide1 小时前
GitHub 9.8 万 Star!把整个代码仓库变成知识图谱,这个 AI Coding 工具太适合 Claude Code / Codex 了
前端·后端·ai编程
hunterandroid2 小时前
[鸿蒙从零到一] HarmonyOS 通知与提醒实战:消息发布、点击跳转与定时触达
前端
Lxinz2 小时前
vscode调试ts代码思路
前端
极梦网络无忧2 小时前
real-ai-editor:一款轻量、智能的纯前端 AI 富文本与 Markdown 编辑器
前端·人工智能·编辑器
ClickHouseDB2 小时前
ClickHouse托管Postgres:OLTP+OLAP,新能力解锁最佳数据平台
java·前端·数据库
gis开发之家2 小时前
《Vue3 从入门到大神40篇》Vue3 源码详解(十):diff 算法全解析 —— 为什么 Vue3 比 Vue2 更快?
javascript·算法·typescript·前端框架·vue3·vue3源码
technology_x3 小时前
2026年财务报表分析软件测评:兼容与安全解析
java·服务器·前端
程序员鱼皮3 小时前
Claude Opus 5 全新发布,7 大项目实测,夯还是拉?半价吊打 Fable 5?
前端·后端·ai编程