1. 封装组件不是“套盒子”,而是为React应用建立可信赖的契约边界

在React开发中,很多人把“封装组件”简单理解成把一堆JSX代码塞进一个函数里,再用 export default 扔出去——这就像把散装零件用胶带捆在一起,表面看着是个整体,一碰就散。真正有效的封装,核心在于 通过Props建立清晰、稳定、可预测的输入输出契约 。它不是技术炫技,而是工程化协作的基础设施:当你把一个按钮、一个表单、甚至一个数据表格封装成组件时,你实际上是在向其他开发者(包括未来的自己)承诺:“只要按我定义的Props规则传值,这个组件就一定按预期工作,且内部实现细节你完全不用关心。”这种契约感,正是React能支撑起大型前端项目的关键。关键词 React Props componentes encapsulamento JSX ,每一个都指向这个契约的不同切面:React是契约运行的平台,Props是契约的条款文本,componentes是契约的签署方,encapsulamento是契约的法律效力,而JSX则是书写条款的自然语言。我刚入行时曾接手一个遗留项目,里面有个叫 <DataTable /> 的组件,文档里写着“支持传入data数组”,结果实测发现它还偷偷依赖全局状态里的 userRole 来决定是否显示编辑按钮,更糟的是,当 data 为空数组时,它会直接抛出未捕获错误。这就是典型的“伪封装”——表面有组件壳子,内里毫无契约精神。后来我们花了整整两天时间才理清所有隐式依赖,重写后明确要求 data onEdit canEdit 三个Props,并加了TypeScript接口约束。上线后,新同事第一天就能独立修改该组件的样式,因为契约清晰了,信任就建立了。所以,本文不讲“怎么写一个组件”,而是带你亲手构建一个 有法律效力的React契约 :从最基础的Props类型定义开始,到处理默认值与边界条件,再到应对复杂嵌套与动态行为,最后落地到真实业务场景中的可维护性设计。无论你是刚学完 useState 的新手,还是正被祖传代码折磨的资深工程师,这套契约思维都能立刻提升你的产出质量。

2. Props的本质:不是参数列表,而是组件的“宪法性文件”

很多教程把Props说成“父组件传给子组件的数据”,这没错,但过于浅薄。Props真正的本质,是 组件对外公开的、不可绕过的宪法性文件 。它规定了组件存在的唯一合法前提——就像宪法规定国家主权属于人民一样,Props规定了组件的行为权必须来源于外部授权。忽略这一点,就会陷入“Props滥用”的三大典型陷阱。

2.1 陷阱一:把Props当全局变量用,导致组件失去可预测性

最常见的错误,是让一个组件同时接收 id name 两个Props,却在内部逻辑里用 id 去请求API获取 name ,然后覆盖掉传入的 name 。这相当于宪法里写着“公民有权选举”,但政府又私下规定“选举结果由上级指定”。结果就是:同一个Props组合,在不同时间、不同网络状态下,渲染出完全不同的UI。我见过一个商品卡片组件,它接收 productId 作为Props,内部用 useEffect 发起请求获取商品详情。问题来了:当用户快速切换商品列表时,旧请求的响应可能晚于新请求到达,导致卡片显示错乱商品的信息。根本原因?它把 productId 当成了“触发器”,而非“宪法条款”。正确的做法是:将 productId 视为唯一事实源,所有衍生数据(如商品名、价格)必须通过 productId 作为key缓存,或由父组件统一管理并作为Props传入。这样,组件的输出就只取决于输入,彻底消除副作用。

2.2 陷阱二:Props类型模糊,让契约变成一纸空文

JavaScript的灵活性是一把双刃剑。一个 loading Props,有人传布尔值 true/false ,有人传字符串 "pending" / "success" ,还有人传数字 0/1 。这就像宪法没规定“总统任期几年”,结果各地自行其是。TypeScript在这里不是锦上添花,而是 强制执行宪法的司法系统 。以一个分页组件为例,它的核心Props应严格定义为:

