UNPKG

vue-atoms

Version:

Better type-safe provide() and inject() for Vue

145 lines (116 loc) 4.17 kB
# vue-atoms _Note: This is pre-1.0 so feedback welcome, and API is not yet stable. Right now, this is designed as a drop-in, type-safe replacement to `provide()` and `inject()`._ ## Installation ```sh npm install vue-atoms ``` ## Usage ```ts import { atom } from 'vue-atoms' export const counterAtom = atom(0) ``` ```vue <script setup lang="ts"> import { inject } from 'vue-atoms' import { counterAtom } from './atoms' const counter = inject(counterAtom) </script> <template> <div>{{ counter }}</div> <button @click="counter++">Increment</button> </template> ``` ## Explanation ### The Problem with `provide()` and `inject()` The `provide()` and `inject()` Vue API is one of the more awkward parts, especially when it comes to TypeScript and type-safety. To get proper types (sort of), Vue asks you to do a few things. 1. Use a symbol as a key. 2. Type your symbol as `InjectionKey<{type}>` This looks like: ```ts import { provide } from 'vue' export const key = Symbol() as InjectionKey<string> provide(key, 'foo') ``` When consuming the value, to get the type, you then do: ```ts import { inject } from 'vue' import { key } from './injection-keys' const foo = inject(key) ``` **This causes a number of side-effects / problems.** For one, the value of `foo` is not guaranteed, meaning that the value is always returned as `T | undefined` even if you gave an explicit type for `T`. Vue tells you to workaround this by using `as` again like: ```ts const foo = inject(key) as string ``` This can lead to unexpected runtime errors in your code, because `as` essentially circumvents any type-checking. Meaning, the official Vue documentation for typing provide / inject is both [an abuse of the type system](https://github.com/microsoft/TypeScript/issues/54885#issuecomment-1620688284), and represents poor TypeScript practices. Furthermore, because Vue is abusing the type system, your symbol key is no longer recognized as a symbol. This can lead to type errors when using Vue Test Utils, like so: ```ts mount(MyComponent, { global: { provide: { // Throws TypeScript error: "A computed property name must be of type 'string', 'number', 'symbol', or 'any'" [key]: 'value' } } }) ``` ### A better type-safe `provide()` and `inject()` for Vue Inspired by [React Context](https://react.dev/learn/passing-data-deeply-with-context) and [Jotai](https://jotai.org/), `vue-atoms` creates small pieces of state called "atoms", which have a default value, and are typed either implicitly by the value, or by an explicit type. First, you create an atom: ```ts import { ref } from 'vue' import { atom } from 'vue-atoms' export const counterAtom = atom(ref(0)) ``` In your Vue component, you inject the value like you normally would. However, the atom does not need an explicit provider, and if one isn't found, will use the default value. ```vue <script setup lang="ts"> import { inject } from 'vue-atoms' import { counterAtom } from './atoms' // type is Ref<number> with a value of `0` const counter = inject(counterAtom) </script> <template> <div>{{ counter }}</div> <button @click="counter++">Increment</button> </template> ``` If you wish to provide a new value for part of the component tree, you can do so like the following: ```vue <script setup lang="ts"> import { ref } from 'vue' import { provide } from 'vue-atoms' import { counterAtom } from './atoms' // The value for `provide` is type-checked to be of the same type as your atom. provide(counterAtom, ref(100)) </script> <template> <Consumer /> </template> ``` You can also compute values from atoms to provide for deeper consumers: ```vue <script setup lang="ts"> import { inject, provide } from 'vue-atoms' import { computed } from 'vue' import { counterAtom } from './atoms' const counter = inject(counterAtom) const computedCounter = computed(() => counter.value + 10) provide(counterAtom, computedCounter) </script> ``` ### Atoms are symbols! This will now work: ```ts // ... test mount(MyComponent, { global: { provide: { [counterAtom]: ref(10) } } }) ``` ## Demo See: https://stackblitz.com/edit/vitejs-vite-baobw8?file=src%2FApp.vue