阅读视图

发现新文章,点击刷新页面。

《React 入门实战:从零搭建 TodoList》

React 入门实战:从零搭建 TodoList(父子通信+本地存储+Stylus)

作为 React 入门的经典案例,TodoList 几乎涵盖了 React 基础开发中最核心的知识点——组件拆分、父子组件通信、响应式状态管理、本地存储持久化,再搭配 Stylus 预处理和 Vite 构建,既能夯实基础,又能贴近实际开发场景。

本文将基于完整可运行代码,一步步拆解 React TodoList 的实现逻辑,重点讲解父子组件通信的核心技巧、本地存储的优雅实现,以及组件化开发的最佳实践,适合 React 新手入门学习,也适合作为基础复盘素材。

一、项目环境与技术栈

先明确本次实战的技术栈组合,都是前端开发中高频使用的工具,简单易上手:

  • 构建工具:Vite(替代 Webpack,启动更快、打包更高效,适合中小型项目快速开发)
  • 核心框架:React(使用 Hooks 语法,useState 管理组件状态,useEffect 处理副作用)
  • 样式预处理:Stylus(比 CSS 更简洁,支持嵌套、变量、混合等特性,提升样式开发效率)
  • 本地存储:localStorage(实现 Todo 数据持久化,刷新页面数据不丢失)

项目初始化命令(快速搭建基础环境):

# 初始化 Vite + React 项目
npm create vite@latest react-todo-demo -- --template react
# 进入项目目录
cd react-todo-demo
# 安装依赖
npm install
# 安装 Stylus(样式预处理)
npm install stylus --save-dev
# 启动项目
npm run dev

二、项目结构与组件拆分

组件化是 React 开发的核心思想,一个清晰的项目结构能提升代码可读性和可维护性。本次 TodoList 我们拆分为 4 个核心组件,遵循「单一职责原则」,每个组件只负责自己的功能:

src/
├── components/       # 自定义组件目录
│   ├── TodoInput.js  # 输入框组件:添加新 Todo
│   ├── TodoList.js   # 列表组件:展示所有 Todo、切换完成状态、删除 Todo
│   └── TodoStats.js  # 统计组件:展示 Todo 总数、活跃数、已完成数,清空已完成
├── styles/           # 样式目录
│   └── app.styl      # 全局样式(使用 Stylus 编写)
└── App.js            # 根组件:管理全局状态、协调所有子组件

核心逻辑:根组件 App 作为「数据中心」,持有所有 Todo 数据和修改数据的方法,通过 props 将数据和方法传递给子组件;子组件不直接修改数据,只能通过父组件传递的方法提交修改请求,实现数据统一管理。

三、核心功能实现(附完整代码解析)

下面从根组件到子组件,一步步解析每个功能的实现逻辑,重点讲解父子通信、状态管理和本地存储的核心细节。

3.1 根组件 App.js:数据中心与组件协调

App 组件是整个 TodoList 的核心,负责:初始化 Todo 数据、定义修改数据的方法、监听数据变化并持久化到本地存储、传递数据和方法给子组件。

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() {
  // 1. 初始化 Todo 数据(本地存储持久化)
  // useState 高级用法:传入函数,避免每次渲染都执行 JSON.parse(性能优化)
  const [todos, setTodos] = useState(() => {
    const saved = localStorage.getItem('todos');
    // 本地存储有数据则解析,无数据则初始化为空数组
    return saved ? JSON.parse(saved) : [];
  })

  // 2. 定义修改数据的方法(供子组件调用)
  // 新增 Todo:接收子组件传递的文本,添加到 todos 数组
  const addTodo = (text) => {
    // 注意:React 状态不可直接修改,需通过扩展运算符创建新数组
    setTodos([...todos, {
      id: Date.now(), // 用时间戳作为唯一 ID,简单高效
      text,           // 子组件传入的 Todo 文本
      completed: false, // 初始状态为未完成
    }])
  }

  // 删除 Todo:接收子组件传递的 ID,过滤掉对应 Todo
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id))
  }

  // 切换 Todo 完成状态:接收 ID,修改对应 Todo 的 completed 属性
  const toggleTodo = (id) => {
    setTodos(todos.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ))
  }

  // 清空已完成 Todo:过滤掉所有 completed 为 true 的 Todo
  const clearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed))
  }

  // 3. 计算统计数据(传递给 TodoStats 组件)
  const activeCount = todos.filter(todo => !todo.completed).length; // 活跃 Todo 数
  const completedCount = todos.filter(todo => todo.completed).length; // 已完成 Todo 数

  // 4. 副作用:监听 todos 变化,持久化到本地存储
  // 依赖数组 [todos]:只有 todos 变化时,才执行该函数
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]) 

  // 5. 渲染子组件,通过 props 传递数据和方法
  return (
    My Todo List
      {/* 输入框组件:传递 addTodo 方法,用于新增 Todo */}
<TodoInput onAdd={addTodo}/>
      {/* 列表组件:传递 todos 数据,以及删除、切换状态的方法 */}
      <TodoList 
        todos={todos} 
        onDelete={deleteTodo}
        onToggle={toggleTodo}
      />
      {/* 统计组件:传递统计数据和清空方法 */}<TodoStats 
        total={todos.length}
        active={activeCount}
        completed={completedCount}
        onClearCompleted={clearCompleted}
      />
    
  )
}

