当前位置: 首页 > news >正文

跟着 MDN 学 React 框架 Day 5:组件化 React 应用——从单体到模块化

摘要:本文是 React 学习之旅的第五日记录,核心主题是将一个单体式的 React 应用重构为清晰、可复用的组件化架构。文章将引导读者识别并提取应用中的关键组件,从最重要且重复使用的待办事项项入手,创建独立的Todo组件文件。我们将深入学习如何通过 Props 机制向组件动态传递数据(如任务名称、完成状态、唯一 ID),从而用同一组件渲染出不同内容,真正体现组件的复用价值。随后,我们会将任务数据抽象为 JavaScript 对象数组,并利用Array.prototype.map()方法配合特殊的key属性,实现列表的高效渲染。最后,我们将提取FormFilterButton组件,并整合所有组件,构建出一个模块化、易维护的组件树结构。

一、定义第一个组件:从识别到创建

在 React 开发的初期,我们的应用通常被写在一个庞大的App组件中,形成一个难以维护的"单体"结构。要让应用变得可管理、可扩展,我们必须将其分解为一系列职责单一、可描述的组件。React 本身并不强制规定何为组件、何不为组件,这给予了开发者极大的自由度,但也要求我们具备良好的判断力。一个实用的指导原则是:如果一个 UI 片段在应用中是明显的"块",或者它被频繁地复用,那么它就应该被抽离成一个独立组件。

遵循这个原则,我们审视当前的待办清单应用,最明显、重复度最高的 UI 片段就是每个任务项。应用中有三个几乎一模一样的任务列表项,每个都包含复选框、任务名称、编辑和删除按钮。这正是我们第一个要提取的组件。

在动手编写代码前,我们需要为组件建立合适的文件组织结构。良好的文件组织是项目可维护性的基石。我们将在src目录下创建一个专门存放组件的components文件夹,并在其中为第一个组件创建文件。请确保终端位于项目根目录,然后执行以下命令:

mkdirsrc/componentstouchsrc/components/Todo.js

第一条命令创建了components文件夹;第二条命令在其中创建了一个空的Todo.js文件。现在,打开这个新文件,我们将开始编写第一个独立的 React 组件。

二、编写<Todo />组件:从复制到独立

每一个 React 组件文件都需要引入 React 核心库,因为 JSX 最终会被转换为React.createElement调用。在Todo.js的顶部,我们首先添加导入语句:

importReactfrom"react";

接下来,我们需要定义并导出Todo组件。我们使用函数式组件的形式,这也是现代 React 推荐的方式。组件必须是一个首字母大写的函数,并且必须返回有效的 JSX 或者null。如果组件什么都不返回,React 会在浏览器控制台抛出错误。

