React + Vite
Integrate StateLoom with a React application using Vite. Covers hooks for signals, stores, atoms, and proxies.
Data Flow
Prerequisites
- Node.js 18+
- React 18+ (for
useSyncExternalStore) - Vite 5+
Project Setup
Scaffold a new React + Vite project and install StateLoom:
pnpm create vite my-app --template react-ts
cd my-appInstall the core and React adapter:
pnpm add @stateloom/core @stateloom/reactnpm install @stateloom/core @stateloom/reactyarn add @stateloom/core @stateloom/reactAdd a paradigm adapter based on your preference:
pnpm add @stateloom/storepnpm add @stateloom/atompnpm add @stateloom/proxyBasic Integration
Signals with useSignal
Bridge core signals to React components. This is the simplest integration -- no paradigm adapter needed:
// src/state/counter.ts
import { signal, computed } from '@stateloom/core';
export const count = signal(0);
export const doubled = computed(() => count.get() * 2);// src/components/Counter.tsx
import { useSignal } from '@stateloom/react';
import { batch } from '@stateloom/core';
import { count, doubled } from '../state/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={() => count.set(value - 1)}>-1</button>
<button onClick={() => count.set(value + 1)}>+1</button>
<button
onClick={() => {
batch(() => {
for (let i = 0; i < 5; i++) count.set(count.get() + 1);
});
}}
>
+5 (batched)
</button>
<button onClick={() => count.set(0)}>Reset</button>
</section>
);
}useSignal accepts any Subscribable<T> (signals, computed, atoms) and returns the current value. The component re-renders only when the value changes.
Stores with useStore
For feature-scoped state with actions and middleware support:
// src/state/todos.ts
import { createStore } from '@stateloom/store';
import { logger } from '@stateloom/devtools';
import { persist, memoryStorage } from '@stateloom/persist';
type Todo = {
readonly id: number;
readonly text: string;
readonly done: boolean;
};
type TodoState = {
readonly items: readonly Todo[];
readonly nextId: number;
readonly add: (text: string) => void;
readonly toggle: (id: number) => void;
readonly remove: (id: number) => void;
};
export const todoStore = createStore<TodoState>(
(set) => ({
items: [],
nextId: 1,
add: (text: string) =>
set((s) => ({
items: [...s.items, { id: s.nextId, text, done: false }],
nextId: s.nextId + 1,
})),
toggle: (id: number) =>
set((s) => ({
items: s.items.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
})),
remove: (id: number) =>
set((s) => ({
items: s.items.filter((t) => t.id !== id),
})),
}),
{
middleware: [
logger<TodoState>({ name: 'TodoStore', diff: true }),
persist<TodoState>({
key: 'stateloom-todos',
storage: memoryStorage(),
partialize: (s) => ({ items: s.items, nextId: s.nextId }),
}),
],
},
);// src/components/TodoList.tsx
import { useState } from 'react';
import { useStore } from '@stateloom/react/store';
import { todoStore } from '../state/todos';
export function TodoList() {
const { items, add, toggle, remove } = useStore(todoStore);
const [text, setText] = useState('');
const handleAdd = () => {
const trimmed = text.trim();
if (trimmed === '') return;
add(trimmed);
setText('');
};
return (
<section>
<h2>Todo List</h2>
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleAdd();
}}
placeholder="Add a todo..."
/>
<button onClick={handleAdd}>Add</button>
<ul>
{items.map((item) => (
<li key={item.id}>
<span onClick={() => toggle(item.id)}>{item.text}</span>
<button onClick={() => remove(item.id)}>x</button>
</li>
))}
</ul>
</section>
);
}Use Selectors
Pass a selector to useStore to prevent re-renders when unrelated state changes:
// Only re-renders when count changes, not when name changes
const count = useStore(counterStore, (s) => s.count);Atoms with useAtom
For bottom-up, composable state:
// src/state/profile.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 { useAtom } from '@stateloom/react/atom';
import { useSignal } from '@stateloom/react';
import { nameAtom, ageAtom, summaryAtom } from '../state/profile';
export function UserProfile() {
const [name, setName] = useAtom(nameAtom);
const [age, setAge] = useAtom(ageAtom);
const summary = useSignal(summaryAtom);
return (
<section>
<h2>User Profile</h2>
<label>
Name: <input value={name} onChange={(e) => setName(e.target.value)} />
</label>
<label>
Age:{' '}
<input
type="number"
value={age}
onChange={(e) => {
const n = parseInt(e.target.value, 10);
if (!Number.isNaN(n)) setAge(n);
}}
/>
</label>
<p>
Summary: <strong>{summary}</strong>
</p>
</section>
);
}Proxy with useSnapshot
For mutable-style state management:
// src/state/settings.ts
import { observable } from '@stateloom/proxy';
export const settings = observable({
theme: 'light' as 'light' | 'dark',
fontSize: 16,
notifications: true,
});// src/components/ProxyState.tsx
import { useSnapshot } from '@stateloom/react/proxy';
import { settings } from '../state/settings';
export function ProxyState() {
const snap = useSnapshot(settings);
return (
<section>
<h2>Settings</h2>
<button
onClick={() => {
settings.theme = settings.theme === 'light' ? 'dark' : 'light';
}}
>
Toggle Theme
</button>
<button
onClick={() => {
settings.fontSize += 2;
}}
>
Font +
</button>
<button
onClick={() => {
settings.fontSize -= 2;
}}
>
Font -
</button>
<button
onClick={() => {
settings.notifications = !settings.notifications;
}}
>
Toggle Notifications
</button>
<pre>{JSON.stringify(snap, null, 2)}</pre>
</section>
);
}Patterns
Middleware Stack
Add devtools, persistence, and logging to any store:
import { createStore } from '@stateloom/store';
import { devtools } from '@stateloom/devtools';
import { persist } from '@stateloom/persist';
const store = createStore(
(set) => ({
theme: 'light' as 'light' | 'dark',
setTheme: (t: 'light' | 'dark') => set({ theme: t }),
}),
{
middleware: [
devtools({ name: 'Theme', enabled: import.meta.env.DEV }),
persist({ key: 'theme-prefs' }),
],
},
);Multiple Stores
Define stores in separate files and compose them in components:
import { useStore } from '@stateloom/react/store';
import { authStore } from './stores/auth';
import { settingsStore } from './stores/settings';
function Header() {
const userName = useStore(authStore, (s) => s.user?.name);
const theme = useStore(settingsStore, (s) => s.theme);
return (
<header className={theme}>
<span>Hello, {userName ?? 'Guest'}</span>
</header>
);
}Migrating from Zustand
If you are migrating from Zustand, the store API is intentionally similar:
// Zustand
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}));
// StateLoom
import { createStore } from '@stateloom/store';
const counterStore = createStore((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}));
// Use with: useStore(counterStore, (s) => s.count)Key differences:
createStorereturns a store object, not a hook. Pass it touseStorefrom@stateloom/react/store.- Middleware is passed as an array in options, not as function wrapping.
Tips
Define Stores Outside Components
Create stores at module level, not inside components. This prevents re-creation on every render and enables sharing across components.
Don't Mix React State and StateLoom
Use useState/useReducer for local UI state (form inputs, open/closed toggles). Use StateLoom for state shared across components.
Example App
See the complete React + Vite example for a working app that demonstrates all four paradigms together.
Next Steps
- Next.js Guide -- Add SSR support
- React API Reference -- Full hooks documentation
- Store API Reference -- Middleware and patterns
- DevTools API Reference -- Debugging tools
Live Demo
Try the React + Vite example directly in your browser: