Skip to content

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:

bash
npx degit solidjs/templates/ts my-app
cd my-app
bash
pnpm add @stateloom/core @stateloom/solid
bash
npm install @stateloom/core @stateloom/solid
bash
yarn add @stateloom/core @stateloom/solid

Add a paradigm adapter:

bash
pnpm add @stateloom/store
bash
pnpm add @stateloom/atom
bash
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:

typescript
// 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);
}
tsx
// 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:

tsx
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:

typescript
// 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))}`);
tsx
// 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:

typescript
// src/stores/proxy-state.ts
import { observable } from '@stateloom/proxy';

export const settings = observable({
  theme: 'light' as 'light' | 'dark',
  fontSize: 16,
  notifications: true,
});
tsx
// 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:

tsx
const items = useStore(
  store,
  (s) => s.items,
  (a, b) => a.length === b.length,
);

Patterns

Counter with Actions

tsx
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

typescript
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:

tsx
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:

tsx
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:

SolidStateLoomBridge
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

Live Demo

Try the SolidJS example directly in your browser:

Open in StackBlitz | Open Static Demo