export default App

关键知识点解析:

  • useState 高级用法:传入函数初始化状态,避免每次组件渲染都执行 JSON.parse,提升性能(尤其数据量大时)。
  • 状态不可变:React 状态是只读的,修改 todos 时,必须通过 filtermap、扩展运算符等方式创建新数组,不能直接修改原数组(如 todos.push(...) 是错误写法)。
  • useEffect 副作用:监听 todos 变化,将数据存入 localStorage,实现「刷新页面数据不丢失」;依赖数组 [todos] 确保只有数据变化时才执行存储操作,避免无效渲染。
  • 父子通信基础:父组件通过 props 向子组件传递数据(如 todos)和方法(如 addTodo),子组件通过调用这些方法修改父组件的状态。

3.2 子组件 1:TodoInput.js(输入框组件)

负责接收用户输入的 Todo 文本,通过父组件传递的 onAdd 方法,将文本提交给父组件,实现新增 Todo 功能。

import { useState } from 'react'

const TodoInput = (props) => {
  // 接收父组件传递的 addTodo 方法
  const { onAdd } = props;

  // 本地状态:管理输入框的值(React 单向绑定)
  const [inputValue, setInputValue] = useState('');

  // 处理表单提交:新增 Todo
  const handleSubmit = (e) => {
    e.preventDefault(); // 阻止表单默认提交行为(避免页面刷新)
    // 简单校验:输入不能为空
    if (!inputValue.trim()) return;
    // 调用父组件传递的方法,提交输入的文本
    onAdd(inputValue);
    // 清空输入框
    setInputValue('');
  }

  return (
    <form className="todo-input" onSubmit={<input 
        type="text" 
        value={绑定:输入框的值由 inputValue 控制
        onChange={e => setInputValue(e.target.value)} // 监听输入变化,更新状态
        placeholder="请输入 Todo..."
      />
      
  )
}

export default TodoInput

关键知识点解析:

  • React 单向绑定:React 不支持 Vue 中的 v-model 双向绑定(为了性能优化,避免不必要的视图更新),通过「value + onChange」实现数据与视图的同步——输入框的值由 inputValue 控制,输入变化时通过 onChange 更新 inputValue
  • 子父通信:子组件通过调用父组件传递的 onAdd 方法,将输入的文本传递给父组件,实现「子组件向父组件传递数据」(核心:父传方法,子调用方法传参)。
  • 表单校验:简单的非空校验,避免添加空 Todo,提升用户体验。

3.3 子组件 2:TodoList.js(列表组件)

负责展示所有 Todo 列表,接收父组件传递的 todos 数据,以及删除、切换完成状态的方法,实现 Todo 列表的渲染、状态切换和删除功能。

const TodoList = (props) => {
  // 接收父组件传递的数据和方法
  const { todos, onDelete, onToggle } = props;

  return (
    
      {
        // 空状态处理:没有 Todo 时显示提示
        todos.length === 0 ? (
          No todos yet!
        ) : (
          // 遍历 todos 数组,渲染每个 Todo 项
          todos.map(todo => (
            <li 
              key={唯一 key,React 用于优化渲染(避免重复渲染)
              className={todo.completed ? 'completed' : ''} // 根据完成状态添加样式
            >{todo.text}
              {/* 删除按钮:点击时调用 onDelete 方法,传递当前 Todo 的 ID */}<button onClick={ onDelete(todo.id)}>X
          ))
        )
      }
    
  )
}

export default TodoList

关键知识点解析:

  • 列表渲染:使用 map 遍历 todos 数组,渲染每个 Todo 项;必须添加 key 属性(推荐用唯一 ID),React 通过 key 识别列表项的变化,优化渲染性能。
  • 条件渲染:判断 todos 数组长度,为空时显示「No todos yet!」,提升空状态体验。
  • 状态切换与删除:复选框的 checked 属性绑定 todo.completed,点击时调用 onToggle 方法传递 Todo ID;删除按钮点击时调用 onDelete 方法传递 ID,实现子组件触发父组件数据修改。

3.4 子组件 3:TodoStats.js(统计组件)

负责展示 Todo 统计信息(总数、活跃数、已完成数),以及清空已完成 Todo 的功能,接收父组件传递的统计数据和清空方法。

const TodoStats = (props) => {
  // 接收父组件传递的统计数据和清空方法
  const { total, active, completed, onClearCompleted } = props;

  return (
    
      {/* 展示统计信息 */}
     Total: {total} | Active: {active} | Completed: {completed} {
        // 条件渲染:只有已完成数 > 0 时,显示清空按钮
        completed > 0 && (
          <button 
            onClick={            className="clear-btn"
          >Clear Completed
        )
      }
    
  )
}

export default TodoStats

关键知识点解析:

  • 条件渲染优化:只有当已完成 Todo 数大于 0 时,才显示「Clear Completed」按钮,避免按钮无效显示,提升用户体验。
  • 父子通信复用:和其他子组件一样,通过 props 接收父组件的方法(onClearCompleted),点击按钮时调用,触发父组件清空已完成 Todo 的操作。

3.5 样式文件 app.styl(Stylus 编写)

使用 Stylus 编写全局样式,利用嵌套、变量等特性,简化样式编写,提升可维护性(示例代码):

// 定义变量(可复用)
$primary-color = #42b983
$gray-color = #f5f5f5
$completed-color = #999

.todo-app
  max-width: 600px
  margin: 2rem auto
  padding: 0 1rem
  font-family: 'Arial', sans-serif

.todo-input
  display: flex
  gap: 0.5rem
  margin-bottom: 1.5rem
  input
    flex: 1
    padding: 0.5rem
    border: 1px solid #ddd
    border-radius: 4px
  button
    padding: 0.5rem 1rem
    background: $primary-color
    color: white
    border: none
    border-radius: 4px
    cursor: pointer

.todo-list
  list-style: none
  padding: 0
  margin: 0 0 1.5rem 0
  li
    display: flex
    justify-content: space-between
    align-items: center
    padding: 0.8rem
    margin-bottom: 0.5rem
    background: white
    border-radius: 4px
    box-shadow: 0 2px 4px rgba(0,0,0,0.1)
    &.completed
      span
        text-decoration: line-through
        color: $completed-color
    label
      display: flex
      align-items: center
      gap: 0.5rem
    button
      background: #ff4444
      color: white
      border: none
      border-radius: 50%
      width: 20px
      height: 20px
      display: flex
      align-items: center
      justify-content: center
      cursor: pointer
  .empty
    text-align: center
    padding: 1rem
    color: $gray-color
    font-style: italic

.todo-stats
  display: flex
  justify-content: space-between
  align-items: center
  padding: 0.8rem
  background: $gray-color
  border-radius: 4px
  .clear-btn
    padding: 0.3rem 0.8rem
    background: #ff4444
    color: white
    border: none
    border-radius: 4px
    cursor: pointer

四、核心知识点总结(重点!)

通过这个 TodoList 案例,我们掌握了 React 基础开发的核心技能,尤其是父子组件通信和状态管理,这也是 React 开发中最常用的知识点,总结如下:

4.1 父子组件通信(核心)

React 中组件通信的核心是「单向数据流」,即数据从父组件流向子组件,子组件通过调用父组件传递的方法修改数据,具体分为两种情况:

  1. 父传子:通过 props 传递数据(如 todos、total、active)和方法(如 addTodo、onDelete),子组件通过 props.xxx 接收使用。
  2. 子传父:父组件传递一个方法给子组件,子组件调用该方法时传递参数,父组件通过方法参数接收子组件的数据(如 TodoInput 传递输入文本给 App)。

4.2 兄弟组件通信(间接实现)

React 中没有直接的兄弟组件通信方式,需通过「父组件作为中间媒介」实现:

例如 TodoInput(新增 Todo)和 TodoList(展示 Todo)是兄弟组件,它们的通信流程是:TodoInput → 调用父组件 addTodo 方法传递文本 → 父组件更新 todos 状态 → 父组件通过 props 将更新后的 todos 传递给 TodoList → TodoList 重新渲染。

4.3 状态管理与本地存储

  • 使用 useState 管理组件状态,遵循「状态不可变」原则,修改状态必须通过 setXXX 方法,且不能直接修改原状态。
  • 使用 useEffect 处理副作用(如本地存储),依赖数组控制副作用的执行时机,避免无效渲染。
  • localStorage 持久化:将 todos 数据存入本地存储,页面刷新时从本地存储读取数据,实现数据不丢失(注意:localStorage 只能存储字符串,需用 JSON.stringifyJSON.parse 转换)。

4.4 组件化开发最佳实践

  • 单一职责原则:每个组件只负责一个功能(如 TodoInput 只负责输入,TodoList 只负责展示)。
  • 复用性:组件设计时尽量通用,避免硬编码(如 TodoList 不关心 Todo 的具体内容,只负责渲染和触发方法)。
  • 用户体验:添加空状态、表单校验、条件渲染等细节,提升用户使用体验。

五、最终效果与扩展方向

5.1 最终效果

  • 输入文本,点击 Add 按钮新增 Todo。
  • 点击复选框,切换 Todo 完成状态(已完成显示删除线)。
  • 点击 Todo 项右侧的 X,删除对应的 Todo。
  • 底部显示 Todo 统计信息,已完成数大于 0 时显示清空按钮。
  • 刷新页面,所有 Todo 数据不丢失(本地存储生效)。

5.2 扩展方向(进阶练习)

如果想进一步提升,可以尝试以下扩展功能,巩固 React 基础:

  • 添加 Todo 编辑功能(双击 Todo 文本可编辑)。
  • 添加筛选功能(全部、活跃、已完成)。
  • 使用 useReducer 替代 useState 管理复杂状态(适合 todos 操作较多的场景)。
  • 添加动画效果(如 Todo 新增/删除时的过渡动画)。
  • 使用 Context API 实现跨组件状态共享(替代 props 层层传递)。

六、总结

TodoList 虽然是 React 入门案例,但涵盖了 React 开发中最核心的知识点——组件拆分、父子通信、状态管理、副作用处理、本地存储,以及 Stylus 预处理和 Vite 构建的使用。

对于 React 新手来说,建议亲手敲一遍完整代码,重点理解「单向数据流」和「父子组件通信」的逻辑,再尝试扩展功能,逐步夯实 React 基础。

✨ 附:项目运行命令

# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 打包构建(部署用)
npm run build

《React Props 实战避坑:新手必看的组件通信指南》

React Props 从入门到实战:吃透组件通信的核心逻辑

作为React组件化开发的核心基石,Props(Properties)是实现组件通信、复用和灵活组合的关键。很多React新手在入门时,常常混淆Props与State的用法,也不清楚如何规范传递和校验Props。今天就结合实战代码,从基础认知到高级用法,手把手带你吃透React Props,帮你快速上手组件化开发的核心逻辑。

本文所有示例均基于真实开发场景编写,配套完整可运行代码,新手可直接复制实践,老手可快速回顾核心知识点,查漏补缺~

一、先理清核心:Props 到底是什么?

在React的组件化思想中,我们可以把组件想象成“乐高积木”——页面就是由一个个独立的“积木”拼接而成,而Props就是拼接这些积木时的“连接参数”,负责实现组件之间的数据传递。

结合我们最基础的认知,先明确两个核心概念的区别,避免混淆:

  • State:组件自身拥有的数据,可修改、可维护,是组件的“自有属性”,仅作用于当前组件内部。
  • Props:父组件传递给子组件的数据,是组件的“外部输入”,只读不可改(子组件无法修改父组件传递的Props),是父子组件通信的唯一基础方式。

简单来说:State管“自己”,Props管“传递”。组件是开发任务的最小单元,通过组件嵌套形成父子关系(比如App.jsx是“老板”,Greeting.jsx是“员工”),而Props就是“老板”给“员工”下达的“工作指令”,实现组件间的协作与数据流转。

二、Props 基础用法:从简单传递到解构赋值

Props的使用门槛极低,核心流程只有三步:父组件传递 → 子组件接收 → 子组件使用。我们结合实战代码,一步步拆解(以下代码可直接复制到项目中运行)。

2.1 基础示例:简单Props传递

首先定义子组件Greeting,接收父组件传递的name、message、showIcon三个Props,用于渲染不同的问候内容:

// src/components/Greeting.jsx
import PropTypes from 'prop-types'; // 后续用于Props校验,先导入

// 子组件接收Props(props是一个对象,包含所有父组件传递的数据)
function Greeting(props) {
  // 直接通过 props.属性名 访问传递的值
  return (
    
      {/* 根据showIcon判断是否显示图标(布尔值Props) */}
      {props.showIcon && 👋}
      Hello, {props.name}!{props.message}
  );
}

export default Greeting;

然后在父组件App中,使用Greeting组件,并传递对应的Props:

// src/App.jsx
import Greeting from './components/Greeting';

function App() {
  return (
    
      {/* 父组件传递Props:name、message是字符串,showIcon是布尔值 */}
<Greeting name="娇娇" message="欢迎加入阿里" showIcon />
      {/* 传递部分Props(未传递的后续通过默认值补充) */}<Greeting name="磊" />
    
  );
}

export default App;

这里有两个关键细节,新手必看:

  1. 布尔值Props(如showIcon):仅写属性名,等价于showIcon={true},常用于控制UI元素的显示/隐藏;
  2. Props可传递任意JS类型:字符串、数字、布尔值、对象、数组甚至函数,后续会讲解高级用法。

2.2 优化写法:解构赋值(推荐)

如果Props较多,每次都写props.属性名会很繁琐。我们可以通过ES6解构赋值,直接提取Props中的属性,代码更简洁、可读性更高:

// src/components/Greeting.jsx(优化后)
import PropTypes from 'prop-types';

function Greeting(props) {
  // 解构赋值,提取需要的Props属性
  const { name, message, showIcon } = props;
  return (
    
      {showIcon && 👋}
      Hello, {name}!{message}
  );
}

export default Greeting;

更进一步,我们可以直接在函数参数中解构Props,简化代码:

// 更简洁的写法
function Greeting({ name, message, showIcon }) {
  return (
    
      {showIcon && 👋}
      Hello, {name}!{message}
  );
}

三、Props 进阶:默认值与类型校验(提升代码健壮性)

在实际开发中,我们无法保证父组件一定会传递所有需要的Props,也无法避免传递错误类型的数据(比如本该传递字符串name,却传递了数字)。这时候,Props默认值和类型校验就显得尤为重要,能极大提升代码的健壮性和可维护性。

3.1 Props 默认值(defaultProps)

当父组件未传递某个Props时,我们可以为其设置默认值,避免页面出现undefined或异常显示。有两种常用写法,推荐第二种:

// 写法1:使用defaultProps(传统写法,兼容所有版本)
Greeting.defaultProps = {
  message: 'Welcome to ByteDance!', // 未传递message时,使用该默认值
  showIcon: false // 未传递showIcon时,默认不显示图标
};

// 写法2:解构赋值时直接设置默认值(更现代、更简洁)
function Greeting({ 
  name, 
  message = 'Welcome to ByteDance!', 
  showIcon = false 
}) {
  return (
    
      {showIcon && 👋}
      Hello, {name}!{message}
  );
}

注意:默认值仅在Props缺失或值为undefined时生效,如果父组件显式传递了null,默认值不会触发。

3.2 Props 类型校验(prop-types)

类型校验用于约束父组件传递的Props类型,比如规定name必须是字符串、且为必填项。如果传递的类型错误或缺失必填项,控制台会出现清晰的警告,方便我们快速排查问题。

使用步骤很简单,分两步:

  1. 安装prop-types包(注意:包名是prop-types,不是pro-types,很多新手会拼错导致报错);
  2. 为组件定义校验规则。
// 安装prop-types(npm或pnpm均可)
npm install prop-types
# 或
pnpm add prop-types

定义校验规则,完善Greeting组件:

// src/components/Greeting.jsx(完整带校验版本)
import PropTypes from 'prop-types';

function Greeting({ name, message = 'Welcome to ByteDance!', showIcon = false }) {
  return (
    
      {showIcon && 👋}
     Hello, {name}!{message}
  );
}

// 定义Props类型校验规则
Greeting.propTypes = {
  name: PropTypes.string.isRequired, // name是字符串,且为必填项
  message: PropTypes.string, // message是字符串(可选)
  showIcon: PropTypes.bool // showIcon是布尔值(可选)
};

export default Greeting;

常见的校验类型的:

  • PropTypes.string:字符串
  • PropTypes.number:数字
  • PropTypes.bool:布尔值
  • PropTypes.func:函数(用于传递回调)
  • PropTypes.node:可以渲染的节点(如JSX、字符串)
  • .isRequired:标记该Props为必填项

提示:prop-types仅在开发环境生效,生产环境会自动移除,不会影响项目性能,是React开发的最佳实践之一。

四、Props 高级用法:传递组件与children插槽

Props的强大之处在于,它可以传递任意JavaScript值——除了基础类型,还能传递函数、对象,甚至是整个React组件。这也是实现组件高度复用和定制化的核心技巧,我们结合Modal和Card组件实战讲解。

4.1 传递组件作为Props(实现组件定制化)

我们经常会遇到“弹窗组件”这样的场景:弹窗的头部、底部可能需要根据不同场景显示不同内容(比如有的弹窗显示“确认”按钮,有的显示“关闭”按钮)。这时候,我们可以将头部和底部组件作为Props传递给Modal组件,实现高度定制化。

// src/components/Modal.jsx(弹窗组件)
const Modal = (props) => {
  // 接收父组件传递的HeaderComponent、FooterComponent和children
  const { HeaderComponent, FooterComponent, children } = props;
  
  // CSS in JS 写法,简化样式配置
  const styles = {
    overlay: {
      backgroundColor: 'rgba(0,0,0,0.5)',
      position: 'fixed',
      top: 0,
      left: 0,
      right: 0,
      bottom: 0,
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center'
    },
    modal: {
      backgroundColor: 'white',
      padding: '1rem',
      borderRadius: '8px',
      width: '400px'
    },
    content: {
      margin: '1rem 0'
    }
  };
  
  return (<div style={<div style={}>
        {/* 渲染父组件传递的头部组件 */}
        <HeaderComponent />
        {/* 渲染弹窗主体内容(children插槽) */}
        <div style={{children}
        {/* 渲染父组件传递的底部组件 */}
        <FooterComponent />
      
  );
};

export default Modal;

然后在父组件App中,定义头部和底部组件,传递给Modal:

// src/App.jsx(使用Modal组件)
import Modal from './components/Modal';

// 定义弹窗头部组件
const MyHeader = () => {
  return <h2 style={ 0, color: 'blue' }}>自定义标题;
};

// 定义弹窗底部组件
const MyFooter = () => {
  return (
    <div style={<button 
        onClick={ alert('关闭')}
        style={{ padding: '0.5rem 1rem' }}
      >关闭
  );
};

function App() {
  return (
    
      {/* 传递组件作为Props,实现弹窗定制化 */}
      <Modal HeaderComponent={MyHeader} FooterComponent={MyFooter}>
        {/* 弹窗主体内容,会被Modal组件的children接收(插槽用法) */}
        这是一个弹窗你可以在这里显示任何JSX。</Modal>
    
  );
}

export default App;

这种写法的核心价值:Modal组件只负责“弹窗的容器和样式”,具体的头部、底部和主体内容,完全由父组件通过Props控制,实现了组件的复用和定制化分离,符合React“组合优于继承”的开发理念。

4.2 children 特殊Props(组件插槽)

children是React内置的一个特殊Props,无需父组件显式传递,它会自动接收“子组件标签之间的所有内容”,相当于一个“默认插槽”,常用于实现组件的容器化包裹。

我们以Card组件为例,讲解children的用法(结合CSS样式,实现卡片组件复用):

// src/components/Card.jsx
import './Card.css'; // 导入卡片样式

// 接收children和自定义className(用于扩展样式)
const Card = ({ children, className = '' }) => {
  // 合并基础样式和自定义样式(模板字符串用法)
  return <div className={{children};
};

export default Card;

编写Card组件的CSS样式(src/components/Card.css):

.card {
  background-color: #ffffff; /* 白色背景 */
  border-radius: 12px; /* 圆角 */
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); /* 轻微阴影 */
  padding: 20px; /* 内边距 */
  margin: 16px auto; /* 居中并留出上下间距 */
  max-width: 400px; /* 设置最大宽度 */
  transition: all 0.3s ease; /* 动画过渡 */
  overflow: hidden; /* 防止内容溢出圆角 */
}

.card:hover {
  transform: translateY(-4px); /* 悬停上浮效果 */
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); /* 更强阴影 */
}

.card h2 {
  margin-top: 0;
  font-size: 1.5rem;
  color: #333;
}

.card p {
  color: #666;
  font-size: 1rem;
}

.card button {
  margin-top: 12px;
  padding: 8px 16px;
  font-size: 0.9rem;
  border: none;
  border-radius: 6px;
  background-color: #0070f3;
  color: white;
  cursor: pointer;
  transition: background-color 0.2s ease;
}

.card button:hover {
  background-color: #005bb5;
}

在父组件中使用Card组件,通过children传递卡片内容:

// src/App.jsx(使用Card组件)
import Card from './components/Card';

function App() {
  return (
      {/* 卡片1:用户信息卡片 */}
      <Card className="user-card">
        张三高级前端工程师</Card>
      
      {/* 卡片2:商品信息卡片(复用Card组件,仅修改children内容) */}
      <Card className="goods-card">
        React实战教程从零入门React组件化开发</Card>
    
  );
}

可以看到,通过childrenProps,我们实现了Card组件的高度复用——同一个Card组件,只需传递不同的children内容,就能渲染出不同的卡片样式,极大减少了代码冗余。

五、Props 核心注意事项(避坑指南)

结合前面的实战代码,总结几个新手常踩的坑,帮你少走弯路:

  1. Props 只读不可改:子组件绝对不能修改父组件传递的Props,否则会触发React警告。如果需要修改数据,应在父组件中修改State,再通过Props重新传递(单向数据流原则)。
  2. 包名拼写错误:安装prop-types时,不要写成pro-types(少写一个p),否则会报ENOVERSIONS错误(无可用版本)。
  3. 必填项一定要加isRequired:对于必须传递的Props(如Greeting组件的name),一定要加上.isRequired,避免父组件遗漏传递导致页面异常。
  4. 组件目录规范:所有可复用组件建议放在src/components目录下,页面级组件放在src/pages目录下,便于项目维护和协作(组件化开发的最佳实践)。
  5. children的使用场景:当组件需要作为“容器”包裹其他内容时,优先使用childrenProps,避免冗余的Props传递。

六、总结

Props是React组件通信的核心,也是组件化开发的基础。它的用法看似简单,但吃透后能极大提升你的组件设计能力——从基础的Props传递、解构赋值,到默认值、类型校验,再到传递组件、children插槽,每一步都是实战中不可或缺的技巧。

本文结合完整的实战代码,覆盖了Props从入门到进阶的所有核心知识点,新手可以跟着代码一步步实践,熟悉Props的使用流程;老手可以回顾核心细节,规范自己的代码写法。

组件化开发的本质就是“拆分、复用、协作”,而Props就是连接这些环节的“桥梁”。掌握了Props,你就已经迈出了React组件化开发的关键一步,后续结合State、生命周期、 Hooks等知识点,就能轻松应对复杂的React项目开发啦

❌