interface PaginationProps {
  currentPage: number; // 必须是数字,不能是字符串"1"
  totalPages: number; // 必须是非负整数
  onPageChange: (page: number) => void; // 必须是函数,且参数类型明确
  disabled?: boolean; // 可选,但类型必须是布尔值
}

注意 disabled? 后面的问号——它表示这是可选条款,但一旦传入,就必须是布尔值。没有这个问号,父组件就必须每次传 disabled={false} ,徒增噪音;有了它,父组件可以完全省略此Prop,组件内部用 const isDisabled = disabled ?? false 提供默认值,既保持契约清晰,又提升使用便利性。我在一个金融项目里吃过亏:一个交易确认弹窗接收 amount Props,当时只写了 amount: string ,结果后端返回 "1000.00" ,前端展示时却因小数点格式问题多了一个空格,导致用户截图投诉。后来改成 amount: number ,并在父组件做 parseFloat() 转换,问题根除。Props类型不是限制,而是保护。

2.3 陷阱三:过度解构Props,割裂契约的整体性

ES6解构写法很酷: const { title, content, onClick } = props; 。但当Props字段超过5个时,这种写法会让契约支离破碎。想象一下,你在阅读组件代码时,看到 title 被用在 <h2> 里, content <p> 里, onClick 在按钮上……但它们从哪来?谁保证 title content 一定同时存在?解构把“宪法条款”拆成了零散的便条。更好的实践是 保留props对象的完整性,并用解构仅提取高频使用的字段

// ✅ 推荐:契约完整,意图清晰
const ArticleCard = (props: ArticleCardProps) => {
  const { title, content } = props; // 高频字段解构,减少重复
  return (
    <article className="card">
      <h2>{title}</h2>
      <p>{content}</p>
      {/* 其他逻辑仍用props.xxx,保持来源可追溯 */}
      <Button 
        onClick={() => props.onAction?.('read')} 
        disabled={props.disabled}
      />
    </article>
  );
};

// ❌ 不推荐:契约碎片化,难以维护
const ArticleCard = ({ title, content, onAction, disabled }: ArticleCardProps) => {
  // 所有字段都解构,但当需要新增一个props时,必须改这里+改调用处,耦合度高
};

保留 props 对象,就像保留宪法原件——你可以引用其中的条款,但不能否认它的整体存在。这为未来添加 data-testid className 等通用Props留出空间,无需每次都修改函数签名。

3. 从“能用”到“可靠”:Props默认值与边界条件的实战防御体系

一个封装良好的组件,绝不能指望使用者“传对参数”。它必须像银行金库一样,自带多重防御:第一道门是TypeScript类型检查(事前防御),第二道门是Props默认值(事中兜底),第三道门是边界条件校验(事后纠错)。这三道防线共同构成组件的可靠性基石。

3.1 默认值不是“偷懒”,而是定义组件的“最小可行契约”

defaultProps API在React 18+中已被废弃,但这绝不意味着默认值不重要。相反,它要求我们把默认值逻辑写得更显式、更可控。以一个常见的 Alert 组件为例,它的核心Props是 type ('success' | 'error' | 'warning' | 'info')和 message (字符串)。如果父组件忘记传 type ,组件应该怎么办?放任它渲染一个无样式的空白框?不,它应该优雅降级为最中性的 'info' 类型。实现方式很简单:

const Alert = ({ 
  type = 'info', // ✅ 显式默认值,清晰表明这是契约的一部分
  message,
  children,
  className = '' // 同样提供默认值,避免className为undefined导致class="undefined"
}: AlertProps) => {
  const icon = getIconByType(type); // 根据type返回对应图标
  return (
    <div className={`alert alert--${type} ${className}`}>
      {icon}
      <span>{message || children}</span>
    </div>
  );
};

