vue-atoms v0.2.0
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
npm install vue-atoms
Usage
import { atom } from 'vue-atoms'
export const counterAtom = atom(0)
<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.
- Use a symbol as a key.
- Type your symbol as
InjectionKey<{type}>
This looks like:
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:
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:
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, 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:
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 and Jotai, 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:
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.
<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:
<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:
<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:
// ... test
mount(MyComponent, {
global: {
provide: {
[counterAtom]: ref(10)
}
}
})
Demo
See: https://stackblitz.com/edit/vitejs-vite-baobw8?file=src%2FApp.vue