Tutorial

Learn EveryState from scratch. By the end, you'll understand the core API, framework integrations, and patterns for building real applications.

Prerequisites Node.js 18+ and npm. Familiarity with JavaScript. No framework knowledge required - we'll show React and Vue integration as optional steps.

Install

Step 1

Install the core package

npm install @everystate/core

Or use it directly in a browser with no build step:

<script type="module">
  import { createEveryState } from 'https://esm.sh/@everystate/core';
</script>

Your First Store

Step 2

Create a store and read/write state

import { createEveryState } from '@everystate/core';

// Create a store with initial state
const store = createEveryState({
  count: 0,
  user: { name: 'Alice', email: 'alice@example.com' }
});

// Read a value
store.get('count');           // 0
store.get('user.name');       // 'Alice'
store.get('user');            // { name: 'Alice', email: '...' }
store.get();                  // entire state object

// Write a value
store.set('count', 42);
store.set('user.name', 'Bob');

// Creates intermediate objects automatically
store.set('app.settings.theme', 'dark');

That's the core API: get(path) reads, set(path, value) writes. Paths are dot-separated strings that address nested state.

Subscribe to Changes

Step 3

React to state changes

// Subscribe to a specific path
const unsub = store.subscribe('count', (value, { oldValue }) => {
  console.log(`count changed: ${oldValue} → ${value}`);
});

store.set('count', 1);  // logs: "count changed: 0 → 1"
store.set('count', 2);  // logs: "count changed: 1 → 2"

// Stop listening
unsub();

subscribe returns an unsubscribe function. Call it when you're done listening. The handler receives the new value and a detail object with path, value, and oldValue.

Nested Paths

Step 4

Work with deeply nested state

const store = createEveryState({
  app: {
    settings: { theme: 'light', lang: 'en' },
    user: { name: 'Alice', profile: { avatar: '/img/alice.png' } }
  }
});

// Read at any depth
store.get('app.settings.theme');            // 'light'
store.get('app.user.profile.avatar');       // '/img/alice.png'

// Write at any depth
store.set('app.settings.theme', 'dark');

// Subscribe to nested path
store.subscribe('app.user.name', (name) => {
  document.title = `${name}'s Dashboard`;
});

There's no limit to nesting depth. Each segment in the dot path addresses one level of the state tree.

Wildcards

Step 5

Listen to groups of changes

// Fires when ANY child of 'user' changes
store.subscribe('user.*', ({ path, value }) => {
  console.log(`${path} changed to`, value);
});

store.set('user.name', 'Bob');    // fires ✓
store.set('user.email', 'x');     // fires ✓
store.set('count', 1);            // does NOT fire ✗

// Global wildcard: fires on EVERY set() call
store.subscribe('*', ({ path, value }) => {
  console.log(`[log] ${path} =`, value);
});
How wildcards fire When you call store.set('user.profile.name', 'Bob'), subscribers fire in order: exact (user.profile.name) → parent wildcard (user.profile.*) → grandparent wildcard (user.*) → global (*). This mirrors DOM event bubbling.

Async Operations

Step 6

Fetch data with automatic status tracking

// setAsync manages loading → success/error lifecycle
await store.setAsync('users', async (signal) => {
  const res = await fetch('/api/users', { signal });
  return res.json();
});

// Three sub-paths are managed automatically:
store.get('users.status');  // 'success'
store.get('users.data');    // [...the fetched data]
store.get('users.error');   // null

// Subscribe to status for loading spinners
store.subscribe('users.status', (status) => {
  spinner.hidden = status !== 'loading';
});

// Cancel an in-flight request
store.cancel('users');  // status → 'cancelled'

If setAsync is called again while a request is in-flight, the previous request is automatically aborted. No race conditions.

Batch Updates

Step 7

Group multiple updates

// Without batch: each set() fires subscribers immediately
store.set('form.name', 'Alice');    // fires subscribers
store.set('form.email', 'a@b.com'); // fires subscribers

// With batch: subscribers fire once after all sets
store.batch(() => {
  store.set('form.name', 'Alice');
  store.set('form.email', 'a@b.com');
});
// subscribers fire once here

// Or use setMany for atomic multi-path updates
store.setMany({
  'ui.route.view': 'dashboard',
  'ui.route.path': '/dashboard',
  'ui.route.params': {},
});

Three Namespaces Pattern

The recommended convention for structuring your store. This is not enforced - it's a pattern that scales well:

NamespacePurposeWho writesWho reads
state.*Authoritative application stateSubscribers (logic)Components
derived.*Computed projections of stateSubscribers (derived)Components
intent.*Write-only signals from the UIComponentsSubscribers (logic)
const store = createEveryState({
  state:   { tasks: [], filter: 'all', taskCount: 0 },
  derived: { tasks: { filtered: [] } },
});

// UI publishes intent
store.set('intent.addTask', 'Buy milk');

// Subscriber handles business logic
store.subscribe('intent.addTask', (text) => {
  const tasks = store.get('state.tasks') || [];
  store.set('state.tasks', [...tasks, { id: Date.now(), text, done: false }]);
  store.set('state.taskCount', tasks.length + 1);
});
Model–View–Intent (MVI) state.* is the Model, derived.* is the ViewModel, intent.* is the Controller. All inside a single flat state tree. Components just read and publish - they never compute.

With React

npm install @everystate/react
import { createEveryState } from '@everystate/core';
import { EventStateProvider, usePath, useIntent } from '@everystate/react';

// Store lives OUTSIDE React
const store = createEveryState({ state: { count: 0 } });

store.subscribe('intent.increment', () => {
  store.set('state.count', store.get('state.count') + 1);
});

function Counter() {
  const count = usePath('state.count');
  const increment = useIntent('intent.increment');
  return <button onClick={() => increment(true)}>Count: {count}</button>;
}

function App() {
  return (
    <EventStateProvider store={store}>
      <Counter />
    </EventStateProvider>
  );
}

usePath reads from the store and re-renders only when that path changes. useIntent returns a stable setter. Built on React 18's useSyncExternalStore.

Full React API Reference →

With Vue

npm install @everystate/vue
// eventStateVue.js - the composables
import { provideStore, usePath, useIntent } from '@everystate/vue';

// App.vue setup()
import { createEveryState } from '@everystate/core';
const store = createEveryState({ state: { count: 0 } });
provideStore(store);
<!-- Counter.vue -->
<script setup>
import { usePath, useIntent } from '@everystate/vue';
const count = usePath('state.count');
const setCount = useIntent('state.count');
</script>

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

usePath returns a read-only computed ref. Under the hood it creates a ref, subscribes on mount, and unsubscribes on unmount.

Full Vue API Reference →

With Vanilla JS

<!DOCTYPE html>
<html>
<body>
  <h1 id="count">0</h1>
  <button id="inc">+</button>

  <script type="module">
    import { createEveryState } from '@everystate/core';

    const store = createEveryState({ count: 0 });

    store.subscribe('count', (value) => {
      document.getElementById('count').textContent = value;
    });

    document.getElementById('inc').onclick = () => {
      store.set('count', store.get('count') + 1);
    };
  </script>
</body>
</html>

No framework, no build step. Works in any browser that supports ES modules.

Derived State

Computed values are just subscribers that write to derived.*:

function recomputeFiltered() {
  const tasks = store.get('state.tasks') || [];
  const filter = store.get('state.filter');

  const filtered = tasks.filter(t => {
    if (filter === 'active') return !t.done;
    if (filter === 'completed') return t.done;
    return true;
  });

  store.set('derived.tasks.filtered', filtered);
}

// Recompute when source data changes
store.subscribe('state.tasks', recomputeFiltered);
store.subscribe('state.filter', recomputeFiltered);
recomputeFiltered(); // initial computation

Components read from derived.* - they never compute. This keeps views pure and logic testable.

State-Driven CSS

npm install @everystate/css

@everystate/css lets you write CSS rules as state paths. Design tokens, theme switching, and reactive styles - all through the same get/set/subscribe pattern.

import { createEveryState } from '@everystate/core';
import { createDesignSystem } from '@everystate/css';

const store = createEveryState({});
const ds = createDesignSystem(store);

// Define design tokens
ds.setTokens({
  'color.primary': '#3b82f6',
  'color.bg': '#0f172a',
  'spacing.md': '1rem',
});

// Tokens are reactive - change a token, styles update
store.set('token.color.primary', '#ef4444');

Full CSS API Reference →

Testing

Because business logic lives in subscribers (not components), you can test everything without mounting a single component:

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

// No render(). No screen.getByRole(). Just state in, state out.
test('adding a task increments count', () => {
  const before = store.get('state.taskCount');
  store.set('intent.addTask', 'test task');
  expect(store.get('state.taskCount')).toBe(before + 1);
});

test('filter shows only active tasks', () => {
  store.set('intent.addTask', 'task 1');
  store.set('intent.addTask', 'task 2');
  store.set('intent.toggleTask', store.get('state.tasks')[0].id);
  store.set('intent.changeFilter', 'active');
  expect(store.get('derived.tasks.filtered')).toHaveLength(1);
});

For structured test suites, use @everystate/test:

npm install @everystate/test
import { createEventTest } from '@everystate/test';

const t = createEventTest({ user: { name: 'Alice' } });
t.trigger('user.name', 'Bob');
t.assertPath('user.name', 'Bob');
t.assertType('user.name', 'string');
t.assertEventFired('user.name', 1);

Next Steps