前言:一种名为"透传焦虑"的怪病

接上回。很多兄弟在说:"我也想把 Redux 删了,但是我组件层级太深了啊!我不把数据放在 Store 里,难道要我把 user 属性从爷爷传给爸爸,爸爸传给儿子,儿子传给孙子,孙子再传给那条狗吗?"
这就是传说中的 Props Drilling(属性透传/打洞)。
听起来确实很烦。为了解决这个痛点,很多人反手就是一个 Context,或者把数据扔进 Zustand。
但今天要告诉你一个残酷的真相:大部分 Props Drilling 根本不需要 Context,也不需要状态管理库。
你觉得痛苦,仅仅是因为你把组件写成了"俄罗斯套娃",而忘记了 React 最原本的样子------乐高积木(Component Composition)。
案发现场:痛苦的"俄罗斯套娃"
先看这段让你血压升高的代码。假设我们需要在页面深处的导航栏里展示头像。
tsx
// ❌ 典型的打洞现场
const App = () => {
const [user] = useState({ name: 'Jack', avatar: '...' });
// App 只想渲染个布局,却被迫要把 user 传下去
return <Layout user={user} />;
};
const Layout = ({ user }) => {
// Layout 内心 OS:我只是个搞排版的,为什么要拿 user?
// 只能含泪继续往下传
return (
<div className="layout">
<Header user={user} />
<MainContent />
</div>
);
};
const Header = ({ user }) => {
// Header 内心 OS:我也不用 user,是那个角落里的 Avatar 要用啊!
return (
<header>
<Logo />
<Avatar user={user} />
</header>
);
};
const Avatar = ({ user }) => {
// 终于到了...
return <img src={user.avatar} />;
};
痛点分析: Layout 和 Header 成了无辜的"运钞车"。它们根本不关心 user 是谁,但为了这一行数据,它们被迫接受 props,再原封不动地传下去。如果哪天 Avatar 还需要个 Click 事件,这整条链路上的组件都要改一遍。
这就是耦合。
破局:把控制权交还给"上帝"
怎么解?其实很简单。 你回想一下,HTML 里的 <div> 是怎么用的?
<header>...</header>
</div>
div 从来不过问里面装的是 header 还是 span。它只是个容器。
在 React 里,我们有一个神奇的属性叫 children。
✅ 方案一:利用 children 做"填空题"
我们把 Layout 变成一个真正的容器,不再让它负责生产 Header,而是负责展示传进来的东西。
//
const Layout = ({ children }) => {
return <div className="layout">{children}</div>;
};
// 改造后的 App:也就是"上帝"组件
const App = () => {
const [user] = useState({ name: 'Jack' });
return (
<Layout>
{/* 💥 见证奇迹的时刻! */}
{/* Header 直接在这里渲染,user 直接传给它,根本不需要经过 Layout! */}
<Header user={user} />
</Layout>
);
};
发现了吗? Layout 不再接收 user 属性了。Props Drilling 消失了 。 数据流变成了:App -> Header -> Avatar。中间那个"中间商" Layout 被架空了。
✅ 方案二:如果有多个坑位怎么办?(Slot 模式)
有人会问:"我的 Layout 很复杂,除了中间的内容,左边还要个侧边栏,上面还要个头部,光一个 children 不够用啊。"
兄弟,谁告诉你 Props 只能传字符串的?Props 可以传组件啊!
这叫 "控制反转" (Inversion of Control) ,俗称"挖坑填萝卜"。
//
const Layout = ({ topSlot, leftSlot, children }) => {
return (
<div className="layout">
<div className="top-bar">{topSlot}</div>
<div className="container">
<aside>{leftSlot}</aside>
<main>{children}</main>
</div>
</div>
);
};
// 上帝组件 App
const App = () => {
const [user] = useState({ name: 'Jack' });
return (
<Layout
// 就像拼乐高一样,把组装好的组件塞进去
topSlot={<Header user={user} />}
leftSlot={<Sidebar />}
>
<Dashboard />
</Layout>
);
};
绝杀! 现在,Layout 彻底变成了一个纯粹的 UI 骨架。它完全不知道 user 数据的存在。 你想给 Header 加 props?你想换掉 Sidebar?直接在 App 里改就行,Layout 代码一行都不用动。
核心哲学:组合 vs 继承
这就是 React 文档里那句晦涩的**"推荐使用组合(Composition)而非继承"**的真谛。
Props Drilling 的本质问题,不是层级太深,而是职责分配不清。
- 父组件(Layout):不该管它包含的内容具体需要什么数据。
- 子组件(Header):不该依赖父组件去"喂"它数据,它应该直接从源头获取,或者由更上层的"上帝"组装好给它。
当你学会了用 children 和 ReactNode props 来传递组件时,你会发现 80% 的 Context 都是多余的。
Context 是留给那些真正的 隐式全局变量的(比如 Theme, Locale, CurrentUserContext)。 而对于页面结构层级的传值,组合(Composition) 才是性能最好、代码最解耦的方案。
总结
别一看到数据要传两层以上就想建 Store。
- 想一想:中间那个组件真的需要在这个数据链条里吗?
- 试一试:能不能把下层组件"提升(Lift up)",在父级组装好,直接传下去?
- 用一用 :
children 和"组件槽(Slots)"是 React 给你最好的礼物。
把你的组件想象成相框 ,而不是水管。相框只负责展示照片,它不管照片里的人是谁。
好了,我要去把那个传了 8 层 props 的屎山代码重构了,祝我的键盘安好。

