从 Vue3 到 React · 快速上手系列第 8 篇:组件通信与 Context(对标插槽与 provide/inject)

面向 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 ,三步走:createContextProvideruseContext
  • 最佳实践:把 Context 封装成 Provider + useXxx 自定义 Hook,体验接近 Pinia。
  • 注意 Context 会触发全体消费者重渲染,只放低频全局数据。

下一篇讲自定义 Hook------它对标你最熟悉的 Composables,是 React 逻辑复用的核心。

相关推荐
米码收割机20 小时前
【前端】html+css 甘肃天水旅游(源码+文档)【独一无二】
前端·css·html
万亿少女的梦16820 小时前
基于Spring Boot、Vue.js与MySQL的流浪猫狗救助系统设计与实现
vue.js·spring boot·mysql·javaweb·权限管理
ITmaster073120 小时前
从前端到AI工程师:一场跨越鸿沟的真实蜕变之旅
前端·人工智能
早睡早起身体好12320 小时前
Vue 3 路由与懒加载入门:从零完成一个迷你学习中心
javascript·vue.js·学习
谷隐凡二21 小时前
【工具推荐】纯前端字数统计工具 WordCountIt 的设计与使用
前端
次次皮21 小时前
Linux 下通过 XShell 手动更新 Tomcat 应用并重启
前端·tomcat·jar
猫猫不是喵喵.21 小时前
Vue 3 项目创建指南:基于 Vue CLI 与 Vite 的两种方式
前端·javascript·vue.js
swipe21 小时前
14|(前端转全栈)商品详情高频访问怎么扛?Redis Cache Aside 实战
前端·后端·面试
swipe21 小时前
15|(前端转全栈)从一个副标题字段看懂后端完整交付链路
前端·后端·面试
卸任21 小时前
LocalTab:一个本地化、简洁且可定制的浏览器新标签页
前端·浏览器