React 组件设计:从 Props 传递到组件封装

React 组件设计:从 Props 传递到组件封装

摘要:本文以 React 进度条组件为例,拆解 Props 传值机制、空值合并运算符的巧妙应用,以及组件封装如何兼顾复用性与健壮性,帮助读者写出更可靠的 React 组件。

📑 目录

  • 从"搭积木"理解 React 组件化
  • 组件的两种数据来源:Props 与 State
  • 实战:设计一个进度条组件
  • Props 的类型定义与默认值处理
  • 父组件如何传递 Props
  • 组件封装的思考:健壮性优先
  • 互动讨论

从"搭积木"理解 React 组件化

在 React 的世界里,组件就是函数,函数返回 JSX。一个组件由 HTML 结构、CSS 样式和 JavaScript 逻辑组合而成,形成一个独立的功能单元。

这种设计让页面开发变成了"搭积木"------把一个个独立的小组件拼装起来,就构成了完整的页面。相比直接操作 DOM 标签,组件树的层级结构更清晰,一眼就能看出页面的构成和功能划分。

组件化带来的好处不仅仅是可读性:

优势 说明
团队协作 不同成员可以并行开发不同组件
代码复用 一个组件可以在多个页面中使用
易于维护 修改只影响组件内部,不波及全局
便于测试 每个组件可以独立测试

组件的两种数据来源:Props 与 State

一个 React 组件的数据来源有两种,理解它们的区别是写好组件的第一步。

State(状态)------组件的"私有财产"

State 是组件自己管理的数据,通过 useState 声明。它完全由组件自己掌控,想怎么改就怎么改。

tsx

scss 复制代码
const [value, setValue] = useState('');  // 这是组件自己的状态

Props(属性)------父组件给的"礼物"

Props 是从父组件传递到子组件的数据,子组件只能读,不能改。就像别人送的礼物,你可以展示它、使用它,但不能擅自改变它。

tsx

javascript 复制代码
// Props 是函数参数,由父组件传入
function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
  // 只能使用 props,不能修改 props
}

如果子组件需要修改 props 传递的数据,正确的做法是:调用父组件传递过来的回调函数,由父组件去修改。这种"数据单向流动"的设计,让 React 应用的状态变化变得可预测、可追踪。

实战:设计一个进度条组件

下面通过一个完整的进度条组件来展示组件设计的关键技术点。

tsx

typescript 复制代码
function formatBytes(size: number) {
  const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
  return (
    +(size / Math.pow(1024, i)).toFixed(2) * 1 +
    ["B", "kB", "MB", "GB", "TB"][i]
  );
}

const Progress = ({ text, percentage, total }: { text: string, percentage: number, total: number }) => {
  percentage ??= 0;
  
  return (
    <div className="w-full bg-gray-100 text-left rounded-lg overflow-hidden mb-0.5">
      <div 
        style={{ width: `${percentage}%` }}
        className="bg-blue-400 whitespace-nowrap px-1"
      >
        {text}
        {percentage.toFixed(2)}%
        {isNaN(total) ? "" : `of ${formatBytes(total)}`}
      </div>
    </div>
  );
};

export default Progress;

接下来逐一拆解这个组件的设计细节。

Props 的类型定义与默认值处理

显式声明 Props 类型

组件接收三个 props:

tsx

typescript 复制代码
({ text, percentage, total }: { text: string, percentage: number, total: number })
  • text:显示在进度条上的文字(如文件名)
  • percentage:进度百分比数值
  • total:文件总大小(用于显示"of 12.5MB"的辅助信息)

空值合并赋值 ??=

在组件内部,有一行关键的防御性代码:

tsx

ini 复制代码
percentage ??= 0;

这是 ES2021 引入的空值合并赋值运算符 。它的行为是:只有当 percentage 的值为 nullundefined 时,才将 0 赋值给它

tsx

ini 复制代码
// 等价于
percentage = (percentage ?? 0);

// 也等价于传统的三元写法
percentage = percentage !== null && percentage !== undefined ? percentage : 0;

为什么要这样写?在实际场景中,进度条的初始状态可能还没有下载进度数据,percentage 可能为 undefined。如果直接使用 percentage 来计算进度条宽度,width: undefined% 会导致样式异常。通过 ??= 保证了即使父组件没有传 percentage,组件也不会出错。

行内样式动态控制宽度

进度条的核心是宽度动态变化,这通过行内样式实现:

tsx

ini 复制代码
<div style={{ width: `${percentage}%` }} className="...">

注意这里的双层花括号:外层 {} 是 JSX 的 JavaScript 表达式语法,内层 {} 是 JavaScript 对象${percentage}% 是模板字符串,最终产生类似 "75%" 的字符串值。

格式化函数

formatBytes 函数将字节数转换为可读的单位:

tsx

arduino 复制代码
function formatBytes(size: number) {
  const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
  return (
    +(size / Math.pow(1024, i)).toFixed(2) * 1 +
    ["B", "kB", "MB", "GB", "TB"][i]
  );
}

它的计算逻辑是:

  1. 计算 size 是 1024 的几次方
  2. 根据次方数选择对应的单位(B、kB、MB、GB、TB)
  3. 将字节数除以对应量级,保留两位小数

条件渲染

组件底部有一段条件渲染逻辑:

tsx

javascript 复制代码
{isNaN(total) ? "" : `of ${formatBytes(total)}`}

如果 total 不是有效数字,就不显示文件大小信息;否则显示"of 12.5MB"。这进一步增强了组件的健壮性------即使父组件没有传入 total,进度条也不会崩溃。

父组件如何传递 Props

在父组件中,通过 map 遍历数据数组,为每个进度条传递不同的 props:

tsx

javascript 复制代码
<div className="w-full max-w-[1500px] text-left mx-auto p-4 bottom-0 mt-auto">
  <p className="text-center mb-1">{loadingMessage}</p>
  {progressItems.map(({ text, percentage, total }, i) => (
    <Progress key={i} text={text} percentage={percentage} total={total} />
  ))}
</div>

这里有三个关键点:

1. 属性名与 Props 参数名一一对应

tsx

scss 复制代码
<Progress text={text} percentage={percentage} total={total} />
//        ↑          ↑              ↑
//    参数名text  参数名percentage  参数名total

2. key 属性用于列表渲染优化

React 在渲染列表时需要 key 来唯一标识每个元素,帮助框架高效地对比和更新 DOM。这里使用了数组索引 i 作为 key

3. 组件可以像 HTML 标签一样使用

定义好的组件通过 import 引入后,就可以像使用 <div> 一样使用 <Progress>,这就是 React 组件化的直观体现------封装好的功能单元,以标签的形式被引用。

组件封装的思考:健壮性优先

这个进度条组件虽然只有 20 多行代码,却体现了组件封装的一条重要原则:封装者多考虑一些,使用者就会多不少爽感。

防御性设计

组件内部通过 ??=isNaN() 判断处理了 props 可能为空的边缘情况。调用者即使忘记传某些参数,组件也不会报错或显示异常。

语义化的 UI 反馈

进度条不仅显示百分比数字,还显示文件名和文件大小。用户能直观地知道"哪个文件下载了多少",而不是只看到一个孤零零的进度条。

无需手动管理样式

调用者只需要传递数据,样式完全由组件内部控制。这保证了项目中所有进度条的外观一致,也减少了调用者的代码量。

对比两种调用方式:

tsx

xml 复制代码
// 不健壮的组件------调用者需要处理所有边缘情况
<Progress text={item.text} percentage={item.percentage ?? 0} total={item.total ?? 0} />

// 健壮的组件------组件内部处理边缘情况,调用者只需传入原始数据
<Progress text={item.text} percentage={item.percentage} total={item.total} />

好的组件设计,应该让调用者少操心、少写防御代码

互动讨论

💬 Props 和 State 的区别是什么?

Props 是从父组件传入的,子组件只能读不能改;State 是组件自己管理的数据,通过 useState 声明,可以自由修改。两者共同构成了组件的"数据输入"------State 是内部输入,Props 是外部输入。

💬 为什么 React 不推荐直接修改 Props?

React 遵循单向数据流原则,数据从父组件流向子组件。如果子组件可以随意修改 Props,数据流动就会变得混乱,难以追踪状态变化的来源,调试和维护都会变得困难。

💬 ??=||= 有什么区别?

??= 只在值为 nullundefined 时赋值;||= 在值为假值(false0""nullundefinedNaN)时都会赋值。对于数值型数据,percentage 可能为 0,但 0 是合法值不应被覆盖,所以 ??= 更合适。

💬 行内样式和 CSS 类名应该如何选择?

动态变化的样式(如进度条宽度)适合用行内样式;静态样式(如颜色、间距、圆角)适合用 CSS 类名。两者结合使用最为高效。

💬 组件拆分的粒度怎么把握?

核心原则是单一职责:一个组件只做一件事。如果一个组件做的事情太多(比如既展示列表又管理输入框),就应该拆分成更小的子组件。进度条组件就是"只做进度展示"的典型例子。

相关推荐
独立开阀者_FwtCoder2 小时前
最近做了一个健身小程序:智形健身助手,健身的佬们来提点意见
前端·javascript·github
舒灿2 小时前
喵ing Tab 1.0.0 正式发布
前端·ai编程
葡萄城技术团队3 小时前
当表格数据来自外部系统:SpreadJS 如何让异步公式一键刷新
前端
小徐_23333 小时前
Open Wot 1.0.5 发布:让 AI 接入 wot-ui,只需要两条命令
前端·uni-app·ai编程
Android研究员3 小时前
Android进阶之事件分发机制深度剖析
android·前端·面试
Csvn3 小时前
🛡️ 前端水印方案全解析——从「能被 F12 删掉」到「删不掉」的反破解实战
前端
学网安的肆伍3 小时前
【046-WEB攻防篇】注入工具&SQLMAP&Tamper编写&指纹修改&高权限操作&目录架构
前端·架构
Highcharts.js5 小时前
如何下载使用Highcharts Grid 开发web可编辑表格
前端·人工智能·grid 表格·web 表格·在线编辑表格·highcharts表格安装·表格开发工具
小月土星5 小时前
React 状态管理核心原理:从批量 DOM 到派生状态
react.js