# 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