Tutorial
Learn EveryState from scratch. By the end, you'll understand the core API, framework integrations, and patterns for building real applications.
Install
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
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
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
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
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);
});
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
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
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:
| Namespace | Purpose | Who writes | Who reads |
|---|---|---|---|
state.* | Authoritative application state | Subscribers (logic) | Components |
derived.* | Computed projections of state | Subscribers (derived) | Components |
intent.* | Write-only signals from the UI | Components | Subscribers (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);
});
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.
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.
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');
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
- Core API Reference - full API docs for
@everystate/core - React Integration - hooks, patterns, and testing
- Vue Integration - composables and Pinia comparison
- CSS Engine - design tokens, reactive CSSOM, theming
- Full Ecosystem - router, renderer, view, perf, types, aliases