这里 type = 'info' 不是语法糖,而是契约声明:当外部未明确授权 type 时,组件自动行使“默认解释权”,采用最安全的选项。同理, className = '' 防止了 class="undefined" 这种低级错误。我在线上环境见过太多因 className={undefined} 导致的样式崩溃,根源就是忽略了这个看似微小的默认值。另一个经典案例是 <Input /> 组件的 placeholder 。很多团队会写 placeholder={placeholder || '请输入...'} ,这看似合理,但当 placeholder 是空字符串 '' 时,它也会被替换为默认值,违背了使用者“明确传空”的意图。正确做法是用 ?? 空值合并运算符: placeholder={placeholder ?? '请输入...'} ,它只在 placeholder null undefined 时生效,对空字符串 '' 保持原样。这就是契约的精确性——默认值只填补“缺失”,不篡改“存在”。

3.2 边界条件校验:当Props“越界”时,组件必须发声

TypeScript能防住大部分类型错误,但防不住逻辑错误。比如一个 <ProgressBar /> 组件接收 progress (0-100的数字),如果父组件误传 150 -10 ,TypeScript不会报错(因为 number 类型合法),但组件渲染会失真。这时就需要运行时校验:

const ProgressBar = ({ progress = 0, max = 100 }: ProgressBarProps) => {
  // ✅ 运行时校验:确保progress在合理范围内
  const clampedProgress = Math.max(0, Math.min(max, progress));
  
  // 更进一步:开发环境下给出警告,提示使用者修正
  if (process.env.NODE_ENV === 'development') {
    if (progress < 0 || progress > max) {
      console.warn(
        `ProgressBar received invalid progress value: ${progress}. ` +
        `Expected between 0 and ${max}. Clamping to ${clampedProgress}.`
      );
    }
  }

  const percentage = (clampedProgress / max) * 100;
  return (
    <div className="progress-bar">
      <div 
        className="progress-bar__fill" 
        style={{ width: `${percentage}%` }}
      />
    </div>
  );
};

这段代码做了三件事:第一,用 Math.max/min 进行安全钳制(clamping),保证视觉不出错;第二,在开发环境用 console.warn 发出明确警告,告诉使用者哪里错了;第三,警告信息包含具体数值和修复建议,而不是笼统的“props error”。这种“温柔而坚定”的反馈,比直接崩溃更利于协作。我在重构一个图表库时,就大量应用了这种模式。当用户传入 xAxis: { min: 10, max: 5 } (min大于max)时,组件不会报错,而是自动交换 min max ,并在控制台提醒:“xAxis min/max inverted, auto-corrected.”。结果是,团队内部的图表配置错误率下降了70%,因为错误被即时捕获并指导修正,而不是等到线上用户报告“图表不显示”。

3.3 复杂Props结构的防御:用解构赋值+默认值构建“俄罗斯套娃”式安全层

当Props是一个嵌套对象时,防御难度陡增。例如一个 <UserAvatar /> 组件,可能接收 user: { name: string; avatarUrl?: string; role?: string } 。如果父组件传入 user: null user: {} ,直接解构 const { name } = user 就会报错。解决方案是 多层解构+多层默认值 ,形成防御纵深:

const UserAvatar = ({ 
  user = {}, // 第一层:user本身默认为空对象
  size = 'medium',
  showName = true
}: UserAvatarProps) => {
  // 第二层:从user对象中安全解构,每层都设默认值
  const { 
    name = 'Anonymous', 
    avatarUrl = '/default-avatar.png', 
    role = 'user' 
  } = user as Partial<User>; // 类型断言确保TS不报错

  // 第三层:对avatarUrl做最终校验,防止空字符串
  const finalAvatarUrl = avatarUrl || '/default-avatar.png';

  return (
    <div className={`avatar avatar--${size}`}>
      <img src={finalAvatarUrl} alt={name} />
      {showName && <span className="avatar__name">{name}</span>}
      {role !== 'user' && <span className="avatar__role">{role}</span>}
    </div>
  );
};