下期预告 :现在你已经会拼积木了,但你的积木(组件)内部是不是还写了一堆 useEffect 和业务逻辑? 下一篇,我们要聊聊 "Headless 组件"与"自定义 Hook" 。教你如何把 UI 和 逻辑彻底剥离,写出那种让同事看了想给你磕头的优雅代码。
<header>...</header>
</div>
div 从来不过问里面装的是 header 还是 span。它只是个容器。
在 React 里,我们有一个神奇的属性叫 children。
✅ 方案一:利用 children 做"填空题"
我们把 Layout 变成一个真正的容器,不再让它负责生产 Header,而是负责展示传进来的东西。
//
const Layout = ({ children }) => {
return <div className="layout">{children}</div>;
};
// 改造后的 App:也就是"上帝"组件
const App = () => {
const [user] = useState({ name: 'Jack' });
return (
<Layout>
{/* 💥 见证奇迹的时刻! */}
{/* Header 直接在这里渲染,user 直接传给它,根本不需要经过 Layout! */}
<Header user={user} />
</Layout>
);
};
发现了吗? Layout 不再接收 user 属性了。Props Drilling 消失了 。 数据流变成了:App -> Header -> Avatar。中间那个"中间商" Layout 被架空了。
✅ 方案二:如果有多个坑位怎么办?(Slot 模式)
有人会问:"我的 Layout 很复杂,除了中间的内容,左边还要个侧边栏,上面还要个头部,光一个 children 不够用啊。"
兄弟,谁告诉你 Props 只能传字符串的?Props 可以传组件啊!
这叫 "控制反转" (Inversion of Control) ,俗称"挖坑填萝卜"。
//
const Layout = ({ topSlot, leftSlot, children }) => {
return (
<div className="layout">
<div className="top-bar">{topSlot}</div>
<div className="container">
<aside>{leftSlot}</aside>
<main>{children}</main>
</div>
</div>
);
};
// 上帝组件 App
const App = () => {
const [user] = useState({ name: 'Jack' });
return (
<Layout
// 就像拼乐高一样,把组装好的组件塞进去
topSlot={<Header user={user} />}
leftSlot={<Sidebar />}
>
<Dashboard />
</Layout>
);
};
绝杀! 现在,Layout 彻底变成了一个纯粹的 UI 骨架。它完全不知道 user 数据的存在。 你想给 Header 加 props?你想换掉 Sidebar?直接在 App 里改就行,Layout 代码一行都不用动。
核心哲学:组合 vs 继承
这就是 React 文档里那句晦涩的**"推荐使用组合(Composition)而非继承"**的真谛。
Props Drilling 的本质问题,不是层级太深,而是职责分配不清。
- 父组件(Layout):不该管它包含的内容具体需要什么数据。
- 子组件(Header):不该依赖父组件去"喂"它数据,它应该直接从源头获取,或者由更上层的"上帝"组装好给它。
当你学会了用 children 和 ReactNode props 来传递组件时,你会发现 80% 的 Context 都是多余的。
Context 是留给那些真正的 隐式全局变量的(比如 Theme, Locale, CurrentUserContext)。 而对于页面结构层级的传值,组合(Composition) 才是性能最好、代码最解耦的方案。
总结
别一看到数据要传两层以上就想建 Store。
- 想一想:中间那个组件真的需要在这个数据链条里吗?
- 试一试:能不能把下层组件"提升(Lift up)",在父级组装好,直接传下去?
- 用一用 :
children和"组件槽(Slots)"是 React 给你最好的礼物。
把你的组件想象成相框 ,而不是水管。相框只负责展示照片,它不管照片里的人是谁。
好了,我要去把那个传了 8 层 props 的屎山代码重构了,祝我的键盘安好。

下期预告 :现在你已经会拼积木了,但你的积木(组件)内部是不是还写了一堆
useEffect和业务逻辑? 下一篇,我们要聊聊 "Headless 组件"与"自定义 Hook" 。教你如何把 UI 和 逻辑彻底剥离,写出那种让同事看了想给你磕头的优雅代码。