React和Vue中Props设计的常见陷阱与最佳实践
我的写法,亲测靠谱
Props 设计这事,我干了快六年,从 Vue 2 到 Vue 3,再到 React 18 + TS,踩过的坑比组件还多。最开始我真以为 props 就是“传个值”,结果上线后被产品拉着改了三轮:按钮颜色不对、文案没国际化、点击后状态不更新……最后发现全是因为 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 的边界怎么划」,后续见。

暂无评论