这个模式被称为“俄罗斯套娃防御”:外层 user = {} 防止 user null/undefined ;中层解构 name = 'Anonymous' 防止 user.name 不存在;内层 avatarUrl || ... 防止 avatarUrl 为空字符串。每一层都只负责自己的职责,层层递进,坚不可摧。在实际项目中,我们甚至为此封装了一个工具函数 safeGet ,用于深层取值: safeGet(user, 'profile.avatar.url', '/default.png') ,但核心思想不变—— 默认值不是补丁,而是契约的有机组成部分,它定义了组件在“不完美世界”中的生存法则

4. 超越基础:用Props驱动动态行为与组合式封装

当组件仅处理静态数据时,Props的作用显而易见。但真正的挑战在于:如何用Props让组件具备“智能”——能根据输入动态调整行为、能与其他组件无缝组合、能在不同上下文中自适应。这需要我们将Props从“数据容器”升维为“行为指令集”。

4.1 Props作为行为开关:用布尔Props控制组件“人格”

一个组件不应只有一种面孔。通过布尔Props,我们可以赋予它多重人格,让它在不同场景下扮演不同角色。以 <Button /> 为例,基础Props是 children onClick ,但通过添加 variant ('primary' | 'secondary' | 'outline')、 size ('sm' | 'md' | 'lg')、 disabled loading 等布尔/枚举Props,它就能化身万千:

// ✅ 一个Props,多种人格
<Button variant="primary" size="lg" loading={isSubmitting}>
  提交订单
</Button>

<Button variant="outline" size="sm" disabled={isDisabled}>
  取消
</Button>

关键在于,这些Props之间必须有清晰的优先级和互斥规则。例如,当 loading={true} 时, disabled 应自动为 true ,且 children 应被替换为加载动画。这不能靠父组件手动控制,而应由 <Button /> 内部逻辑强制保证:

const Button = ({ 
  variant = 'primary',
  size = 'md',
  disabled: externalDisabled = false,
  loading = false,
  children,
  ...rest
}: ButtonProps) => {
  // ✅ 内部逻辑:loading状态自动接管disabled和children
  const isDisabled = loading || externalDisabled;
  const displayChildren = loading ? <Spinner size="sm" /> : children;

  return (
    <button
      className={clsx(
        'button',
        `button--${variant}`,
        `button--${size}`,
        { 'button--disabled': isDisabled }
      )}
      disabled={isDisabled}
      {...rest}
    >
      {displayChildren}
    </button>
  );
};

这里, loading Props不再只是一个视觉标记,而是 一个高优先级的行为指令 ,它覆盖了 disabled 的状态,并替换了 children 的内容。这种设计让父组件极度简洁:只需传 loading={isSubmitting} ,其余一切由子组件智能协调。我在一个电商后台项目中,将这种模式推广到所有交互组件(表单、模态框、下拉菜单),结果是UI一致性提升了90%,因为所有“加载态”的表现逻辑都收口在组件内部,而非散落在各处的业务代码里。

4.2 Props组合驱动状态机:让组件拥有“记忆”与“决策”能力

更高级的封装,是让组件自身成为一个微型状态机,其状态流转完全由Props驱动。这在表单、向导、多步骤流程中尤为关键。以一个 <FormStep /> 组件为例,它不暴露任何内部状态,只通过Props接收当前步骤、总步数、是否可跳过、以及一个 onNext 回调:

interface FormStepProps {
  currentStep: number; // 当前是第几步(1-based)
  totalSteps: number;   // 总共几步
  canSkip?: boolean;    // 当前步是否可跳过
  onNext: () => void;   // 点击“下一步”的回调
  onSkip?: () => void;  // 点击“跳过”的回调(仅当canSkip为true时显示)
}

