Learn
Vue/11-provide-inject

provide / inject 跨层级状态

当数据要在「祖 → 孙」甚至更深层传递时,一层层 props 往下传(props 透传 / prop drilling)既啰嗦又脆弱。provide / inject 让祖先直接把数据「注入」到任意后代,跳过中间层。

1. 基本用法

<!-- 祖先 Ancestor.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue';
const theme = ref('dark');
provide('theme', theme); // 第一个参数是 key(字符串或 Symbol)
</script>
<!-- 后代(任意深度)Descendant.vue -->
<script setup lang="ts">
import { inject } from 'vue';
const theme = inject('theme'); // 拿到的类型和 provide 时一致(这里是 Ref<string>)
</script>

inject 的第二个参数是默认值(找不到 provider 时用):

const theme = inject('theme', ref('light'));

2. 类型安全:用 InjectionKey

字符串 key 容易拼写错且丢失类型。推荐用 InjectionKey 把 key 和值类型绑在一起:

// keys.ts
import { inject, provide, type InjectionKey, ref } from 'vue';
const ThemeKey: InjectionKey<ReturnType<typeof ref<string>>> = Symbol('theme');
 
// 提供方
provide(ThemeKey, ref('dark'));
// 使用方:自动推断出 Ref<string>,无需默认值也能拿到正确类型
const theme = inject(ThemeKey)!;

! 表示你确信上层一定提供了。若想更稳妥,给默认值:inject(ThemeKey, ref('light'))。

3. 提供响应式状态

provide 一个 ref / reactive,后代 inject 拿到的是同一个引用——祖先改了,后代自动更新:

// 祖先
const user = reactive({ name: 'Ada', age: 20 });
provide(UserKey, user);
 
// 后代
const user = inject(UserKey)!;
user.age++; // 祖先的 user 同步变化(共享同一 reactive)

⚠️ 这让后代也能直接改祖先状态。若想「只读暴露」,用 readonly 包一层再 provide:

import { provide, reactive, readonly } from 'vue';
const user = reactive({ name: 'Ada' });
provide(UserKey, readonly(user)); // 后代只能读,改会报警

4. provide 一个修改函数(受控写入)

更清晰的「只读数据 + 显式方法」模式,类似 store 的 action:

// 祖先
const count = ref(0);
provide(CounterKey, {
  count: readonly(count),
  inc: () => count.value++,
});
 
// 后代
const { count, inc } = inject(CounterKey)!;
inc(); // 通过提供的方法修改,而非直接碰 state

5. 什么时候用、什么时候不用

场景选择
父子直接通信props + emit
祖孙 / 跨多层、且不常变provide / inject
全局、多组件共享、需要 DevTools/持久化/严格管控Pinia(下一章)
仅某一处 UI 主题provide 一个 readonly 主题对象

provide/inject 不是「全局状态」的替代品:它绑定在组件树的一条路径上,卸载即消失,也没有 Pinia 的调试能力。需要真正的全局共享状态时用 Pinia。

小结

  • provide(key, value) 在祖先注入,inject(key, 默认值?) 在后代取出,跳过中间层 props 透传
  • 用 InjectionKey<T> 获得类型安全,避免字符串 key 拼写错
  • provide 响应式对象(ref/reactive)时后代共享同一引用;想只读用 readonly 包一层
  • 推荐「readonly(state) + 提供修改函数」的受控模式
  • 全局强共享状态交给 Pinia,别滥用 provide/inject
🎯练习 1:主题切换

用 provide/inject + InjectionKey 实现祖先提供一个 theme('light' | 'dark')的 ref,后代一个按钮切换主题,并让后代的文字颜色随主题变化。

🎯练习 2:只读暴露 + 方法修改

把上一题改成「后代不能直接改 theme,只能通过祖先提供的 toggle() 方法切换」,用 readonly + 函数实现。

🎯练习 3:provide/inject 还是 Pinia?

下面两个场景分别该用 provide/inject 还是 Pinia?说明理由。 (a)一个深色模式开关,整站组件都要读,少数地方能切换。 (b)一个电商购物车,多页共享、需要持久化到 localStorage、需要 DevTools 调试。