面向 Vue3 开发者的 React 快速上手系列
组件之间怎么传数据?Vue 有 props/emit、插槽、provide/inject。React 全都有对应物。前面讲过 props 和回调(父子通信),这一篇补齐两块:插槽的等价物 (children 与 render props)和跨层级通信 (Context,对标 provide/inject)。
一、通信方式全景对照
| 场景 | Vue3 | React |
|---|---|---|
| 父 → 子 | props | props |
| 子 → 父 | emit 事件 |
回调函数 props |
| 内容分发 | 默认插槽 <slot> |
props.children |
| 具名插槽 | <slot name="x"> |
传 JSX 作为具名 prop |
| 作用域插槽 | <slot :data> |
render props(函数 children) |
| 跨层级 | provide/inject |
createContext + useContext |
| 全局状态 | Pinia | Zustand / Redux(第 11 篇) |
父子通信已在第 3 篇讲过,这里聚焦后三行。
二、插槽的等价物
1. 默认插槽 → children(第 3 篇已提,复习)
vue
<!-- Vue3 -->
<Card><p>内容</p></Card>
<!-- Card.vue: <div class="card"><slot /></div> -->
jsx
// React
function Card({ children }) {
return <div className="card">{children}</div>
}
<Card><p>内容</p></Card>
2. 具名插槽 → 传 JSX 当 prop
Vue 用 <template #header> 指定具名插槽。React 没有具名插槽语法,但JSX 可以当值传,所以直接用多个 prop 传不同区块:
vue
<!-- Vue3 -->
<Layout>
<template #header>顶部</template>
<template #footer>底部</template>
主体内容
</Layout>
jsx
// React:header / footer 就是普通 prop,值是 JSX
function Layout({ header, footer, children }) {
return (
<div>
<header>{header}</header>
<main>{children}</main>
<footer>{footer}</footer>
</div>
)
}
// 使用
<Layout
header={<h1>顶部</h1>}
footer={<small>底部</small>}
>
主体内容
</Layout>
心智转换:Vue 的"具名插槽"= React 的"值为 JSX 的普通 prop"。因为 JSX 是一等公民,能像任何值一样传递。
3. 作用域插槽 → render props(函数作为 children)
作用域插槽是 Vue 里较难的概念:子组件把内部数据"暴露"给父组件的插槽内容用。React 用 render props ------把 children 写成一个函数,子组件调用它并传入数据:
vue
<!-- Vue3 作用域插槽 -->
<MouseTracker v-slot="{ x, y }">
鼠标位置:{{ x }}, {{ y }}
</MouseTracker>
jsx
// React render props:children 是函数,接收子组件暴露的数据
function MouseTracker({ children }) {
const [pos, setPos] = useState({ x: 0, y: 0 })
useEffect(() => {
const handler = e => setPos({ x: e.clientX, y: e.clientY })
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
return <div>{children(pos)}</div> // 调用函数,把数据传出去
}
// 使用:children 写成函数
<MouseTracker>
{({ x, y }) => <p>鼠标位置:{x}, {y}</p>}
</MouseTracker>
不过在现代 React 里,这种"共享逻辑"的需求更多用自定义 Hook 解决(下一篇讲),render props 现在用得比以前少了。作用域插槽的逻辑复用场景,自定义 Hook 往往更优雅。
三、跨层级通信:Context 对标 provide/inject
当数据要从祖先传到很深的后代(主题、当前用户、语言等),一层层传 props 太痛苦("prop 逐层透传地狱")。Vue 用 provide/inject,React 用 Context,两者几乎一一对应。
Vue3 的 provide/inject
js
// 祖先组件提供
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
// 后代组件注入
import { inject } from 'vue'
const theme = inject('theme')
React 的 Context(三步)
第一步:创建 Context
jsx
// ThemeContext.js
import { createContext } from 'react'
export const ThemeContext = createContext('light') // 参数是默认值
第二步:用 Provider 提供值(对标 provide)
jsx
import { useState } from 'react'
import { ThemeContext } from './ThemeContext'
function App() {
const [theme, setTheme] = useState('dark')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Page /> {/* 里面无论多深都能拿到 */}
</ThemeContext.Provider>
)
}
第三步:用 useContext 消费(对标 inject)
jsx
import { useContext } from 'react'
import { ThemeContext } from './ThemeContext'
function DeepButton() {
const { theme, setTheme } = useContext(ThemeContext)
return (
<button
className={theme}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
当前主题:{theme}
</button>
)
}
React 19 提示:可以直接写
<ThemeContext value={...}>(省略.Provider),并支持用新的use(ThemeContext)读取,比useContext更灵活(能在条件语句中调用)。
四、封装 Context:最佳实践
裸用 Context 容易散乱。推荐把"创建 + Provider + 自定义 Hook"封装到一个文件,用起来像 Pinia 的 store 一样顺手:
jsx
// ThemeProvider.jsx
import { createContext, useContext, useState } from 'react'
const ThemeContext = createContext(null)
// 1. 导出 Provider 组件
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light')
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
)
}
// 2. 导出自定义 Hook,隐藏 useContext 细节
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme 必须在 ThemeProvider 内使用')
return ctx
}
使用时干净利落:
jsx
// main:包裹应用
<ThemeProvider><App /></ThemeProvider>
// 任意后代:像用 Pinia store 一样
function Header() {
const { theme, toggle } = useTheme()
return <button onClick={toggle}>{theme}</button>
}
五、Context 的性能注意点
和 Vue 不同,Context 的 value 变化会让所有消费该 Context 的组件重渲染(没有细粒度追踪)。所以:
- 别把"所有全局状态"塞进一个巨大的 Context------按领域拆分多个小 Context;
- 频繁变化的高频状态(如实时鼠标位置)不适合放 Context;
- 复杂的全局状态管理,直接上 Zustand(下下篇讲),它有更好的性能和更简洁的 API。
Context 的定位是"低频变化的全局数据":主题、登录用户、语言、权限等。
小结
- 父子通信:props(下)+ 回调(上),见第 3 篇。
- 默认插槽 =
children;具名插槽 = 值为 JSX 的 prop;作用域插槽 = render props(但优先考虑自定义 Hook)。 - 跨层级:Context 对标
provide/inject,三步走:createContext→Provider→useContext。 - 最佳实践:把 Context 封装成
Provider+useXxx自定义 Hook,体验接近 Pinia。 - 注意 Context 会触发全体消费者重渲染,只放低频全局数据。
下一篇讲自定义 Hook------它对标你最熟悉的 Composables,是 React 逻辑复用的核心。