VueUse createInjectionState —— 类型安全的 provide/inject 封装

作者:袖梨 2026-06-20

一、痛点:原生 provide/inject 写起来到底有多绕

跨层级共享状态,Vue 提供了三套官方解法:props 透传、provide/inject、Pinia。其中 provide/inject 是最被低估的——它专门解决"组件子树内共享上下文"的问题,比 props 透传干净,比 Pinia 轻量得多。

VueUse createInjectionState —— provide/inject 的类型安全封装

但只要真写过一次类型安全的 provide/inject,你就会同意它也是最啰嗦的

来看一段教科书写法:

 复制代码// keys.ts
import type { InjectionKey, Ref } from 'vue'export interface CounterContext {
  count: Ref<number>
  double: Readonly<Ref<number>>
  increment: () => void
}// 1) 必须手写 InjectionKey 配 Symbol
export const counterKey: InjectionKey<CounterContext> = Symbol('counter')
 复制代码<!-- Provider.vue -->
<script setup lang="ts">
import { computed, provide, ref } from 'vue'
import { counterKey, type CounterContext } from './keys'const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++// 2) provide 时类型对得上才行
provide<CounterContext>(counterKey, { count, double, increment })
</script>
 复制代码<!-- Consumer.vue -->
<script setup lang="ts">
import { inject } from 'vue'
import { counterKey } from './keys'// 3) 默认值是 undefined,要么写默认值,要么 ! 断言,要么手写 if 校验
const ctx = inject(counterKey)
if (!ctx) throw new Error('counter context not provided')const { count, double, increment } = ctx
</script>

问题集中在三点:

  1. InjectionKey 必须手动声明,每多一个上下文就多一个 Symbol;
  2. 类型要在 key、provide、inject 三处保持一致,改一处忘改另一处就静默坏掉;
  3. inject 的返回值默认是 T | undefined,业务代码里到处充斥着 !if (!ctx),要么忍受不安全的非空断言,要么写一坨防御代码。

VueUse 的 createInjectionState 就是冲着这三点来的。它用一个 composable 函数,把"声明上下文 + 提供 + 注入"三件事打包成 [useProvideXxx, useXxxState] 的配对 API。下面我们一项一项拆开讲。

本文基于 VueUse v14.3.0(2026 年 5 月发布,要求 Vue 3.5+),所有代码示例可直接运行。

二、API 详解:一对配对函数搞定所有事

2.1 最小示例

直接看官方的计数器例子(VueUse 官方文档):

 复制代码// useCounterStore.ts
import { computed, shallowRef } from 'vue'
import { createInjectionState } from '@vueuse/core'const [useProvideCounterStore, useCounterStore] = createInjectionState(
  (initialValue: number) => {
    const count = shallowRef(initialValue)
    const double = computed(() => count.value * 2)
    function increment() {
      count.value++
    }
    return { count, double, increment }
  },
)export { useProvideCounterStore, useCounterStore }
 复制代码<!-- Root.vue:在祖先里 provide -->
<script setup lang="ts">
import { useProvideCounterStore } from './useCounterStore'useProvideCounterStore(0)
</script><template>
  <slot />
</template>
 复制代码<!-- Counter.vue:在任意后代里 inject -->
<script setup lang="ts">
import { useCounterStore } from './useCounterStore'// 类型已经被推导为 { count, double, increment } | undefined
const { count, double, increment } = useCounterStore()!
</script><template>
  <button @click="increment">+</button>
  count: {{ count }} / double: {{ double }}
</template>

跟原生写法相比,一目了然的差异:

  • 没有 InjectionKey——内部自动生成 Symbol;
  • 没有 provide / inject 调用——封装在 useProvideXxx / useXxxState 里;
  • 类型从 composable 返回值自动推导——你怎么写,类型怎么来;
  • 接受参数——useProvideCounterStore(0)initialValue 透传给 composable,参数类型也被泛型推导。

2.2 配对设计哲学:明确角色边界