exportdefaultfunctionTodo(){return(// 这里将放置我们的JSX);}

现在,我们需要给这个空壳组件填充 UI 内容。回到src/App.js,找到<ul>无序列表中任意一个<li>任务项,将其完整的 JSX 代码复制下来,粘贴到Todo函数的return语句中。此时,Todo.js的内容如下:

export default function Todo() { return ( <li className="todo stack-small"> <div className="c-cb"> <input id="todo-0" type="checkbox" defaultChecked={true} /> <label className="todo-label" htmlFor="todo-0"> Eat </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">Eat</span> </button> <button type="button" className="btn btn__danger"> Delete <span className="visually-hidden">Eat</span> </button> </div> </li> ); }

至此,Todo组件本身已经完成。我们可以回到App.js去使用它。首先,在文件顶部导入Todo组件:

importTodofrom"./components/Todo";

然后,将<ul>中原本的三个<li>元素全部替换为自闭合的<Todo />标签。修改后的列表部分看起来如下:

<ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading"> <Todo /> <Todo /> <Todo /> </ul>

然而,刷新浏览器后你会发现一个不幸的结果:三个相同的 “Eat” 任务被重复渲染了三次。这是因为我们直接将硬编码的数据(如 “Eat”)写死在了组件内部。为了让组件真正具有复用价值,我们必须让它能接收外部数据并渲染出不同的内容。

三、制作不同的<Todo />:Props 让组件活起来

组件的强大之处在于,它能让我们重用 UI 的绝大部分结构,同时又允许动态地改变小部分内容。在 React 中,这一机制就是 Props。Props 是父组件向子组件传递数据的桥梁,就像给 HTML 元素设置属性一样。

用 Props 传递任务名称

首先,我们在App.js中为每个<Todo />实例传入一个名为name的 prop,赋上不同的任务名称:

<Todo name="Eat" /> <Todo name="Sleep" /> <Todo name="Repeat" />

此时刷新浏览器,页面并不会有任何变化,因为Todo组件内部还没有使用这个 prop。我们需要回到Todo.js进行两项关键修改:

  • 修改函数签名,让它接收一个props参数。
  • 在 JSX 中,将所有硬编码的 “Eat” 替换为对props.name的引用。在 JSX 中,我们通过大括号{}来访问 JavaScript 变量或表达式。

修改后的Todo组件如下:

export default function Todo(props) { return ( <li className="todo stack-small"> <div className="c-cb"> <input id="todo-0" type="checkbox" defaultChecked={true} /> <label className="todo-label" htmlFor="todo-0"> {props.name} </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">{props.name}</span> </button> <button type="button" className="btn btn__danger"> Delete <span className="visually-hidden">{props.name}</span> </button> </div> </li> ); }

刷新浏览器,你会看到三个具有不同名称的任务项。注意,visually-hidden类包裹的辅助文本中,我们也动态替换了任务名,这对屏幕阅读器用户非常重要。

用 Props 控制完成状态

解决了名称问题,但所有任务的复选框都默认被勾选了。回顾原静态页面,只有 “Eat” 是完成状态。这为我们提供了第二个 Props 用例。在App.js中,为每个<Todo />传入一个名为completed的 prop,其值为布尔类型truefalse。注意,在 JSX 中传递布尔值必须使用大括号包裹。

<Todo name="Eat" completed={true} /> <Todo name="Sleep" completed={false} /> <Todo name="Repeat" completed={false} />

然后,回到Todo.js,将<input>元素的defaultChecked属性值从硬编码的{true}替换为{props.completed}

<input id="todo-0" type="checkbox" defaultChecked={props.completed} />

现在,刷新浏览器,你将看到只有 “Eat” 一项被勾选,这完全符合我们通过 Props 传入的初始状态。

用 Props 确保唯一 ID

还有一个遗留的 HTML 规范问题:每个<Todo />组件内的<input>元素id属性都是"todo-0"id必须在整个文档中保持唯一,重复的 ID 会导致 CSS 样式错乱、JavaScript 选择器失效等严重问题。因此,我们需要为每个Todo组件传入一个唯一的idprop。

App.js中:

<Todo name="Eat" completed={true} id="todo-0" /> <Todo name="Sleep" completed={false} id="todo-1" /> <Todo name="Repeat" completed={false} id="todo-2" />

Todo.js中,更新<input>id属性和<label>htmlFor属性,使其动态绑定到props.id

<div className="c-cb"> <input id={props.id} type="checkbox" defaultChecked={props.completed} /> <label className="todo-label" htmlFor={props.id}> {props.name} </label> </div>

至此,我们通过三个 Props——namecompletedid——让同一个Todo组件实例化出了三个外观和初始状态各不相同的任务项。这完美展示了组件的复用能力。

四、任务作为数据:实现数据驱动的渲染

尽管我们成功实现了组件的复用,但在App.js中手动编写三行极其相似的<Todo />代码仍然显得重复和低效。随着任务数量增加,这种硬编码方式将完全不可维护。根本问题在于,我们的 UI 渲染逻辑与数据是耦合的。现代前端开发的核心范式是"数据驱动渲染":UI 应该是数据的映射。为此,我们需要将任务信息抽象为一个数据结构,然后通过 JavaScript 的迭代能力动态生成 UI。

定义任务数据结构

审视每个任务的三个核心属性——唯一标识id、任务名称name和完成状态completed,它们可以完美地用一个 JavaScript 对象来表示。而多个任务则构成了一个对象数组。我们在src/index.js中,在ReactDOM.render()调用之前,定义一个常量数组DATA。采用全大写命名是 JavaScript 社区的一种惯例,用于向其他开发者传达"此数据在此定义后将永不改变"的信息。

constDATA=[{id:"todo-0",name:"Eat",completed:true},{id:"todo-1",name:"Sleep",completed:false},{id:"todo-2",name:"Repeat",completed:false},];

接下来,我们需要将这个数据传递给App组件。我们将它作为一个名为tasks的 prop 传入:

ReactDOM.render(<App tasks={DATA}/>,document.getElementById("root"));

此时,在App组件内部,我们就可以通过props.tasks访问到这个任务数组了。

使用 map() 方法进行迭代渲染

JavaScript 的Array.prototype.map()方法是实现数据驱动渲染的关键。它遍历数组中的每个元素,对每个元素执行一个回调函数,并返回一个由回调函数返回值组成的新数组。

我们在App组件的return语句之前,创建一个名为taskList的常量。我们将使用map()方法遍历props.tasks数组,并在每次迭代中返回一个配置好的<Todo />组件。

const taskList = props.tasks.map((task) => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} /> ));

然后在 JSX 的<ul>内部,我们只需简单地引用taskList这个变量:

<ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading"> {taskList} </ul>

这段代码完美体现了 React 的声明式特性:我们不再关心如何一步步构建 DOM,只需声明"UI 是这个数据数组的映射",React 会高效地执行 DOM 更新。

五、特殊的 key 属性:React 列表渲染的必备品

当 React 渲染一个由数组动态生成的列表时,它需要一种机制来跟踪每个列表项的身份,以便在数据发生变化时(如重新排序、添加、删除)能高效地确定哪些 DOM 节点需要更新,而不是粗暴地销毁并重建整个列表。这就是key属性的作用。它是一个由 React 内部使用的、特殊的 prop,你不能在子组件中通过props.key来访问它。

每个key的值在其兄弟列表中必须是唯一且稳定的。对于我们的任务列表,每个任务对象的id天生就是最理想的key。我们已经在上一节的代码中为每个<Todo />添加了key={task.id}。如果你在渲染列表时忘记提供key,或者使用了数组索引(index)作为key(在列表顺序可能改变时不推荐),React 会在浏览器控制台发出严厉的警告,并且可能导致界面出现难以调试的怪异行为。

六、整合 App 的其他部分:提取剩余的组件

现在,我们已经将最核心、最复杂的Todo组件整理完毕。遵循同样的组件化原则,我们可以轻松地将 App 的其余部分也拆分为独立组件。观察可知,顶部的输入表单是一个明显的独立 UI 块,应提取为<Form />组件;而底部的三个筛选按钮功能相似且会重复使用,每个按钮可提取为一个<FilterButton />组件。我们使用命令批量创建它们:

touchsrc/components/Form.js src/components/FilterButton.js

对于Form.js,我们遵循与Todo.js完全相同的模式:导入 React,定义并导出Form函数组件,然后将App.js<form>及其内部的全部 JSX 剪切过来,粘贴在return语句中。最终代码如下:

import React from "react"; function Form(props) { return ( <form> <h2 className="label-wrapper"> <label htmlFor="new-todo-input" className="label__lg"> What needs to be done? </label> </h2> <input type="text" id="new-todo-input" className="input input__lg" name="text" autoComplete="off" /> <button type="submit" className="btn btn__primary btn__lg"> Add </button> </form> ); } export default Form;

对于FilterButton.js,同样导入 React,定义并导出FilterButton函数组件,然后复制App.jsfilters这个<div>内的第一个按钮的 JSX 代码。注意,我们暂时只复制了一个按钮,因为后续我们将利用 Props 让它们产生差异。

import React from "react"; function FilterButton(props) { return ( <button type="button" className="btn toggle-btn" aria-pressed="true"> <span className="visually-hidden">Show </span> <span>all </span> <span className="visually-hidden"> tasks</span> </button> ); } export default FilterButton;

最后,我们回到App.js并完成最终的组装。在文件顶部导入FormFilterButton,然后更新return语句,用自定义组件标签替换原本的原始 HTML 标记。筛选按钮区域我们暂时放置了三个<FilterButton />,尽管它们现在看起来一样,但我们已经为后续的动态化改造预留了接口。最终整合后的App.js内容如下:

import React from "react"; import Form from "./components/Form"; import FilterButton from "./components/FilterButton"; import Todo from "./components/Todo"; function App(props) { const taskList = props.tasks.map((task) => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} /> )); return ( <div className="todoapp stack-large"> <h1>TodoMatic</h1> <Form /> <div className="filters btn-group stack-exception"> <FilterButton /> <FilterButton /> <FilterButton /> </div> <h2 id="list-heading">3 tasks remaining</h2> <ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading"> {taskList} </ul> </div> ); } export default App;

总结

本文系统地完成了 React 应用从单体结构到组件化架构的完整重构过程。我们从识别可复用 UI 块出发,创建了第一个独立的Todo组件,并深刻理解了组件必须返回有效 JSX 的规则。通过namecompletedid三个 Props 的逐步引入,我们彻底掌握了父组件向子组件动态传递数据的模式,让同一组件渲染出各不相同的任务项。之后,我们迈向了数据驱动渲染的关键一步,将任务信息抽象为对象数组,并利用 JavaScript 的map()方法配合不可或缺的key属性,高效优雅地渲染出整个任务列表。最后,我们将FormFilterButton提取为独立组件,并成功整合所有模块,构建了一个结构清晰、职责分明的组件树。

此刻,我们的应用已经具备了坚实的静态结构和数据基础,但所有按钮仍然像道具一样没有反应。在下一篇文章中,我们将进入 React 最激动人心的部分——事件处理与状态管理,为应用注入真正的交互灵魂。

http://www.rkmt.cn/news/1533498.html

相关文章:

  • 终极解决方案:VisualCppRedist AIO全合一安装包完全指南
  • 跨视角地理定位技术:SFDE网络与频域特征应用
  • Python零基础入门实战:从环境搭建到项目开发的完整学习路径
  • GT-POWER实战:从零搭建四缸汽油机一维仿真模型
  • 乌海市黄金回收白银回收铂金回收彩金回收店铺排行榜 2026实测五家诚信优选实体门店及电话地址推荐 - 大熊猫898989
  • 数字取证实战:从美亚杯竞赛解析电子数据调查核心技能
  • 2026年深圳汽车租赁公司怎么选?实地调研7家主流服务商,附中巴/大巴/跨境包车真实案例与价格参考 - 优质品牌商家
  • 汉中市黄金回收白银回收铂金回收彩金回收店铺排行榜 2026实测五家诚信优选实体门店及电话地址推荐 - 大熊猫898989
  • 基于OV2640打造低成本全局快门工业相机:从原理到实践
  • 滁州市黄金回收白银回收铂金回收彩金回收店铺排行榜 2026实测五家诚信优选实体门店及电话地址推荐 - 大熊猫898989
  • Ubuntu安装避坑指南:从硬件兼容到分区加密的完整实践
  • 大模型学习路线图:从Transformer到Agent应用开发实战指南
  • RV1106嵌入式AI视觉开发全流程:从环境搭建到模型部署实战
  • 济宁市黄金回收白银回收铂金回收彩金回收店铺排行榜 2026实测五家诚信优选实体门店及电话地址推荐 - 大熊猫898989
  • 嘉兴市黄金回收白银回收铂金回收彩金回收店铺排行榜 2026实测五家诚信优选实体门店及电话地址推荐 - 大熊猫898989
  • Matplotlib折线图深度解析:从基础绘图到出版级可视化
  • OceanBase seekdb:AI原生混合搜索数据库实战解析
  • 大同市黄金回收白银回收铂金回收彩金回收店铺排行榜 2026实测五家诚信优选实体门店及电话地址推荐 - 大熊猫898989
  • 5种AI Agent设计模式深度解析:告别提示词时代,构建生产级智能体!
  • 合肥市黄金回收白银回收铂金回收彩金回收店铺排行榜 2026实测五家诚信优选实体门店及电话地址推荐 - 大熊猫898989
  • 分析靠谱的居民搬家,四通搬家的口碑 - mypinpai
  • 单片机BLDC PID控制实验
  • 江门市黄金回收白银回收铂金回收彩金回收店铺排行榜 2026实测五家诚信优选实体门店及电话地址推荐 - 大熊猫898989
  • 鹤壁市黄金回收白银回收铂金回收彩金回收店铺哪家靠谱?2026实测五家诚信优选实体门店及电话地址推荐 - 盛世金银回收
  • verdi 将常用信号保存,方便后面调用
  • DBSCAN密度聚类原理与实战:解决不规则簇与噪声点识别
  • HCTSA生物医学信号处理:如何从EEG、ECG等生理信号中提取有用特征
  • 收藏 | AI小白必看:轻松掌握大模型核心概念,从基础到实战全解析
  • 2026年西安民间借贷律师选择指南:专业能力与执行经验深度分析 - 优质品牌商家
  • 企业级权限管理实战:从RBAC到ABAC混合模型设计与实现