@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.
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:
- Create a
refwith the current value from the store - Subscribe to the path when the component mounts (
onMounted) - Update the
refwhen the store notifies - Unsubscribe when the component unmounts (
onBeforeUnmount) - Return a
computedfor 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)
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()
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)
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)
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)
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)
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
| Concern | Pinia | EveryState + Vue |
|---|---|---|
| Reactivity | Automatic (proxy-based) | Explicit (subscribe + ref) |
| Actions | Store methods | Store subscribers |
| Framework coupling | Vue-only | Framework-agnostic |
| DI mechanism | defineStore | provide/inject |
| Testing | Needs Vue test utils | Pure state in → state out |
| DevTools | Vue DevTools integration | Path 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.