React和Vue中Props设计的常见陷阱与最佳实践

小丽红 组件 阅读 1,731
赞 14 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

Props 设计这事,我干了快六年,从 Vue 2 到 Vue 3,再到 React 18 + TS,踩过的坑比组件还多。最开始我真以为 props 就是“传个值”,结果上线后被产品拉着改了三轮:按钮颜色不对、文案没国际化、点击后状态不更新……最后发现全是因为 props 没设计好。

React和Vue中Props设计的常见陷阱与最佳实践

现在我写组件,第一件事不是写 template 或 JSX,而是打开一个空的 TypeScript 接口文件,先把 props 定义出来。不是随便写几个 string | boolean,而是像搭积木一样,先想清楚这个组件「只该知道什么」、「不该知道什么」、「谁该负责转换数据」。

比如我最近写的 ArticleCard 组件,它只展示标题、摘要、发布时间和一个「阅读更多」链接。我一开始是这么写的:

interface ArticleCardProps {
  title: string
  excerpt: string
  publishTime: string // ❌ 错误!字符串时间格式太脆弱
  link: string
}

结果测试时发现:后端返回的是 ISO 时间戳,运营后台又传 Unix 时间戳,有时候还是中文日期字符串。我在组件里写了三段时间格式化逻辑,还漏了时区处理……折腾半天发现,这根本不是组件该干的事。

现在我的写法是这样:

interface ArticleCardProps {
  title: string
  excerpt: string
  publishTime: Date // ✅ 必须是 Date 类型,强制上游转换
  link: string
  onClick?: () => void
  className?: string
}

注意两点:一是 publishTime 类型锁死为 Date,谁传错谁负责;二是所有可选 props 显式标注 ?,连 className 都不放过——因为我要保证组件能被 Tailwind 用户、CSS Modules 用户、甚至内联 style 用户无痛接入。

而且,我从来不在组件内部做数据转换。比如 publishTime 的显示格式,我交给父组件传进来:formattedPublishTime: string,或者干脆用插槽。别觉得麻烦,组件越 dumb,越不容易出问题。

这几种错误写法,别再踩坑了

下面这几个写法,我都在生产环境里亲手埋过雷,列出来大家避坑:

  • 用 any 或 unknown 当 props 类型:有人图省事写 props: any,美其名曰“灵活”。结果三个月后加个新字段,TS 不报错,但运行时报 Cannot read property 'xxx' of undefined。我改过两次,每次都要翻 commit 找是谁加的字段。
  • 把业务逻辑塞进 props 名称里:比如 isShowDeleteButton: boolean。看着合理?其实错了。这个布尔值到底是“权限控制”还是“编辑态开关”?下游根本分不清。后来我改成 canDelete: boolean,配合注释说明来源(如「由用户角色和文章状态共同决定」),维护性高了一倍。
  • props 默认值写在组件定义里:Vue 里写 default: () => ({}),React 里写 const { loading = false } = props。问题在于:默认值一旦写死,就和业务强耦合了。我们有个搜索组件,placeholder 默认是“搜索商品”,结果海外版要改成英文,还得改组件源码。现在我一律让父组件传,默认值在调用处写:<Search placeholder="Search products" />
  • 用 props 控制样式类名拼接:比如 size: 'sm' | 'md' | 'lg',然后在组件里写 class="btn btn--${size}"。看起来很干净?但 Tailwind 用户没法用,他们想写 class="px-4 py-2 text-sm"。我现在统一收口成 className?: string,其他 size、variant 全部移除——让样式交由使用者决定,组件只管结构和行为。

实际项目中的坑

真实项目里,props 最容易出问题的地方不是类型,而是「上下游理解不一致」。

举个例子:我们有个 UserAvatar 组件,需要头像 URL 和 fallback 文字。一开始约定 avatarUrl?: string,结果后端有时返回空字符串、有时返回 null、有时返回 "https://jztheme.com/avatar/missing.png"(占位图)。前端拿到后直接 img.src = avatarUrl,结果页面上一堆占位图。

后来我们定了条铁律:所有可选图片 props,必须明确约定「空值含义」。现在接口是这样:

interface UserAvatarProps {
  avatarUrl: string | null // null = 无头像,不渲染 img;空字符串 = 渲染但 src 为空
  fallbackText: string
  size?: 'sm' | 'md' | 'lg'
}

并且加了运行时校验(开发环境):

if (process.env.NODE_ENV === 'development') {
  if (typeof props.avatarUrl === 'string' && props.avatarUrl.trim() === '') {
    console.warn('UserAvatar: avatarUrl is empty string, use null instead')
  }
}

还有个坑是「props 变更触发副作用」。比如你传了个 data 对象进去,组件里 watch 它并重新请求详情页。结果父组件用了 Object.assign 浅拷贝 data,导致引用没变,watch 不触发。我们后来统一要求:所有对象类 props,必须带 key 强制重渲染,或者用 JSON.stringify(data) 做依赖(仅限小数据)。

最后一点:别怕 props 多。我们有个表单组件,最初只有 5 个 props,后来加到 17 个。有人建议拆分成多个子组件,但我试过,反而更难维护——每个子组件都要暴露自己的 props,调用方要写三层嵌套。现在我们用组合式 API 把逻辑抽成 hooks,props 还是集中管理,清晰多了。

结尾

以上是我这几年在 props 设计上踩坑、改坑、再踩坑总结出来的实战经验。没有银弹,也没有“绝对正确”的方案,只有“当前项目最合适”的选择。有些方案看起来啰嗦(比如强制传 Date),但省下的 debug 时间够我喝三杯咖啡。

如果你有更轻量、更健壮的 props 管理方式,或者遇到过我没提的奇葩 case,欢迎评论区交流。这个主题我还会继续写下去,比如「如何用 defaultProps 配合 TS 实现安全降级」、「props 与 v-model 的边界怎么划」,后续见。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论