EveryState + Solid
A complete task manager. Every file shown below.
Install: npm i @everystate/core @everystate/solid solid-js
5 files, ~140 lines
Store setup, 4 components, composition root.
Zero component re-renders
Component functions run once. Only signal accessors update.
No props drilling
Components read from the store directly.
store.js Store + Logic
import { createEveryState } from '@everystate/core';
export const store = createEveryState({
state: { tasks: [], taskCount: 0, filter: 'all' },
derived: { tasks: { filtered: [] } }
});
// Intent handlers - business logic lives here, not in components
store.subscribe('intent.addTask', (text) => {
const t = String(text || '').trim();
if (!t) return;
const tasks = store.get('state.tasks') || [];
const next = [...tasks, { id: Date.now().toString(36), text: t, completed: false, important: /!/i.test(t) }];
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(x => x.id === id ? { ...x, completed: !x.completed } : x));
});
store.subscribe('intent.deleteTask', (id) => {
const tasks = store.get('state.tasks') || [];
const next = tasks.filter(x => x.id !== id);
store.set('state.tasks', next);
store.set('state.taskCount', next.length);
});
store.subscribe('intent.changeFilter', (f) => store.set('state.filter', f));
// Derived state
const recompute = () => {
const tasks = store.get('state.tasks') || [];
const f = store.get('state.filter');
store.set('derived.tasks.filtered',
f === 'active' ? tasks.filter(t => !t.completed)
: f === 'completed' ? tasks.filter(t => t.completed)
: f === 'important' ? tasks.filter(t => t.important) : tasks);
};
store.subscribe('state.tasks', recompute);
store.subscribe('state.filter', recompute);
recompute();
Header.jsx Read-only
import { usePath } from '@everystate/solid';
import { store } from './store';
export function Header() {
const count = usePath(store, 'state.taskCount');
return (
<header>
<div class="title">Solid + EveryState</div>
<div class="badge">{count()} task{count() !== 1 ? 's' : ''}</div>
</header>
);
}
TaskInput.jsx Local + intent
import { useIntent } from '@everystate/solid';
import { store } from './store';
export function TaskInput() {
let input;
const addTask = useIntent(store, 'intent.addTask');
const submit = () => {
addTask(input.value);
input.value = '';
};
return (
<div class="card">
<div class="input-row">
<input ref={input} placeholder="What needs to be done?"
onKeyDown={(e) => e.key === 'Enter' && submit()} />
<button class="primary" onClick={submit}>Add</button>
</div>
</div>
);
}
Filters.jsx Read + publish
import { usePath, useIntent } from '@everystate/solid';
import { store } from './store';
export function Filters() {
const filter = usePath(store, 'state.filter');
const setFilter = useIntent(store, 'intent.changeFilter');
return (
<div class="card filters">
<button classList={{ active: filter() === 'all' }} onClick={() => setFilter('all')}>All</button>
<button classList={{ active: filter() === 'active' }} onClick={() => setFilter('active')}>Active</button>
<button classList={{ active: filter() === 'completed' }} onClick={() => setFilter('completed')}>Done</button>
<button classList={{ active: filter() === 'important' }} onClick={() => setFilter('important')}>Important</button>
</div>
);
}
TaskList.jsx Derived + intents
import { usePath, useIntent } from '@everystate/solid';
import { For } from 'solid-js';
import { store } from './store';
export function TaskList() {
const items = usePath(store, 'derived.tasks.filtered');
const toggle = useIntent(store, 'intent.toggleTask');
const del = useIntent(store, 'intent.deleteTask');
return (
<div class="card">
<ul>
<For each={items()}>{(task) =>
<li>
<input type="checkbox" checked={task.completed} onChange={() => toggle(task.id)} />
<span>{task.text}</span>
<div class="right">
{task.important && <span class="pill">Important</span>}
<button onClick={() => del(task.id)}>Delete</button>
</div>
</li>
}</For>
</ul>
</div>
);
}
App.jsx Composition
import { Header } from './Header';
import { Filters } from './Filters';
import { TaskInput } from './TaskInput';
import { TaskList } from './TaskList';
export function App() {
return (
<div class="app">
<Header />
<div class="container">
<Filters />
<TaskInput />
<TaskList />
</div>
</div>
);
}
The Adapter Source
This is the entire @everystate/solid package. ~30 lines.
@everystate/solid/eventStateSolid.js ~30 lines
import { createSignal, onCleanup } from 'solid-js';
export function usePath(store, path) {
const [value, setValue] = createSignal(store.get(path));
const unsub = store.subscribe(path, (val) => setValue(() => val));
onCleanup(unsub);
return value;
}
export function useIntent(store, path) {
return (value) => store.set(path, value);
}
export function useWildcard(store, wildcardPath) {
const parentPath = wildcardPath.endsWith('.*') ? wildcardPath.slice(0, -2) : wildcardPath;
const [value, setValue] = createSignal(store.get(parentPath));
const unsub = store.subscribe(wildcardPath, () => setValue(() => store.get(parentPath)));
onCleanup(unsub);
return value;
}
export function useAsync(store, path) {
return {
data: usePath(store, path + '.data'),
status: usePath(store, path + '.status'),
error: usePath(store, path + '.error'),
execute: (fetcher) => store.setAsync(path, fetcher),
cancel: () => store.cancel(path),
};
}
Testing Without Solid
store.test.js No createRoot
import { store } from './store';
test('adding a task increments taskCount', () => {
store.set('intent.addTask', 'Buy groceries');
expect(store.get('state.taskCount')).toBe(1);
expect(store.get('state.tasks')[0].text).toBe('Buy groceries');
});
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);
});