Skip to content

matthew-dean/vue-atoms

Repository files navigation

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.

  1. Use a symbol as a key.
  2. 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

About

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

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published