Prop drilling 是指把一个 prop 穿过并不使用它的组件往下传,只为了让组件树更深处的某个组件能用到它。在这里,user 从 App 出发,经过 Page 和 Sidebar,才到达唯一读取它的组件 Avatar。
一共四层,只有最后一层关心 user。控制台显示,Page 和 Sidebar 为一个它们从不显示的值运行了一遍。点击按钮,它们会再次打印日志。
为什么麻烦
两三层没问题。随着组件树变大,prop drilling 就会成为问题:
- 中间组件带着它们不用的 props。
Page和Sidebar的签名里现在都有一个userprop,读代码时你总会疑惑它是干什么用的。 - 每次修改都要动每一层。 加一个
user.avatarUrl,或者再加一个onLogout这样的值,你都得修改路径上的每个组件,而不只是两端。 - 移动组件会打断传递链。 把
Avatar移到另一个分支,你就必须沿新路径把user重新传一遍。 - 很容易漏传 prop。 只要某一层忘了转发,深层组件拿到的就是
undefined,而漏传的那一层不会报任何错。
试一试:在 App 中加一个应该由 Avatar 调用的 onLogout 函数。你会发现四个组件都要改。
解决办法 1:用 children 组合
这是大家最容易忽略的办法。与其把数据往下传,让深层组件拿着它去渲染,不如让拥有数据的组件自己渲染这个深层组件,再把做好的元素作为 children(或任何其他 prop)传下去。中间层只负责渲染一个插槽,完全看不到 user。
Page 和 Sidebar 不再知道有 user 这回事。App 在 user 所在的地方创建 Avatar,把它作为现成的元素传下去。这样的布局组件变得可以复用,现在加一个 onLogout 只需要改 App 和 Avatar。每次点击时两者仍然会打印日志,因为 App 每次渲染都会传给它们一个新元素:组合简化的是代码,而不是渲染次数。children 一页介绍了更多插槽写法。
当拥有数据的组件也能决定深层部分长什么样时,组合就能奏效。如果深层组件位于某个库内部,或位于数据拥有者并不渲染的结构中,组合就帮不上忙了。
解决办法 2:context
当不同深度的很多组件都需要同一个值时(主题、当前登录的用户、当前语言),就把它放进 context。provider 之下的任何组件都能用 useContext 读取它,中间的组件一处都不用改。
Avatar 和 Greeting 在两个不同的深度读取 user,而 Page 和 Sidebar 不接收任何 props。在 Page 之下任意位置再加第三个读取者,无需改动其他代码就能工作。代价是:数据流不再体现在 props 中,而且值变化时每个读取者都会重新渲染。useContext 一页介绍了 provider、默认值以及这些重新渲染。
解决办法 3:状态管理库
对于很多组件都会读取、而且经常变化的大型应用 state(购物车、文档编辑器、实时数据),Redux Toolkit、Zustand 或 Jotai 等状态管理库可以让每个组件只订阅它用到的那一部分。这样既避免了 prop drilling,也避免了一个大 context 带来的“所有使用者都重新渲染”的代价。但它也意味着多一个依赖、多一套概念,所以要在前两种办法不够用时才使用,而不是为了一个只传三层的 prop。
// Zustand, for comparison (not available in the editor here)
const useCart = create((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
}));
function CartCount() {
const count = useCart((s) => s.items.length); // renders only when the count changes
return <span>{count}</span>;
}
什么时候 prop drilling 没问题
传递 props 本来就是 React 的数据流方式,也是最容易阅读的写法:只看 JSX,你就能从上到下跟踪一个值。一个 prop 经过一两个会用到其中一部分的组件,或者经过一小串紧密相关的组件,这都不是需要解决的问题。把 state 提升到最近的共同父组件,往下传,只有当中间层开始携带它们根本用不上的 props 时,再换别的方法。
常见问题
React 中的 prop drilling 是什么?
把一个 prop 穿过好几层并不使用它的组件往下传,只为了让组件树深处的某个组件能读取它。中间的每个组件都接收这个 prop,再把它转发下去。
prop drilling 不好吗?
本身并不坏。把一个值往下传两三层,清晰明确,也容易跟踪。当很多层都带着它们从不使用的 props 时,它才会成为问题,因为每次改名或新增字段,都要改动一堆与之无关的文件。
如何避免 prop drilling?
先试试组合:让父组件自己构建深层组件,再作为 children 或其他 prop 传下去,这样中间层就完全接触不到这些数据。如果不同深度的很多组件都需要这个值,就用 context。对于体量大且变化频繁的 state,可以考虑状态管理库。
context 是解决 prop drilling 的唯一办法吗?
不是,而且往往不是最好的办法。用 children 进行组合,无需新增 context 就能消除 prop drilling,还能让数据流在代码中保持清晰可见。