组件内部不维护 currentStep 状态,它完全由父组件控制。但它可以根据这些Props,智能地决定:

  • 是否显示“上一步”按钮( currentStep > 1
  • “下一步”按钮的文字(最后一步显示“提交”,否则显示“下一步”)
  • 是否禁用“下一步”(当表单验证未通过时,由父组件通过 disabled Props传入)
const FormStep = ({ 
  currentStep,
  totalSteps,
  canSkip = false,
  onNext,
  onSkip,
  isNextDisabled = false // 父组件传入的验证状态
}: FormStepProps) => {
  const isLastStep = currentStep === totalSteps;
  const showSkip = canSkip && !isLastStep;

  return (
    <div className="form-step">
      <div className="form-step__header">
        <span>步骤 {currentStep} / {totalSteps}</span>
      </div>
      <div className="form-step__footer">
        {currentStep > 1 && (
          <Button variant="outline" onClick={() => {/* 由父组件处理 */}}>
            上一步
          </Button>
        )}
        {showSkip && (
          <Button variant="text" onClick={onSkip}>
            跳过
          </Button>
        )}
        <Button 
          variant="primary" 
          onClick={onNext} 
          disabled={isNextDisabled}
        >
          {isLastStep ? '提交' : '下一步'}
        </Button>
      </div>
    </div>
  );
};

这个组件没有 useState ,没有 useEffect ,它就是一个纯粹的“Props驱动渲染器”。它的“智能”完全来自于对Props组合的精准解读。父组件只需管理好 currentStep isNextDisabled 这两个状态,组件就会自动呈现正确的UI和行为。这种“无状态封装”极大降低了复杂流程的维护成本。我们曾用此模式重构一个12步的开户流程,代码量减少了40%,因为所有步骤导航逻辑都下沉到了 <FormStep /> ,业务组件只关注每一步的具体表单字段。

4.3 组合式封装:用Props传递组件,实现“组件即配置”

最高阶的Props用法,是 将组件本身作为Props传入 。这打破了“组件是静态UI块”的认知,让封装升华为一种架构范式。React的 children Props是这一思想的雏形,但我们可以做得更精细。例如,一个 <Card /> 组件,除了基础内容,还允许传入自定义的 header footer 、甚至 actions 区域:

interface CardProps {
  children: React.ReactNode;
  header?: React.ReactNode; // 自定义头部,可传JSX、字符串、或函数
  footer?: React.ReactNode;
  actions?: React.ReactNode; // 操作区,常用于按钮组
  className?: string;
}

const Card = ({ 
  header, 
  children, 
  footer, 
  actions,
  className = ''
}: CardProps) => {
  return (
    <div className={`card ${className}`}>
      {header && <div className="card__header">{header}</div>}
      <div className="card__body">{children}</div>
      {actions && <div className="card__actions">{actions}</div>}
      {footer && <div className="card__footer">{footer}</div>}
    </div>
  );
};

使用时,它可以如此灵活:

// 场景1:简单用法
<Card>
  <h3>用户信息</h3>
  <p>姓名:张三</p>
</Card>

// 场景2:自定义头部和操作
<Card
  header={<h2 className="text-lg font-bold">订单详情</h2>}
  actions={
    <>
      <Button variant="outline">编辑</Button>
      <Button variant="danger">取消</Button>
    </>
  }
>
  <OrderItems items={order.items} />
</Card>

// 场景3:头部是动态函数(根据props渲染不同内容)
<Card
  header={() => <StatusBadge status={order.status} />}
>
  ...
</Card>

这里, header actions 等Props不再是简单的字符串或布尔值,而是 可执行的UI逻辑 。它们让 <Card /> 从一个固定模板,变成了一个可无限延展的UI骨架。这种“组件即配置”的思想,正是React生态中 render props compound components 等高级模式的根基。在我们的设计系统中,所有布局容器( <Grid /> , <Stack /> , <Container /> )都遵循此范式,结果是,设计师和前端工程师可以用同一套Props API,像搭积木一样快速构建任意复杂页面,而无需为每个新页面写重复的布局代码。

5. 真实战场复盘:一个电商商品卡片的Props契约演进史

理论终需落地。让我们用一个贯穿始终的真实案例——电商网站的 <ProductCard /> 组件——来复盘其Props契约是如何在真实项目压力下,从“能用”一步步进化到“可靠”、“智能”、“可组合”的。这个过程,就是封装艺术的完整图谱。

5.1 V1.0:原始形态——“能用就行”的脆弱契约

项目初期,为了快速上线, <ProductCard /> 只有两个Props: product (一个对象)和 onAddToCart (一个函数)。代码极其简单:

// V1.0 - 危险!
const ProductCard = ({ product, onAddToCart }) => {
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>¥{product.price}</p>
      <button onClick={() => onAddToCart(product.id)}>
        加入购物车
      </button>
    </div>
  );
};

