SolidJS
Integrate StateLoom with a SolidJS application. Solid's fine-grained reactivity aligns naturally with StateLoom's signal system.
Data Flow
Prerequisites
- Solid.js 1.0+
- Node.js 18+
- Vite 5+ (recommended)
Project Setup
Scaffold a new Solid + Vite project and install StateLoom:
npx degit solidjs/templates/ts my-app
cd my-apppnpm add @stateloom/core @stateloom/solidnpm install @stateloom/core @stateloom/solidyarn add @stateloom/core @stateloom/solidAdd a paradigm adapter:
pnpm add @stateloom/storepnpm add @stateloom/atompnpm add @stateloom/proxyBasic Integration
Signals with useSignal
Bridge StateLoom signals to Solid accessors. useSignal returns an Accessor<T> -- call it as a function to read the value:
// src/stores/counter.ts
import { signal, computed } from '@stateloom/core';
export const count = signal(0);
export const doubled = computed(() => count.get() * 2);
export function increment() {
count.update((n) => n + 1);
}
export function decrement() {
count.update((n) => n - 1);
}
export function reset() {
count.set(0);
}// src/components/Counter.tsx
import { useSignal } from '@stateloom/solid';
import { count, doubled, increment, decrement, reset } from '../stores/counter';
export function Counter() {
const value = useSignal(count);
const double = useSignal(doubled);
return (
<section>
<h2>Counter</h2>
<p>
Count: {value()} | Doubled: {double()}
</p>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
<button onClick={increment}>+</button>
</section>
);
}Accessor Pattern
useSignal and useStore return Solid accessors. Always call them as functions: value(), not value. Forgetting the parentheses is a common mistake.
Stores with useStore
Subscribe to stores with optional selectors:
import { createStore } from '@stateloom/store';
import { useStore } from '@stateloom/solid';
const counterStore = createStore((set) => ({
count: 0,
name: 'Alice',
increment: () => set((s) => ({ count: s.count + 1 })),
}));
// Full state
function FullState() {
const state = useStore(counterStore);
return <span>{state().count}</span>;
}
// Selected slice: only updates when count changes
function CountOnly() {
const count = useStore(counterStore, (s) => s.count);
return <span>{count()}</span>;
}Atoms
Use atoms for composable, bottom-up state:
// src/stores/user-atoms.ts
import { atom, derived } from '@stateloom/atom';
export const nameAtom = atom('Alice');
export const ageAtom = atom(30);
export const summaryAtom = derived((get) => `${get(nameAtom)}, age ${String(get(ageAtom))}`);// src/components/UserProfile.tsx
import { useSignal } from '@stateloom/solid';
import { nameAtom, ageAtom, summaryAtom } from '../stores/user-atoms';
export function UserProfile() {
const name = useSignal(nameAtom);
const age = useSignal(ageAtom);
const summary = useSignal(summaryAtom);
return (
<section>
<h2>User Profile</h2>
<input value={name()} onInput={(e) => nameAtom.set(e.currentTarget.value)} />
<input
type="number"
value={age()}
onInput={(e) => ageAtom.set(Number(e.currentTarget.value))}
/>
<p>
Summary: <strong>{summary()}</strong>
</p>
</section>
);
}Proxy
For mutable-style state:
// src/stores/proxy-state.ts
import { observable } from '@stateloom/proxy';
export const settings = observable({
theme: 'light' as 'light' | 'dark',
fontSize: 16,
notifications: true,
});// src/components/ProxyState.tsx
import { useSignal } from '@stateloom/solid';
import { computed } from '@stateloom/core';
import { snapshot } from '@stateloom/proxy';
import { settings } from '../stores/proxy-state';
export function ProxyState() {
const snap = useSignal(computed(() => snapshot(settings)));
return (
<section>
<h2>Settings (Proxy)</h2>
<button
onClick={() => {
settings.theme = settings.theme === 'light' ? 'dark' : 'light';
}}
>
Toggle Theme
</button>
<pre>{JSON.stringify(snap(), null, 2)}</pre>
</section>
);
}Custom Equality
Pass a custom equality function to control when updates propagate:
const items = useStore(
store,
(s) => s.items,
(a, b) => a.length === b.length,
);Patterns
Counter with Actions
import { createStore } from '@stateloom/store';
import { useStore } from '@stateloom/solid';
const counterStore = createStore((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
decrement: () => set((s) => ({ count: s.count - 1 })),
reset: () => set({ count: 0 }),
}));
function Counter() {
const count = useStore(counterStore, (s) => s.count);
const actions = counterStore.getState();
return (
<div>
<button onClick={actions.decrement}>-</button>
<span>{count()}</span>
<button onClick={actions.increment}>+</button>
<button onClick={actions.reset}>Reset</button>
</div>
);
}Middleware Integration
import { createStore } from '@stateloom/store';
import { devtools } from '@stateloom/devtools';
import { persist } from '@stateloom/persist';
const themeStore = createStore(
(set) => ({
theme: 'light' as 'light' | 'dark',
toggle: () =>
set((s) => ({
theme: s.theme === 'light' ? 'dark' : 'light',
})),
}),
{
middleware: [
devtools({ name: 'Theme', enabled: import.meta.env.DEV }),
persist({ key: 'theme-prefs' }),
],
},
);SSR with SolidStart
Scope Isolation
Use ScopeProvider to isolate state per request:
import { createScope, runInScope } from '@stateloom/core';
import { ScopeProvider, useScope } from '@stateloom/solid';
export default function Page() {
const scope = createScope();
runInScope(scope, () => {
// Initialize server-side state
});
return (
<ScopeProvider scope={scope}>
<Content />
</ScopeProvider>
);
}
function Content() {
const scope = useScope();
return <div>Content with scope isolation</div>;
}Nested Scopes
ScopeProvider components can be nested. Inner providers override outer ones:
const parentScope = createScope();
const childScope = createScope();
<ScopeProvider scope={parentScope}>
<ParentContent />
<ScopeProvider scope={childScope}>
<ChildContent />
</ScopeProvider>
</ScopeProvider>;Fine-Grained Reactivity
Solid's reactivity model aligns naturally with StateLoom. Both systems use fine-grained signals:
| Solid | StateLoom | Bridge |
|---|---|---|
createSignal() | signal() | useSignal() |
createMemo() | computed() | useSignal() |
createEffect() | effect() | Direct use |
createStore() | createStore() | useStore() |
Use StateLoom for state shared across components or modules. Use Solid's built-in primitives for local component state.
Tips
Minimal Adapter
The Solid adapter is ~0.2 KB because both systems share the same reactive paradigm. The bridge is thin by design.
Solid Reactive Context
useSignal and useStore must be called within a Solid reactive context (component, createRoot, createEffect). They use onCleanup for automatic subscription cleanup.
Example App
See the complete Solid + Vite example for a working app that demonstrates all paradigms.
Next Steps
- Solid API Reference -- Full adapter documentation
- Store API Reference -- Store patterns and middleware
- Getting Started -- Core concepts and paradigm comparison
Live Demo
Try the SolidJS example directly in your browser: