EveryState + Vue 3

A complete task manager built with @everystate/vue. Every file shown below. No magic, no hidden code.

Install: npm i @everystate/core @everystate/vue vue

5 files, ~120 lines total

Store, business logic, 3 Vue components. That's the whole app.

Zero business logic in components

Components read (usePath) and publish (useIntent). Subscribers handle logic.

Testable without Vue

Every behavior is testable with store.set() + store.get(). No mount needed.

store.js store + logic
import { createEveryState } from '@everystate/core';

// ---- Store (created outside Vue) ----
export const store = createEveryState({
  state: {
    tasks: [],
    taskCount: 0,
    filter: 'all',      // 'all' | 'active' | 'completed'
  },
  derived: {
    tasks: { filtered: [] },
  },
});

// ---- Business logic (pure subscribers, no Vue imports) ----

let nextId = 1;

store.subscribe('intent.addTask', (text) => {
  const t = String(text || '').trim();
  if (!t) return;
  const tasks = store.get('state.tasks') || [];
  const next = [...tasks, { id: nextId++, text: t, completed: false }];
  store.set('state.tasks', next);
  store.set('state.taskCount', next.length);
});

store.subscribe('intent.toggleTask', (id) => {
  const tasks = store.get('state.tasks') || [];
  store.set('state.tasks',
    tasks.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
  );
});

store.subscribe('intent.deleteTask', (id) => {
  const tasks = (store.get('state.tasks') || []).filter(t => t.id !== id);
  store.set('state.tasks', tasks);
  store.set('state.taskCount', tasks.length);
});

store.subscribe('intent.changeFilter', (filter) => {
  store.set('state.filter', filter);
});

// ---- Derived state ----

function recomputeFiltered() {
  const tasks = store.get('state.tasks') || [];
  const filter = store.get('state.filter');
  const filtered = tasks.filter(t => {
    if (filter === 'active') return !t.completed;
    if (filter === 'completed') return t.completed;
    return true;
  });
  store.set('derived.tasks.filtered', filtered);
}

store.subscribe('state.tasks', recomputeFiltered);
store.subscribe('state.filter', recomputeFiltered);
recomputeFiltered();
App.vue root component
<script setup>
import { provideStore } from '@everystate/vue';
import { store } from './store.js';
import TaskInput from './TaskInput.vue';
import TaskList from './TaskList.vue';
import FilterBar from './FilterBar.vue';

// Inject the store into all descendants
provideStore(store);
</script>

<template>
  <div class="app">
    <h1>Tasks</h1>
    <TaskInput />
    <FilterBar />
    <TaskList />
  </div>
</template>
TaskInput.vue publish intent
<script setup>
import { ref } from 'vue';
import { useIntent } from '@everystate/vue';

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

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

<template>
  <div class="task-input">
    <input
      v-model="text"
      placeholder="What needs to be done?"
      @keydown.enter="submit"
    />
    <button @click="submit">Add</button>
  </div>
</template>
FilterBar.vue read + publish
<script setup>
import { usePath, useIntent } from '@everystate/vue';

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

<template>
  <div class="filter-bar">
    <span>
      {{ count }} task{{ count === 1 ? '' : 's' }}
    </span>
    <div class="filter-buttons">
      <button
        v-for="f in ['all', 'active', 'completed']"
        :key="f"
        :class="{ active: filter === f }"
        @click="setFilter(f)"
      >
        {{ f }}
      </button>
    </div>
  </div>
</template>
TaskList.vue read derived + publish 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 class="task-list" v-if="items && items.length">
    <li v-for="task in items" :key="task.id" :class="{ done: task.completed }">
      <input
        type="checkbox"
        :checked="task.completed"
        @change="toggle(task.id)"
      />
      <span class="task-text">{{ task.text }}</span>
      <button class="delete-btn" @click="del(task.id)">&times;</button>
    </li>
  </ul>
  <p class="empty" v-else>No tasks match the current filter.</p>
</template>
store.test.js test without Vue
// No Vue imports. No mount(). No wrapper.find(). Just state.
import { store } from './store.js';

test('addTask creates a task and increments count', () => {
  store.set('intent.addTask', 'Buy milk');
  expect(store.get('state.taskCount')).toBe(1);
  expect(store.get('state.tasks')[0].text).toBe('Buy milk');
});

test('toggleTask flips completed', () => {
  const id = store.get('state.tasks')[0].id;
  store.set('intent.toggleTask', id);
  expect(store.get('state.tasks')[0].completed).toBe(true);
});

test('filter shows only active tasks', () => {
  store.set('intent.addTask', 'Walk dog');
  store.set('intent.changeFilter', 'active');
  const filtered = store.get('derived.tasks.filtered');
  expect(filtered.every(t => !t.completed)).toBe(true);
});

test('deleteTask removes task and decrements count', () => {
  const before = store.get('state.taskCount');
  const id = store.get('state.tasks')[0].id;
  store.set('intent.deleteTask', id);
  expect(store.get('state.taskCount')).toBe(before - 1);
});

The Entire Vue Adapter (~40 lines)

This is the full source of @everystate/vue. No hidden abstractions.

@everystate/vue/eventStateVue.js adapter source
import { ref, computed, onMounted, onBeforeUnmount, provide, inject } from 'vue';

const STORE_KEY = Symbol('everystate');

// -- Dependency injection --

export function provideStore(store) {
  provide(STORE_KEY, store);
}

export function useStore() {
  const store = inject(STORE_KEY);
  if (!store) {
    throw new Error(
      'useStore: no store found. Call provideStore(store) in an ancestor setup().'
    );
  }
  return store;
}

// -- Read a single path --

export function usePath(path) {
  const store = useStore();
  const value = ref(store.get(path));
  let unsub = null;

  onMounted(() => {
    unsub = store.subscribe(path, (val) => {
      value.value = val;
    });
  });

  onBeforeUnmount(() => { if (unsub) unsub(); });

  return computed(() => value.value);
}

// -- Publish to a path --

export function useIntent(path) {
  const store = useStore();
  return (value) => store.set(path, value);
}

// -- Wildcard subscription --

export function useWildcard(wildcardPath) {
  const store = useStore();
  const parentPath = wildcardPath.endsWith('.*')
    ? wildcardPath.slice(0, -2)
    : wildcardPath;

  const value = ref(store.get(parentPath));
  let unsub = null;

  onMounted(() => {
    unsub = store.subscribe(wildcardPath, () => {
      value.value = store.get(parentPath);
    });
  });

  onBeforeUnmount(() => { if (unsub) unsub(); });

  return computed(() => value.value);
}

// -- Async with status tracking --

export function useAsync(path) {
  const store = useStore();
  const data   = usePath(`${path}.data`);
  const status = usePath(`${path}.status`);
  const error  = usePath(`${path}.error`);

  const execute = (fetcher) => store.setAsync(path, fetcher);
  const cancel  = () => store.cancel(path);

  return { data, status, error, execute, cancel };
}

Key Takeaways

Store lives outside Vue

Created once in store.js. Independent lifecycle. No defineStore, no reactive() wrapper.

Components are thin

usePath reads, useIntent writes. Components never compute or mutate state directly.

Logic is in subscribers

All business rules live in store.subscribe() handlers. Testable without mounting any component.

Vue reactivity still works

usePath returns a computed ref. Templates react normally. v-for, v-if, :class all work as expected.

Derived state is explicit

Filtered tasks are a subscriber that writes to derived.*. Components just read the result.

~40 lines adapter

The entire @everystate/vue package is shown above. ref + computed + provide/inject + lifecycle hooks.

Full Vue API Reference npm GitHub