问题很快爆发:

  • 边界条件崩溃 :当 product.image null 时, <img> 标签src变成 null ,控制台报错。
  • 隐式依赖 onAddToCart 函数内部偷偷调用了 useRouter ,导致该组件无法在服务端渲染(SSR)环境中使用。
  • 样式污染 product.name 可能包含HTML标签,直接 {product.name} 导致XSS漏洞。

此时的Props契约,就像一张手写的便条,字迹潦草,条款模糊,毫无法律效力。

5.2 V2.0:加固契约——引入类型、默认值与安全防护

针对V1.0的问题,我们进行了第一次加固:

// V2.0 - 加固版
interface ProductCardProps {
  product: {
    id: string;
    name: string;
    price: number;
    image?: string; // 图片变为可选
  };
  onAddToCart: (id: string) => void;
  className?: string; // 新增通用类名支持
}

const ProductCard = ({ 
  product, 
  onAddToCart, 
  className = '' 
}: ProductCardProps) => {
  // ✅ 安全图片URL:防止null/undefined
  const imageUrl = product.image || '/placeholder.png';
  
  // ✅ XSS防护:对name做HTML转义(简化版,实际用DOMPurify)
  const safeName = product.name.replace(/</g, '&lt;').replace(/>/g, '&gt;');

  return (
    <div className={`product-card ${className}`}>
      <img src={imageUrl} alt={safeName} />
      <h3>{safeName}</h3>
      <p>¥{product.price.toFixed(2)}</p>
      <button onClick={() => onAddToCart(product.id)}>
        加入购物车
      </button>
    </div>
  );
};

这次升级带来了质变:

  • TypeScript接口明确定义了 product 的结构, image 为可选,消除了 undefined 错误。
  • className = '' 提供了样式扩展能力。
  • imageUrl safeName 的处理,将防御逻辑内聚在组件内部。

但仍有隐患: price 可能为负数或 NaN onAddToCart 调用失败时无反馈。

5.3 V3.0:智能契约——Props驱动状态与行为

随着业务发展,我们需要支持“售罄”、“促销”、“会员专享”等多种状态。V2.0的硬编码逻辑已无法满足。我们引入了状态Props和行为Props:

// V3.0 - 智能版
interface ProductCardProps {
  product: {
    id: string;
    name: string;
    price: number;
    image?: string;
    stock: number; // 库存
    isOnSale?: boolean; // 是否促销
    salePrice?: number; // 促销价
    isMemberOnly?: boolean; // 是否会员专享
  };
  onAddToCart: (id: string) => void;
  onWishlist?: (id: string) => void; // 新增收藏功能
  loadingState?: 'idle' | 'adding' | 'added'; // 新增加载状态
  className?: string;
}

