@everystate/vue 1.0.0

Vue 3 adapter for @everystate/core. Six composables - that's the entire API. ~40 lines, zero dependencies beyond Vue and the core store.

See it in action Complete Vue + EveryState example - a full task manager with every file shown inline. Great for sharing with Vue devs.

Install

npm install @everystate/vue @everystate/core vue

Peer dependencies: @everystate/core >=1.0.5 and vue >=3.0.0.

Quick Start

<!-- App.vue -->
<script setup>
import { createEveryState } from '@everystate/core';
import { provideStore } from '@everystate/vue';

const store = createEveryState({ count: 0 });
provideStore(store);
</script>

<template>
  <Counter />
</template>
<!-- Counter.vue -->
<script setup>
import { usePath, useIntent } from '@everystate/vue';

const count = usePath('count');
const setCount = useIntent('count');
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="setCount(count + 1)">Increment</button>
  </div>
</template>

How It Works

Vue's reactivity is proxy-based (ref, reactive), automatic, and built for templates. EveryState's is path-based, explicit, and framework-agnostic. The bridge uses ref, computed, and lifecycle hooks:

  1. Create a ref with the current value from the store
  2. Subscribe to the path when the component mounts (onMounted)
  3. Update the ref when the store notifies
  4. Unsubscribe when the component unmounts (onBeforeUnmount)
  5. Return a computed for read-only template access

When store.set(path, value) is called, EveryState fires the subscription. We update the ref. Vue's reactivity system detects the change and re-renders. This is Vue's built-in reactivity combined with EveryState's explicit subscriptions.

provideStore(store)

provideStore(store: EveryStateStore) → void

Makes a store available to all descendant components via Vue's provide/inject. Call this in your root component's setup().

// App.vue - setup()
import { createEveryState } from '@everystate/core';
import { provideStore } from '@everystate/vue';

const store = createEveryState({ state: { tasks: [] } });
provideStore(store);

Same principle as React's EventStateProvider: it's dependency injection, not state management. The store's lifecycle is independent of Vue.

useStore()

useStore() → EveryStateStore

Returns the EveryState store from the injection context. Throws if provideStore() was not called in an ancestor.

const store = useStore();
const value = store.get('some.path');

usePath(path)

usePath(path: string) → ComputedRef<any>

Subscribe to a dot-path in the store. Returns a read-only computed ref that updates when the value at that path changes.

<script setup>
import { usePath } from '@everystate/vue';

const count = usePath('state.taskCount');
const filter = usePath('state.filter');
const items = usePath('derived.tasks.filtered');
</script>

<template>
  <span>{{ count }} task{{ count === 1 ? '' : 's' }}</span>
  <ul>
    <li v-for="task in items" :key="task.id">{{ task.text }}</li>
  </ul>
</template>

useIntent(path)

useIntent(path: string) → (value: any) → any

Returns a function that publishes a value to a path. Does not need to be reactive - it just writes to the store.

<script setup>
import { ref } from 'vue';
import { useIntent } from '@everystate/vue';

const text = ref('');
const add = useIntent('intent.addTask');

const submit = () => {
  if (text.value.trim()) {
    add(text.value);
    text.value = '';
  }
};
</script>

<template>
  <input v-model="text" @keydown.enter="submit" />
  <button @click="submit">Add</button>
</template>

useWildcard(path)

useWildcard(wildcardPath: string) → ComputedRef<any>

Subscribe to a wildcard path. Returns the parent object as a computed ref. Re-renders when any child changes.

const user = useWildcard('state.user.*');
// Reactive when state.user.name, state.user.email, etc. change

useAsync(path)

useAsync(path: string) → { data: ComputedRef, status: ComputedRef, error: ComputedRef, execute: Function, cancel: Function }

Async data fetching with reactive status tracking. All returned values are computed refs.

<script setup>
import { onMounted } from 'vue';
import { useAsync } from '@everystate/vue';

const { data, status, error, execute } = useAsync('users');

onMounted(() => {
  execute((signal) =>
    fetch('/api/users', { signal }).then(r => r.json())
  );
});
</script>

<template>
  <div v-if="status === 'loading'">Loading...</div>
  <div v-else-if="error">Error: {{ error }}</div>
  <ul v-else>
    <li v-for="u in data" :key="u.id">{{ u.name }}</li>
  </ul>
</template>

Component Patterns

Read-Only Component

<script setup>
import { usePath } from '@everystate/vue';
const count = usePath('state.taskCount');
</script>

<template>
  <header>{{ count }} task{{ count === 1 ? '' : 's' }}</header>
</template>

Read + Publish Component

<script setup>
import { usePath, useIntent } from '@everystate/vue';

const filter = usePath('state.filter');
const setFilter = useIntent('intent.changeFilter');
</script>

<template>
  <button :class="{ active: filter === 'all' }" @click="setFilter('all')">All</button>
  <button :class="{ active: filter === 'active' }" @click="setFilter('active')">Active</button>
  <button :class="{ active: filter === 'completed' }" @click="setFilter('completed')">Completed</button>
</template>

Derived + Multiple Intents

<script setup>
import { usePath, useIntent } from '@everystate/vue';

const items = usePath('derived.tasks.filtered');
const toggle = useIntent('intent.toggleTask');
const del = useIntent('intent.deleteTask');
</script>

<template>
  <ul v-if="items && items.length">
    <li v-for="task in items" :key="task.id">
      <input type="checkbox" :checked="task.completed" @change="toggle(task.id)" />
      <span :class="{ completed: task.completed }">{{ task.text }}</span>
      <button @click="del(task.id)">Delete</button>
    </li>
  </ul>
  <div v-else>No tasks yet.</div>
</template>

Comparison to Pinia

ConcernPiniaEveryState + Vue
ReactivityAutomatic (proxy-based)Explicit (subscribe + ref)
ActionsStore methodsStore subscribers
Framework couplingVue-onlyFramework-agnostic
DI mechanismdefineStoreprovide/inject
TestingNeeds Vue test utilsPure state in → state out
DevToolsVue DevTools integrationPath introspection built-in

Use Pinia when: you're building a Vue-only app, you want automatic reactivity tracking, you want Vue DevTools integration.

Use EveryState when: you need framework independence, you want explicit testable boundaries, you're sharing state across multiple rendering layers, or you prefer intent-driven architecture.

Testing Without Vue

Because business logic lives in subscribers, you can test without Vue:

import { store } from './store.js';

test('adding a task increments taskCount', () => {
  const before = store.get('state.taskCount');
  store.set('intent.addTask', 'test task');
  const after = store.get('state.taskCount');
  expect(after).toBe(before + 1);
});

No mount(). No wrapper.find(). No async waiting. Just state in, state out.