@everystate/solid
v1.0.1
Published
EveryState Solid adapter: usePath, useIntent, useWildcard, useAsync - bridges the reactive store to Solid's fine-grained signals
Downloads
166
Maintainers
Readme
@everystate/solid v1.0.1
Solid adapter for EveryState. Four functions that bridge the reactive store to Solid's fine-grained signal-based reactivity. ~30 lines, zero dependencies beyond Solid and the core store.
npm install @everystate/solid @everystate/corePeer dependencies: solid-js >=1.6.0 and @everystate/core >=1.0.5.
Why a Solid Adapter?
Solid compiles to vanilla JS, so you might ask: does it even need an adapter?
Yes - but barely. Solid's compiled output still relies on Solid's own reactivity primitives (createSignal, createEffect, createMemo). External state must be wrapped in Solid signals for the fine-grained update system to track it. Without the wrapper, Solid components are blind to store changes.
The adapter is thin (~30 lines), but those 30 lines are the difference between "it works" and "figure it out yourself."
What it eliminates
- Manual signal wiring - No need to write
createSignal+createEffect+onCleanupfor every path. - Props drilling - Components read from the store directly. No forwarding through component trees.
- Boilerplate context setup - The store is just an object. Pass it however you like.
- Lifecycle mistakes -
onCleanupis handled automatically. No leaked subscriptions.
The API
usePath(store, path)
Subscribe to a dot-path. Returns a read-only Solid accessor that updates when the store value changes. Only computations reading this accessor re-run - fine-grained by default.
import { usePath } from '@everystate/solid';
function Header(props) {
const count = usePath(props.store, 'state.taskCount');
return <span>{count()} tasks</span>;
}useIntent(store, path)
Returns a stable function that publishes a value to a path.
import { useIntent } from '@everystate/solid';
function TaskInput(props) {
let input;
const addTask = useIntent(props.store, 'intent.addTask');
return (
<div>
<input ref={input} />
<button onClick={() => {
addTask(input.value);
input.value = '';
}}>Add</button>
</div>
);
}useWildcard(store, path)
Subscribe to a wildcard path. Returns a read-only accessor that updates when any child changes.
const user = useWildcard(store, 'state.user.*');
// Re-runs when state.user.name, state.user.email, etc. changeuseAsync(store, path)
Async data fetching with automatic status tracking. Auto-aborts previous in-flight requests.
const { data, status, error, execute, cancel } = useAsync(store, 'users');
execute((signal) => fetch('/api/users', { signal }).then(r => r.json()));Context Pattern (Optional)
Solid has createContext/useContext if you prefer DI over prop-passing:
import { createContext, useContext } from 'solid-js';
import { createEveryState } from '@everystate/core';
const StoreContext = createContext();
function App() {
const store = createEveryState({
state: { tasks: [], taskCount: 0, filter: 'all' },
});
// Business logic in subscribers
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, done: false }];
store.set('state.tasks', next);
store.set('state.taskCount', next.length);
});
return (
<StoreContext.Provider value={store}>
<Header />
<TaskInput />
</StoreContext.Provider>
);
}
function useStore() {
return useContext(StoreContext);
}The adapter doesn't force a DI pattern - unlike React/Vue, where the provider is essentially required. In Solid, passing the store as a prop is perfectly idiomatic.
Solid-Specific Advantages
Fine-grained is the default
Solid doesn't have a virtual DOM. When usePath returns an accessor that changes, only the exact DOM node reading it updates. No diffing, no reconciliation. This is the most efficient bridge possible - EveryState fires per-path, Solid updates per-signal.
No component re-renders
In React, a state change re-runs the entire component function. In Solid, the component function runs once. Only the signal accessors re-fire. This means usePath never causes "unnecessary re-renders" - the concept doesn't exist.
Batch compatibility
Solid has its own batch(). EveryState's batch() coalesces writes at the store level. They compose naturally:
import { batch } from 'solid-js';
batch(() => {
store.batch(() => {
store.set('form.name', 'Alice');
store.set('form.email', '[email protected]');
});
});Testing without Solid
test('adding a task increments taskCount', () => {
const store = createEveryState({ state: { tasks: [], taskCount: 0 } });
store.subscribe('intent.addTask', (text) => {
const tasks = store.get('state.tasks') || [];
const next = [...tasks, { text, done: false }];
store.set('state.tasks', next);
store.set('state.taskCount', next.length);
});
store.set('intent.addTask', 'test task');
expect(store.get('state.taskCount')).toBe(1);
});No createRoot. No renderToString. Just state in, state out.
Documentation
Full documentation available at everystate.dev.
Cross-Framework Story
The same store code runs unchanged across Solid, React, Vue, and Angular:
| Framework | Bridge | Read | Write |
|-----------|--------|------|-------|
| Solid | createSignal() + subscribe() | usePath(store, path) | useIntent(store, path) |
| Angular | signal() + subscribe() | usePath(store, path) | useIntent(store, path) |
| React | useSyncExternalStore() | usePath(path) | useIntent(path) |
| Vue | ref() + subscribe() | usePath(path) | useIntent(path) |
The store code, intent handlers, and derived state are identical. Only the 3-line bridge function changes per framework.
Ecosystem
| Package | Description | License |
|---|---|---|
| @everystate/aliases | Ergonomic single-character and short-name DOM aliases for vanilla JS | MIT |
| @everystate/angular | Angular adapter: usePath, useIntent, useWildcard, useAsync — bridges store to Angular signals | MIT |
| @everystate/core | Path-based state management with wildcard subscriptions and async support | MIT |
| @everystate/css | Reactive CSSOM engine: design tokens, typed validation, WCAG enforcement, all via path-based state | MIT |
| @everystate/examples | Example applications and patterns | MIT |
| @everystate/perf | Performance monitoring overlay | MIT |
| @everystate/react | React hooks adapter: usePath, useIntent, useAsync hooks and EventStateProvider | MIT |
| @everystate/renderer | Direct-binding reactive renderer: bind-*, set, each attributes. Zero build step | MIT |
| @everystate/router | SPA routing as state | MIT |
| @everystate/solid | Solid adapter: usePath, useIntent, useWildcard, useAsync — bridges store to Solid signals | MIT |
| @everystate/test | Event-sequence testing for EveryState stores. Zero dependency. | MIT |
| @everystate/types | Typed dot-path autocomplete for EveryState stores | MIT |
| @everystate/view | State-driven view: DOMless resolve + surgical DOM projector. View tree as first-class state | MIT |
| @everystate/vue | Vue 3 composables adapter: provideStore, usePath, useIntent, useWildcard, useAsync | MIT |
License
MIT (c) Ajdin Imsirovic