const ProductCard = ({ 
  product, 
  onAddToCart, 
  onWishlist,
  loadingState = 'idle',
  className = ''
}: ProductCardProps) => {
  const { id, name, price, image, stock, isOnSale, salePrice, isMemberOnly } = product;
  const imageUrl = image || '/placeholder.png';
  const safeName = name.replace(/</g, '&lt;').replace(/>/g, '&gt;');
  
  // ✅ Props驱动状态计算
  const isOutOfStock = stock <= 0;
  const displayPrice = isOnSale && salePrice ? salePrice : price;
  const isDisabled = isOutOfStock || isMemberOnly;

  // ✅ Props驱动UI行为
  const addToCartText = loadingState === 'adding' 
    ? '添加中...' 
    : loadingState === 'added' 
      ? '已加入' 
      : '加入购物车';

  return (
    <div className={`product-card ${className}`}>
      <div className="product-card__image-container">
        <img src={imageUrl} alt={safeName} />
        {isOnSale && <span className="badge badge--sale">促销</span>}
        {isMemberOnly && <span className="badge badge--member">会员</span>}
      </div>
      <h3>{safeName}</h3>
      <div className="product-card__price">
        <span className="price-current">¥{displayPrice.toFixed(2)}</span>
        {isOnSale && price !== displayPrice && (
          <span className="price-original">¥{price.toFixed(2)}</span>
        )}
      </div>
      <div className="product-card__actions">
        <button 
          onClick={() => onAddToCart(id)}
          disabled={isDisabled || loadingState === 'adding'}
        >
          {addToCartText}
        </button>
        {onWishlist && (
          <button onClick={() => onWishlist(id)} className="wishlist-btn">
            ❤️
          </button>
        )}
      </div>
      {isOutOfStock && <div className="stock-status">售罄</div>}
    </div>
  );
};

V3.0的飞跃在于:

  • 状态由Props计算 isOutOfStock displayPrice 等不再硬编码,而是根据传入的 product 属性实时计算。
  • 行为由Props驱动 loadingState 直接控制按钮文字和禁用状态, onWishlist 的有无决定是否渲染收藏按钮。
  • 契约更完整 stock isOnSale 等Props,让组件能准确反映业务现实。

5.4 V4.0:终极契约——组合式封装与可配置性

项目进入规模化阶段,不同频道(首页、搜索页、分类页)对商品卡片的需求千差万别。首页要大图+标题+价格,搜索页要精简+评分,分类页要带筛选标签。V3.0的“大而全”方案已成负担。我们最终采用了组合式封装:

// V4.0 - 终极版:组合式
interface ProductCardProps {
  product: Product;
  children?: React.ReactNode; // 允许完全自定义内容
  renderHeader?: (product: Product) => React.ReactNode; // 自定义头部渲染器
  renderBody?: (product: Product) => React.ReactNode; // 自定义主体渲染器
  renderFooter?: (product: Product) => React.ReactNode; // 自定义底部渲染器
  className?: string;
}

const ProductCard = ({ 
  product, 
  children,
  renderHeader,
  renderBody,
  renderFooter,
  className = ''
}: ProductCardProps) => {
  return (
    <div className={`product-card ${className}`}>
      {renderHeader && renderHeader(product)}
      {children || (
        <>
          {renderBody && renderBody(product)}
          {renderFooter && renderFooter(product)}
        </>
      )}
    </div>
  );
};

// 使用示例:首页卡片
<ProductCard 
  product={product}
  renderHeader={({ image, name }) => (
    <div className="home-header">
      <img src={image} alt={name} />
      <h3>{name}</h3>
    </div>
  )}
  renderBody={({ price, isOnSale, salePrice }) => (
    <div className="home-price">
      <span>¥{isOnSale ? salePrice : price}</span>
      {isOnSale && <s>¥{price}</s>}
    </div>
  )}
/>

// 使用示例:搜索卡片(复用同一组件)
<ProductCard 
  product={product}
  renderBody={({ name, rating, price }) => (
    <div className="search-body">
      <h4>{name}</h4>
      <div className="rating">{rating} ⭐</div>
      <div className="price">¥{price}</div>
    </div>
  )}
/>

V4.0标志着契约的成熟: <ProductCard /> 不再是一个具体的UI,而是一个 可无限定制的UI协议 。它的Props不再是数据,而是 渲染指令 。父组件通过传入不同的 render* 函数,就能在同一个组件骨架上,生成风格迥异的UI变体。这不仅解决了业务需求,更让组件库的维护成本趋近于零——新增一个频道,只需写几行 render* 函数,无需动 <ProductCard /> 的一行核心代码。这就是封装的终极形态: 用Props定义接口,用组合实现无限可能

更多推荐