createInjectionState 返回的两个函数命名带有强烈的语义暗示:

  • useProvideXxx:在祖先组件调用,"我来提供这套状态";
  • useXxxState:在后代组件调用,"我要消费这套状态"。

数组解构出来的两个函数本质上是配对使用的:调用 useProvideXxx 时内部执行 composable、把返回值通过 provideLocal 注入;调用 useXxxState 时通过 injectLocal 取出。这种配对模式比单一的 hook 更清晰——你看代码就知道当前组件在状态树里扮演什么角色。

设计上还有个细节常被忽略:useProvideXxx 也会把 composable 的返回值当作普通函数返回。意思是 provider 自己也可以直接用:

 复制代码<!-- Root.vue -->
<script setup lang="ts">
const { count, increment } = useProvideCounterStore(0)// provider 自己就能消费这套状态,不需要再跑一遍 useCounterStore
console.log(count.value)
</script>

这是从 v10.5.0 PR #3387 起就支持的设计:允许 provide 和 inject 在同一组件里发生。在原生 API 里要做到这点你得在 setup() 里手动 provide 完再 inject 一次,繁琐且违反直觉。

2.3 自定义 InjectionKey

默认 key 是 Symbol(composable.name || 'InjectionState')——v10.8.0 之后会自动用 composable 的函数名作为 Symbol 描述符(PR #3788),方便调试。

但每次调用 createInjectionState 都会生成新的 Symbol。模块级别声明一次,所有组件用同一个 Symbol,没问题。但如果你需要:

  • 在多个独立打包的产物之间共享同一个上下文(micro frontend 场景);
  • 用字符串 key 方便调试或 DevTools 识别;

可以传 injectionKey 选项:

 复制代码import type { InjectionKey } from 'vue'const counterKey: InjectionKey<CounterStore> = Symbol('counter-store')const [useProvideCounterStore, useCounterStore] = createInjectionState(
  (initialValue: number) => { /* ... */ },
  { injectionKey: counterKey },
)// 也可以是字符串
// { injectionKey: 'counter-store' }

字符串 key 是 Vue inject 原生就支持的,但你失去了类型推导的桥梁,所以优先用 InjectionKey<T> 强类型

2.4 默认值与 throw 兜底:三种错误处理策略

provide 没调用就 inject,是最常见的运行时陷阱。createInjectionState 给出了三档应对:

策略 A:什么都不做(默认)

 复制代码const counter = useCounterStore() // 类型: CounterStore | undefined
counter?.increment()

useXxxState 的返回类型默认包含 undefined,调用方需要自己处理。

策略 B:传 defaultValue,自动兜底

从 v10.10.0(PR #3902)起新增。v14.3.0 进一步改进了类型(PR #5306)——一旦传了 defaultValue,返回类型就不再包含 undefined

 复制代码const [useProvideCounterStore, useCounterStore] = createInjectionState(
  (initialValue: number) => ({ /* ... */ }),
  {
    defaultValue: {
      count: shallowRef(0),
      double: shallowRef(0),
      increment: () => {},
    },
  },
)const { count, double, increment } = useCounterStore() //  不再是 undefined

适合"祖先没 provide 也能正常工作"的可选上下文。

策略 C:包一层抛错或回退函数

官方推荐的工程实践是:别直接 export useCounterStore,而是包一层

 复制代码// useCounterStore.ts
const [useProvideCounterStore, useCounterStoreRaw] = createInjectionState(
  (initialValue: number) => { /* ... */ },
)export { useProvideCounterStore }// 强制存在:祖先没 provide 直接抛错
export function useCounterStoreOrThrow() {
  const ctx = useCounterStoreRaw()
  if (ctx == null) {
    throw new Error(
      '[CounterStore] 请先在祖先组件调用 useProvideCounterStore',
    )
  }
  return ctx
}// 默认兜底:祖先没 provide 时返回缺省实现
export function useCounterStoreWithDefault() {
  return useCounterStoreRaw() ?? {
    count: shallowRef(0),
    double: shallowRef(0),
    increment: () => {},
  }
}

通过封装让 inject 端完全摆脱 undefined,再也不需要业务代码满天 ! 断言。这是 VueUse 官方文档推荐的模式。

三、源码原理浅析:本质是 provide/inject 的薄包装

打开 packages/shared/createInjectionState/index.ts,核心实现非常短:

 复制代码import type { InjectionKey } from 'vue'
import { injectLocal } from '../injectLocal'
import { provideLocal } from '../provideLocal'export function createInjectionState<Arguments extends Array<any>, Return>(
  composable: (...args: Arguments) => Return,
  options?: CreateInjectionStateOptions<Return>,
) {
  // 1) 默认用 composable 函数名作为 Symbol 描述符
  const key: string | InjectionKey<Return> =
    options?.injectionKey || Symbol(composable.name || 'InjectionState')
  const defaultValue = options?.defaultValue  // 2) provider 端:执行 composable + provide
  const useProvidingState = (...args: Arguments) => {
    const state = composable(...args)
    provideLocal(key, state)
    return state
  }  // 3) consumer 端:从同一个 key 注入
  const useInjectedState = () => injectLocal(key, defaultValue)  return [useProvidingState, useInjectedState]
}

总共不到 20 行。但有两个值得拿出来说的细节:

1. provideLocal / injectLocal,不是原生的 provide / inject

这是 VueUse 自己的小封装。原生 Vue 的 provide 不允许在同一个组件里 provide 完立即 inject 同一个 key(取出来的还是上层的 provide)。provideLocal 通过把 provide 内容直接挂到当前实例的本地缓存里,让"自身组件 inject 自身 provide 的值"成为可能。

这是 PR #3387 引入的能力,官方文档里"provider 自己也能用 store"那一段就是建立在这个机制上。

2. 函数重载提供两套类型签名。

 复制代码// 重载 1:传了 defaultValue,返回类型不带 undefined
export function createInjectionState<Args, Return>(
  composable: (...args: Args) => Return,
  options: { defaultValue: Return } & CreateInjectionStateOptions<Return>,
): CreateInjectionStateReturn<Args, Return, Return>// 重载 2:没传 defaultValue,返回类型带 undefined
export function createInjectionState<Args, Return>(
  composable: (...args: Args) => Return,
  options?: CreateInjectionStateOptions<Return>,
): CreateInjectionStateReturn<Args, Return, Return | undefined>

通过函数重载,defaultValue 的存在与否会精确反映在 useXxxState 的返回类型上。这是 v14.3.0 PR #5306 加的能力——之前的版本无论传不传 defaultValue,类型都会带上 undefined,开发者反复要去断言;现在终于"传了就保证有"。

理解到这层,你就能感受到 VueUse 对类型安全的执念:接口没变,但开发体验提升一个台阶

四、实战:四个跨层级状态共享的真实场景

4.1 表单上下文:让任意嵌套字段共享校验状态

复杂表单经常有"嵌套区域 + 全局校验"的需求。比如下面这个用户资料表,外层管理整体禁用、是否提交中、字段错误集合:

 复制代码// useFormContext.ts
import { computed, reactive, shallowRef } from 'vue'
import { createInjectionState } from '@vueuse/core'interface FormContextOptions {
  initialValues?: Record<string, any>
}const [useProvideFormContext, useFormContextRaw] = createInjectionState(
  (options: FormContextOptions = {}) => {
    const values = reactive<Record<string, any>>({ ...options.initialValues })
    const errors = reactive<Record<string, string>>({})
    const submitting = shallowRef(false)
    const disabled = shallowRef(false)    const isValid = computed(() => Object.keys(errors).length === 0)    function setValue(key: string, value: any) {
      values[key] = value
      delete errors[key]
    }    function setError(key: string, message: string) {
      errors[key] = message
    }    return { values, errors, submitting, disabled, isValid, setValue, setError }
  },
)export { useProvideFormContext }export function useFormContext() {
  const ctx = useFormContextRaw()
  if (!ctx) throw new Error('请在 <Form> 组件内使用表单字段')
  return ctx
}
 复制代码<!-- Form.vue:祖先 -->
<script setup lang="ts">
import { useProvideFormContext } from './useFormContext'const props = defineProps<{ initialValues?: Record<string, any> }>()
useProvideFormContext({ initialValues: props.initialValues })
</script><template>
  <form>
    <slot />
  </form>
</template>
 复制代码<!-- Field.vue:任意层级的子组件 -->
<script setup lang="ts">
import { computed } from 'vue'
import { useFormContext } from './useFormContext'const props = defineProps<{ name: string; label: string }>()
const { values, errors, disabled, setValue } = useFormContext()const value = computed({
  get: () => values[props.name] ?? '',
  set: (v) => setValue(props.name, v),
})
</script><template>
  <label>
    {{ label }}
    <input v-model="value" :disabled="disabled" />
    <em v-if="errors[name]">{{ errors[name] }}</em>
  </label>
</template>

无论 <Field> 嵌套多深、外层套了几层 layout 容器,它始终能拿到表单上下文。整套机制比 Pinia 轻得多——它只在这棵子树里有效,多个 <Form> 互不干扰。

4.2 Tab 容器:父子组件解耦的经典场景

<Tabs><Tab> 是 provide/inject 的教科书用法。用 createInjectionState 改写:

 复制代码// useTabsContext.ts
import { shallowRef } from 'vue'
import { createInjectionState } from '@vueuse/core'const [useProvideTabsContext, useTabsContextRaw] = createInjectionState(
  (initial?: string) => {
    const active = shallowRef(initial ?? '')
    const tabs = shallowRef<{ name: string; label: string }[]>([])    function register(name: string, label: string) {
      if (!tabs.value.some(t => t.name === name)) {
        tabs.value = [...tabs.value, { name, label }]
        if (!active.value) active.value = name
      }
    }    function unregister(name: string) {
      tabs.value = tabs.value.filter(t => t.name !== name)
    }    return { active, tabs, register, unregister }
  },
)export { useProvideTabsContext }
export function useTabsContext() {
  const ctx = useTabsContextRaw()
  if (!ctx) throw new Error('<Tab> 必须在 <Tabs> 内使用')
  return ctx
}
 复制代码<!-- Tabs.vue -->
<script setup lang="ts">
import { useProvideTabsContext } from './useTabsContext'const props = defineProps<{ modelValue?: string }>()
const { active, tabs } = useProvideTabsContext(props.modelValue)
</script><template>
  <div class="tabs">
    <nav class="tabs__nav">
      <button
        v-for="t in tabs"
        :key="t.name"
        :class="{ active: t.name === active }"
        @click="active = t.name"
      >
        {{ t.label }}
      </button>
    </nav>
    <div class="tabs__panel">
      <slot />
    </div>
  </div>
</template>
 复制代码<!-- Tab.vue -->
<script setup lang="ts">
import { onBeforeUnmount, onMounted } from 'vue'
import { useTabsContext } from './useTabsContext'const props = defineProps<{ name: string; label: string }>()
const { active, register, unregister } = useTabsContext()onMounted(() => register(props.name, props.label))
onBeforeUnmount(() => unregister(props.name))
</script><template>
  <div v-show="active === name">
    <slot />
  </div>
</template>

使用方:

 复制代码<Tabs model-value="profile">
  <Tab name="profile" label="基本资料">
    <UserProfile />
  </Tab>
  <Tab name="security" label="安全设置">
    <Security />
  </Tab>
</Tabs>

<Tab> 不需要知道 <Tabs> 的内部结构,注册/注销都通过上下文调用。比起 <Tabs :tabs="..."> 的"声明式" props 配置,组合式 + provide/inject 更接近"插件化"的思维方式。

4.3 Stepper 步骤器:当前步、能否前进/后退

 复制代码// useStepperContext.ts
import { computed, shallowRef } from 'vue'
import { createInjectionState } from '@vueuse/core'const [useProvideStepperContext, useStepperContextRaw] = createInjectionState(
  (totalSteps: number) => {
    const current = shallowRef(0)    const isFirst = computed(() => current.value === 0)
    const isLast = computed(() => current.value === totalSteps - 1)    function next() {
      if (!isLast.value) current.value++
    }
    function prev() {
      if (!isFirst.value) current.value--
    }
    function goto(index: number) {
      if (index >= 0 && index < totalSteps) current.value = index
    }    return { current, isFirst, isLast, next, prev, goto }
  },
)export { useProvideStepperContext }
export function useStepperContext() {
  const ctx = useStepperContextRaw()
  if (!ctx) throw new Error('<Step*> 必须在 <Stepper> 内使用')
  return ctx
}
 复制代码<!-- Stepper.vue -->
<script setup lang="ts">
import { useProvideStepperContext } from './useStepperContext'
const props = defineProps<{ steps: number }>()
useProvideStepperContext(props.steps)
</script><template>
  <slot />
</template>
 复制代码<!-- StepNav.vue:放在表单底部当导航 -->
<script setup lang="ts">
import { useStepperContext } from './useStepperContext'
const { isFirst, isLast, next, prev } = useStepperContext()
</script><template>
  <div class="step-nav">
    <button :disabled="isFirst" @click="prev">上一步</button>
    <button :disabled="isLast" @click="next">下一步</button>
  </div>
</template>

跟前面的 Tab 同理,<StepNav> 完全不需要 props,它从上下文里自取所需。重点是这种状态绝对不该上 Pinia——它跟当前 <Stepper> 实例强绑定,全局化反而是负担。

4.4 表格行选:跨表头/行/批量操作栏共享选中集合

 复制代码// useSelectionContext.ts
import { computed, reactive } from 'vue'
import { createInjectionState } from '@vueuse/core'interface SelectionContextOptions<T> {
  itemKey: (item: T) => string
}const [useProvideSelectionContext, useSelectionContextRaw] =
  createInjectionState(<T,>(options: SelectionContextOptions<T>) => {
    const selected = reactive(new Set<string>())    function isSelected(item: T) {
      return selected.has(options.itemKey(item))
    }    function toggle(item: T) {
      const key = options.itemKey(item)
      selected.has(key) ? selected.delete(key) : selected.add(key)
    }    function clear() {
      selected.clear()
    }    function selectAll(items: T[]) {
      items.forEach(item => selected.add(options.itemKey(item)))
    }    const count = computed(() => selected.size)    return { selected, isSelected, toggle, clear, selectAll, count }
  })export { useProvideSelectionContext }
export function useSelectionContext<T>() {
  const ctx = useSelectionContextRaw()
  if (!ctx) throw new Error('请在 <Table> 内使用')
  return ctx as ReturnType<typeof useSelectionContextRaw> & {} // 类型见下文
}
 复制代码<!-- DataTable.vue -->
<script setup lang="ts" generic="T">
import { useProvideSelectionContext } from './useSelectionContext'const props = defineProps<{
  data: T[]
  itemKey: (item: T) => string
}>()const { selected, isSelected, toggle, selectAll, clear, count } =
  useProvideSelectionContext({ itemKey: props.itemKey })
</script><template>
  <div>
    <div v-if="count" class="batch-bar">
      已选 {{ count }} 条
      <button @click="clear">取消</button>
      <slot name="batch-actions" :selected-keys="[...selected]" />
    </div>    <table>
      <thead>
        <tr>
          <th>
            <input
              type="checkbox"
              :checked="count === data.length && count > 0"
              @change="count === data.length ? clear() : selectAll(data)"
            />
          </th>
          <slot name="header" />
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in data" :key="itemKey(item)">
          <td>
            <input type="checkbox" :checked="isSelected(item)" @change="toggle(item)" />
          </td>
          <slot name="row" :item="item" />
        </tr>
      </tbody>
    </table>
  </div>
</template>

表头里的"全选"复选框、行里的"单选"、批量操作栏里的"已选 N 条"——三处分别在三个不同的 slot 里渲染,但都基于同一个选中状态。这是 provide/inject 模式的天然战场。

五、对比矩阵:跟 Pinia / props / createSharedComposable 怎么选

每隔半年都会出现"Pinia 和 provide/inject 选哪个"的争论。createInjectionState 的位置其实非常清楚——它是 provide/inject 的现代写法,而不是 Pinia 的替代品。

方案作用范围实例隔离类型推导SSR 安全适合的场景
props 透传父子直接每实例独立一两层、字段有限
createInjectionState子树每个 provider 子树独立 完整推导表单/Tab/Stepper/表格行选等"组件族"
createSharedComposable整个客户端所有调用共享同一份 SSR 自动降级为非共享全局鼠标位置、滚动状态等"单例资源"
createGlobalState整个客户端所有调用共享 同上与 SharedComposable 类似,更轻量
Pinia应用级单例 store 一流(带 hydration)用户身份、购物车、跨路由的业务状态

几条选型直觉:

  1. 状态生命周期跟着某个父组件走createInjectionState<Form> 卸载,所有内部字段的状态一起被回收,自然且安全。
  2. 状态需要在多个互相独立的实例间隔离createInjectionState。同一个页面里两个 <Tabs>,绝不能共享 active tab;如果你用 Pinia 反而要给每个 store 加 id,繁琐。
  3. 状态确实是"全局唯一资源"createSharedComposable。比如 useMouse() 想全局只注册一个 mousemove 监听,加 createSharedComposable 包一层即可。
  4. 状态是业务状态、跨路由、需要持久化或调试 → 上 Pinia。createInjectionState 不是 store,没有 devtools 集成、没有 SSR 序列化、没有时间旅行。

特别说说 createInjectionStatecreateSharedComposable 的差异,这俩最容易搞混——名字都带 "create",都来自 @vueuse/shared

 复制代码// createSharedComposable:全应用单例
import { createSharedComposable, useMouse } from '@vueuse/core'
const useSharedMouse = createSharedComposable(useMouse)// CompA.vue
const { x, y } = useSharedMouse() // 第一次调用:注册 mousemove
// CompB.vue
const { x, y } = useSharedMouse() // 复用上一次的状态,不再注册// createInjectionState:组件树作用域
const [useProvideXxx, useXxx] = createInjectionState(/* ... */)
// 必须先在祖先组件 useProvideXxx() 才能在后代 useXxx()

核心区别createSharedComposable 是"全应用一份",没有 provider 概念;createInjectionState 是"每个 provider 子树一份",必须有祖先 provide 才能在后代使用。生命周期上,createSharedComposable 在最后一个订阅者卸载时停掉 effectScope;createInjectionState 跟着 provider 组件走。

createSharedComposable 官方文档:

createInjectionState 因为基于 provide/inject,本身就是请求隔离的,不需要这种降级。SSR 项目里更推荐 createInjectionState

六、坑与解

6.1 别忘了 provider 一定要先挂载

createReusableTemplate 一样,使用顺序是硬约束:

 复制代码<!--  会得到 undefined -->
<App>
  <UseSomething /> <!-- 这里 inject 不到 -->
  <Provider />     <!-- provider 还没出现 -->
</App><!--  -->
<App>
  <Provider>
    <UseSomething />
  </Provider>
</App>

写工具组件时养成习惯:永远封装一个 useXxxOrThrow,让缺失 provider 时直接报错而不是静默失败。这条建议来自官方文档。

6.2 异步 setup 中的 inject 时机

如果某个组件的 setupasync 的(比如里面有 top-level await),需要确认 useXxxState() 调用发生在第一个 await 之前,否则会丢失当前组件实例:

 复制代码<script setup lang="ts">
//  在 await 之前 inject
const ctx = useTabsContext()const data = await fetchData()//  在 await 之后 inject
// const ctx = useTabsContext() // 此时 currentInstance 已经变了
</script>

这是 Vue 3 inject 自身的限制,不是 VueUse 的特殊行为。

6.3 别在 createInjectionState 内部用 onMounted 等 hooks 期望它跟 consumer 绑定

composable 函数体是在 provider 组件的 setup 阶段执行的:

 复制代码const [useProvideXxx] = createInjectionState(() => {
  onMounted(() => {
    // 这是 provider 组件的 onMounted,不是 consumer
  })
  return { /* ... */ }
})

这跟普通 composable 的行为一致,但因为 createInjectionState 的"消费"动作是延迟到后代组件的,新手容易误以为生命周期会跟着 consumer 走。记住:composable 体在 useProvideXxx() 被调用的那一刻执行,且只执行一次

6.4 多个 provider 的优先级

如果在祖先链上多次调用同一个 useProvideXxx,inject 拿到的是最近的祖先那一份。这是 Vue 原生行为,可以利用它做"覆盖式上下文"(比如某个区域的表单禁用规则覆盖父级),但也容易意外。建议用 DevTools 的 Provided / Injected 面板检查实际拿到的是哪一份。

6.5 SSR 下别在 composable 里直接访问 window

createInjectionState 本身 SSR 安全,但你的 composable 函数体一旦访问 window / document,就和原生 composable 一样会爆。该 if (typeof window !== 'undefined') 还是要写。

七、版本要求与变更速览

据 VueUse createInjectionState Changelog:

版本关键变更
v14.3.0(2026-05-01)defaultValue 时返回类型不再含 undefined(#5306)
v14.0.0(2025-10)要求 Vue 3.5+,构建产物迁移
v13.6.0(2025-07-28)@__NO_SIDE_EFFECTS__ 注解,改善 tree-shaking
v12.0.0-beta.1(2024-11-21)不再支持 Vue 2
v10.10.0(2024-05-27)新增 defaultValue 选项(#3902)
v10.8.0(2024-02-20)injectionKey 默认使用 composable 名称(#3788)
v10.5.0(2023-10-07)新增 injectionKey 选项(#3404);允许在同一组件中 provide 和 inject(#3387)

新项目直接装 v14.3.0 最稳:

 复制代码npm i @vueuse/core

如果你的项目还在 Vue 2,需要锁在 v9.x

八、小结

createInjectionState 的价值不在于它能做什么——这些事情原生 provide/inject 都能做。它的价值在于把那些重复的样板代码全部砍掉,并且把 TypeScript 类型推到极致

可以这样记忆它的定位:

  • 想让 provide/inject 更好写?createInjectionState
  • 想让一棵子树共享状态?createInjectionState,不要轻易上 Pinia。
  • 想让全局只有一份资源?createSharedComposable
  • 想做应用级业务状态? 用 Pinia。

它的 API 设计——[useProvideXxx, useXxxState] 配对——是把"抽象成 store 又怕过度工程"的灰色地带做了精确的填补:你既不必手写 InjectionKey 三件套,也不必引入完整的 store 库。该有类型有类型,该有错误处理有错误处理,该轻量轻量,跟 VueUse 一贯的"够用就好"哲学一致。

下次再写跨层级状态共享,先想想:这状态真的需要 Pinia 吗?还是它只跟某个父组件相关? 如果是后者,createInjectionState 应该是你的第一反应。


参考资料:

  • VueUse createInjectionState 官方文档:vueuse.org/shared/crea…
  • 源码(GitHub):github.com/vueuse/vueu…
  • 同组件 provide+inject PR:github.com/vueuse/vueu…
  • defaultValue 选项 PR:github.com/vueuse/vueu…
  • 默认值非 undefined 类型 PR(v14.3.0):github.com/vueuse/vueu…
  • createSharedComposable 官方文档:vueuse.org/shared/crea…
  • Vue 官方 Provide / Inject 指南:cn.vuejs.org/guide/compo…
  • Composables vs. Provide/Inject vs. Pinia — When to Use What:vueschool.io/articles/vu…

相关文章

精彩推荐