SolidJS
February 14, 2026 ยท View on GitHub
Integrate StateLoom with a SolidJS application. Solid's fine-grained reactivity aligns naturally with StateLoom's signal system.
Data Flow
graph LR
subgraph StateLoom
S["signal / store / atom"]
end
subgraph Solid
Hook["useSignal / useStore"]
Acc["Solid Accessor<T>"]
Comp["Component re-render"]
end
S -->|subscribe| Hook
Hook -->|Accessor| Acc
Acc -->|"value()"| Comp
Comp -->|set / mutate| S
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-app
::: code-group
pnpm add @stateloom/core @stateloom/solid
npm install @stateloom/core @stateloom/solid
yarn add @stateloom/core @stateloom/solid
:::
Add a paradigm adapter:
::: code-group
pnpm add @stateloom/store
pnpm add @stateloom/atom
pnpm add @stateloom/proxy
:::
Basic 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>
);
}
::: warning 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
::: tip Minimal Adapter The Solid adapter is ~0.2 KB because both systems share the same reactive paradigm. The bridge is thin by design. :::
::: warning 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: