React组件通信:从零搭建TodoList的积木式思维

26 阅读5分钟

React组件通信:从零搭建TodoList的积木式思维

"组件通信不是魔法,而是积木的拼接艺术。" —— 一个被React代码淹没的前端工程师

一、为什么我们需要组件通信?

想象一下,你正在搭建一个乐高城堡。城堡的每个部分(墙、塔、门)都是独立的模块,但它们必须通过某种方式连接在一起,才能构成一个完整的城堡。在React中,组件就是这些"乐高模块",而组件通信就是连接它们的"乐高连接器"。

今天,我们将通过一个简单的TodoList应用,深入理解React组件通信的核心原理。让我们一起用积木思维搭建这个应用,看看如何实现"父组件持有数据,子组件请求修改"的模式。

核心概念:数据流的"单行道"

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

  1. 父组件持有数据状态(useState
  2. 父组件通过props将数据和修改数据的方法传递给子组件
  3. 子组件不能直接修改父组件的数据,只能通过回调函数通知父组件
  4. 父组件收到通知后,用新状态替换旧状态(使用...展开运算符或filter等方法)

为什么不能直接修改?因为React的虚拟DOM需要知道"哪里变化了",直接修改原始数据会导致React无法追踪变化,破坏应用的响应性。

二、App.jsx:积木的"城堡地基"

import { useState, useEffect } from 'react';
import './styles/app.styl';
import TodoList from './components/TodoList';
import TodoInput from './components/TodoInput';
import TodoStats from './components/TodoStats';

function App() {
  // 父组件持有数据状态
  const [todos, setTodos] = useState([]);

  // 添加任务:使用...展开创建新数组
  const addTodo = (text) => {
    setTodos([...todos, { id: Date.now(), text, completed: false }]);
  };

  // 删除任务:使用filter筛选出不匹配ID的任务
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // 切换任务状态:使用map遍历并更新特定任务
  const toggleTodo = (id) => {
    setTodos(todos.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  // 清除已完成:使用filter筛选出未完成的任务
  const clearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed));
  };

  // 计算统计信息
  const activeCount = todos.filter(todo => !todo.completed).length;
  const completedCount = todos.filter(todo => todo.completed).length;

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

export default App;

关键点解析

  1. 状态持有者:App组件是整个应用的"地基",它使用useState管理todos状态。
  2. 方法传递:将修改状态的方法(如addTododeleteTodo等)作为props传递给子组件。
  3. 计算属性activeCountcompletedCount是基于状态计算得出的派生数据。

为什么这样设计?
这是React的核心理念:单一数据源。所有状态都由父组件持有,子组件只能请求修改,不能直接操作。这确保了状态的可预测性和可维护性。

三、TodoInput.jsx:添加待办事项的"输入口"

import { useState } from 'react'

const TodoInput = (props) => {
  const { onAdd } = props;
  const [inputValue, setInputValue] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault(); // 阻止表单默认提交行为
    onAdd(inputValue);
    setInputValue('');
  }

  return (
    <form className="todo-input" onSubmit={handleSubmit}>
      <input 
        type="text" 
        value={inputValue} 
        onChange={e => setInputValue(e.target.value)} 
      />
      <button type="submit">Add</button>
    </form>
  )
}

export default TodoInput

关键点解析

  1. 单向数据流:React不支持双向绑定(如Vue的v-model),而是通过valueonChange实现单向数据流。
  2. preventDefaulte.preventDefault()阻止了表单的默认提交行为(页面刷新),这是处理表单的关键。
  3. 子组件请求:当用户提交表单时,子组件调用父组件传递的onAdd方法,而不是直接修改状态。

为什么用preventDefault?
浏览器默认会提交表单并刷新页面,preventDefault阻止了这一行为,让React可以处理表单数据,避免页面刷新。

四、TodoList.jsx:待办事项的"展示与操作区"

const TodoList = (props) => {
  const { todos, onDelete, onToggle } = props;

  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)}>X</button>
          </li>
        ))
      )}
    </ul>
  )
}

export default TodoList

关键点解析

  1. 列表渲染:使用map遍历todos数组,为每个待办事项生成列表项。
  2. 条件渲染todos.length === 0时显示空列表提示。
  3. 状态展示className={todo.completed ? 'completed' : ''}根据完成状态应用CSS类。
  4. 事件处理onChangeonClick触发子组件请求,调用父组件传递的onToggleonDelete方法。

为什么用filter?
filter方法创建新数组,保留满足条件的元素。例如,todos.filter(todo => !todo.completed)会创建一个不包含已完成事项的新数组,用于计算活跃项数量。

五、TodoStats.jsx:统计信息的"仪表盘"

const TodoStats = (props) => {
  const { total, active, completed, onClearCompleted } = props

  return (
    <div className="todo-stats">
      <p>Total: {total} | Active: {active} | Completed: {completed}</p>
      {completed > 0 && (
        <button onClick={onClearCompleted} className="clear-btn">
          Clear Completed
        </button>
      )}
    </div>
  )
}

export default TodoStats

关键点解析

  1. 条件渲染{completed > 0 && (...)}确保只有当有已完成事项时才显示"Clear Completed"按钮。
  2. 按钮功能:点击按钮触发onClearCompleted方法,清空已完成的待办事项。

六、组件通信的"积木式"思维

让我们用"积木"的比喻来理解组件通信:

  1. 父组件是"城堡地基" :持有所有数据和修改数据的方法。

  2. 子组件是"乐高积木" :每个积木只能通过"连接器"(props)与地基通信。

  3. 通信方式

    • 父→子:通过props传递数据和方法
    • 子→父:子组件调用父组件传递的方法,请求修改数据

为什么不能子组件直接修改父组件状态?
这是React的核心设计原则:单向数据流。如果子组件能直接修改父组件状态,会导致状态管理混乱,难以追踪数据变化。通过"请求-响应"模式,我们可以清晰地知道状态变化的来源。

七、总结:组件通信的"积木哲学"

  1. 单一数据源:所有状态由父组件持有
  2. 单向数据流:父→子通过props传递数据和方法
  3. 请求-响应模式:子组件请求修改,父组件响应修改
  4. 不可变性:通过创建新状态而非修改原状态,确保状态可预测

在TodoList应用中,我们看到了这种模式的完美体现:

  • App(父)持有todos状态
  • TodoInput(子)请求添加新待办事项
  • TodoList(子)请求删除或标记完成
  • TodoStats(子)请求清除已完成事项

组件通信不是复杂的魔法,而是清晰的积木拼接。 通过"父组件持有数据,子组件请求修改"的模式,我们构建了一个可维护、可预测、易于调试的应用。