Skip to main content
Advertisement

18.5 Stores and State Sharing

For state within a single component, createSignal is sufficient. But for deeply nested object structures or complex state shared across multiple components, Stores are a far more powerful tool. In this chapter, we'll systematically cover the internals of createStore, sharing global state via the Context API, and third-party state management libraries.


1. createStore — Nested Reactive State

Core Concept

While createSignal tracks reactivity for a single value, createStore wraps entire nested objects and arrays in a Proxy so that reading any property at any depth automatically sets up reactive tracking.

import { createStore } from 'solid-js/store';

const [state, setState] = createStore({
user: {
name: 'John Doe',
age: 30,
address: {
city: 'New York',
district: 'Manhattan',
},
},
todos: [],
settings: {
theme: 'dark',
language: 'en',
},
});

// Reading — nested properties are automatically tracked
console.log(state.user.name); // 'John Doe'
console.log(state.user.address.city); // 'New York'

Unlike createSignal, state is not a function — it is the object itself. You read properties directly using dot (.) notation.

Basic Updates

import { createStore } from 'solid-js/store';

function UserProfile() {
const [user, setUser] = createStore({
name: 'John Doe',
age: 30,
email: 'john@example.com',
});

return (
<div>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
<p>Email: {user.email}</p>

<button onClick={() => setUser('name', 'Jane Smith')}>
Change Name
</button>
<button onClick={() => setUser('age', a => a + 1)}>
Increment Age
</button>
</div>
);
}

setUser('name', 'Jane Smith') is a path-based update. Every argument up to the second-to-last forms the path, and the last argument is the new value or updater function.


2. Path-Based Updates — setStore In Depth

Nested Path Updates

import { createStore } from 'solid-js/store';

function AddressEditor() {
const [state, setState] = createStore({
user: {
name: 'John Doe',
address: {
city: 'New York',
district: 'Manhattan',
zipCode: '10001',
},
},
});

const updateCity = (newCity) => {
// Path: 'user' → 'address' → 'city'
setState('user', 'address', 'city', newCity);
};

const updateAddress = (newAddress) => {
// Merge part of a nested object (no spread needed!)
setState('user', 'address', newAddress);
};

return (
<div>
<p>City: {state.user.address.city}</p>
<p>District: {state.user.address.district}</p>
<button onClick={() => updateCity('Los Angeles')}>
Change to Los Angeles
</button>
<button onClick={() => updateAddress({ city: 'Chicago', district: 'Lincoln Park' })}>
Update Part of Address
</button>
</div>
);
}

Array Updates

import { createStore } from 'solid-js/store';

function TodoList() {
const [store, setStore] = createStore({
todos: [
{ id: 1, text: 'Grocery shopping', done: false },
{ id: 2, text: 'Exercise', done: false },
{ id: 3, text: 'Reading', done: true },
],
});

// Update a specific item by index
const toggleTodo = (index) => {
setStore('todos', index, 'done', d => !d);
};

// Replace the entire array
const clearDone = () => {
setStore('todos', todos => todos.filter(t => !t.done));
};

// Add a new item
const addTodo = (text) => {
setStore('todos', todos => [
...todos,
{ id: Date.now(), text, done: false },
]);
};

// Update multiple items at once using a filter function
const completeAll = () => {
setStore('todos', t => !t.done, 'done', true);
// When the first argument is a function, it acts as a filter on each item
};

return (
<div>
<ul>
<For each={store.todos}>
{(todo, i) => (
<li
style={{ 'text-decoration': todo.done ? 'line-through' : 'none' }}
onClick={() => toggleTodo(i())}
>
{todo.text}
</li>
)}
</For>
</ul>
<button onClick={clearDone}>Remove Completed</button>
<button onClick={completeAll}>Complete All</button>
<button onClick={() => addTodo('New task')}>Add</button>
</div>
);
}

Range-Based Updates (Array Slice)

// Update multiple items by index range
setStore('items', { from: 2, to: 5 }, 'selected', true);

// Update only odd-indexed items
setStore('items', (_, i) => i % 2 === 1, 'highlighted', true);

3. produce — Immer-Style Mutations

produce is a helper inspired by Immer that lets you write immutable updates in a mutation style. It greatly simplifies code for complex nested updates.

import { createStore, produce } from 'solid-js/store';

function CartManager() {
const [cart, setCart] = createStore({
items: [
{ id: 1, name: 'Laptop', price: 1200, qty: 1 },
{ id: 2, name: 'Mouse', price: 35, qty: 2 },
],
coupon: null,
shippingFree: false,
});

// Without produce — verbose and hard to read
const incrementQtyOld = (id) => {
setCart('items', item => item.id === id, 'qty', q => q + 1);
};

// With produce — intuitive mutation style
const incrementQty = (id) => {
setCart(produce(draft => {
const item = draft.items.find(i => i.id === id);
if (item) item.qty += 1;
}));
};

const removeItem = (id) => {
setCart(produce(draft => {
const index = draft.items.findIndex(i => i.id === id);
if (index !== -1) draft.items.splice(index, 1);
}));
};

const applyCoupon = (code) => {
setCart(produce(draft => {
draft.coupon = { code, discount: 0.1 };
if (draft.items.reduce((sum, i) => sum + i.price * i.qty, 0) >= 50) {
draft.shippingFree = true;
}
}));
};

const totalPrice = () =>
cart.items.reduce((sum, item) => sum + item.price * item.qty, 0);

return (
<div>
<For each={cart.items}>
{(item) => (
<div>
<span>{item.name} x {item.qty}</span>
<span>${(item.price * item.qty).toLocaleString()}</span>
<button onClick={() => incrementQty(item.id)}>+</button>
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
)}
</For>
<p>Total: ${totalPrice().toLocaleString()}</p>
<button onClick={() => applyCoupon('SAVE10')}>Apply Coupon</button>
</div>
);
}

Using produce's Return Value

// If you mutate the draft without returning, the mutations are applied
setStore(produce(draft => {
draft.status = 'active';
}));

// To replace the entire store with a new value, return it
setStore(produce(() => ({
items: [],
status: 'reset',
})));

4. reconcile — Syncing with External Data

reconcile merges new data fetched from a server into the store while preserving existing references as much as possible, preventing unnecessary re-renders.

import { createStore, reconcile } from 'solid-js/store';

function UserList() {
const [store, setStore] = createStore({ users: [] });

const fetchUsers = async () => {
const response = await fetch('/api/users');
const newUsers = await response.json();

// Without reconcile — replaces the entire array, re-renders every row
// setStore('users', newUsers);

// With reconcile — diffs by id, re-renders only changed items
setStore('users', reconcile(newUsers, { key: 'id', merge: true }));
};

onMount(fetchUsers);

return (
<For each={store.users}>
{(user) => <UserRow user={user} />}
</For>
);
}

reconcile options:

  • key: The field used for identity comparison (default: 'id')
  • merge: If true, merges new properties into existing objects; if false, fully replaces them

5. Context API — Sharing Global State

createContext and useContext

Context is a mechanism for sharing state through the component tree without manually passing props.

import { createContext, useContext, createSignal } from 'solid-js';

// 1. Create Context — provide a default value
const CounterContext = createContext(0);

// 2. Provider component
function CounterProvider(props) {
const [count, setCount] = createSignal(0);

return (
<CounterContext.Provider value={[count, setCount]}>
{props.children}
</CounterContext.Provider>
);
}

// 3. Consumer components
function CounterDisplay() {
const [count] = useContext(CounterContext);
return <p>Count: {count()}</p>;
}

function CounterButton() {
const [, setCount] = useContext(CounterContext);
return <button onClick={() => setCount(c => c + 1)}>Increment</button>;
}

// 4. Usage in the app
function App() {
return (
<CounterProvider>
<CounterDisplay />
<CounterButton />
</CounterProvider>
);
}

Type-Safe Context Pattern

import { createContext, useContext, createSignal } from 'solid-js';

// Define Context type
const ThemeContext = createContext({
theme: () => 'light',
toggleTheme: () => {},
});

// Provider — real implementation
function ThemeProvider(props) {
const [theme, setTheme] = createSignal('light');
const toggleTheme = () =>
setTheme(t => (t === 'light' ? 'dark' : 'light'));

const value = { theme, toggleTheme };

return (
<ThemeContext.Provider value={value}>
{props.children}
</ThemeContext.Provider>
);
}

// Convenience custom hook
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used inside ThemeProvider');
return ctx;
}

// Consumer
function ThemeToggleButton() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Current theme: {theme()}
</button>
);
}

Combining createStore with Context

This is the most common pattern in large-scale applications.

import { createContext, useContext } from 'solid-js';
import { createStore, produce } from 'solid-js/store';

// --- Default state ---
const defaultCart = {
items: [],
isOpen: false,
};

const CartContext = createContext();

// --- Provider ---
export function CartProvider(props) {
const [cart, setCart] = createStore(defaultCart);

const actions = {
addItem(product) {
setCart(produce(draft => {
const existing = draft.items.find(i => i.id === product.id);
if (existing) {
existing.qty += 1;
} else {
draft.items.push({ ...product, qty: 1 });
}
}));
},
removeItem(id) {
setCart('items', items => items.filter(i => i.id !== id));
},
updateQty(id, qty) {
if (qty <= 0) {
actions.removeItem(id);
return;
}
setCart('items', item => item.id === id, 'qty', qty);
},
clearCart() {
setCart('items', []);
},
toggleCart() {
setCart('isOpen', v => !v);
},
};

const derived = {
totalItems: () => cart.items.reduce((sum, i) => sum + i.qty, 0),
totalPrice: () => cart.items.reduce((sum, i) => sum + i.price * i.qty, 0),
};

return (
<CartContext.Provider value={{ cart, actions, derived }}>
{props.children}
</CartContext.Provider>
);
}

// --- Custom hook ---
export function useCart() {
const ctx = useContext(CartContext);
if (!ctx) throw new Error('useCart must be used inside CartProvider');
return ctx;
}

6. Global State Management Patterns

Module-Scoped Signals — Simplest Global State

// store/auth.js
import { createSignal } from 'solid-js';

// Created at module level — shared across the entire app
const [user, setUser] = createSignal(null);
const [isLoading, setIsLoading] = createSignal(false);

export const authStore = {
user,
isLoading,
async login(credentials) {
setIsLoading(true);
try {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
});
const userData = await res.json();
setUser(userData);
} finally {
setIsLoading(false);
}
},
logout() {
setUser(null);
},
isAuthenticated: () => user() !== null,
};
// Import and use in any component
import { authStore } from './store/auth';

function Navbar() {
return (
<nav>
<Show when={authStore.user()} fallback={<LoginButton />}>
<span>Hello, {authStore.user().name}</span>
<button onClick={authStore.logout}>Logout</button>
</Show>
</nav>
);
}

Context vs Global Signals — When to Use Which

CriteriaGlobal Signal (module scope)Context API
Setup complexityVery simpleRequires Provider setup
Test isolationLow (shared state)High (can swap Provider)
SSR safetyRisky (state leaks between requests)Safe
Best forCSR-only apps, quick prototypesSSR apps, apps with testing needs
Multiple instancesNot possiblePossible (nested Providers)

In SSR environments, always use Context. Module-scoped signals share state between server requests, creating a security vulnerability.

Domain-Separated Context Strategy

// Separate Contexts by domain for better separation of concerns
function App() {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
<NotificationProvider>
<Router>
<AppRoutes />
</Router>
</NotificationProvider>
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
}

When Contexts pile up, composing them into a single AppProvider is also effective.

// providers/index.jsx
function AppProvider(props) {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
{props.children}
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
}

7. Third-Party State Management

@tanstack/solid-query

For server state (data fetched from a server), @tanstack/solid-query is the most powerful option.

npm install @tanstack/solid-query
import {
QueryClient,
QueryClientProvider,
createQuery,
createMutation,
useQueryClient,
} from '@tanstack/solid-query';

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // fresh for 5 minutes
gcTime: 1000 * 60 * 10, // GC after 10 minutes
},
},
});

// Provider setup
function App() {
return (
<QueryClientProvider client={queryClient}>
<ProductList />
</QueryClientProvider>
);
}

// Fetching data
function ProductList() {
const productsQuery = createQuery(() => ({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json()),
}));

return (
<Switch>
<Match when={productsQuery.isPending}>
<p>Loading...</p>
</Match>
<Match when={productsQuery.isError}>
<p>Error: {productsQuery.error.message}</p>
</Match>
<Match when={productsQuery.isSuccess}>
<For each={productsQuery.data}>
{(product) => <ProductCard product={product} />}
</For>
</Match>
</Switch>
);
}

// Mutating data
function AddProduct() {
const qc = useQueryClient();

const addMutation = createMutation(() => ({
mutationFn: (newProduct) =>
fetch('/api/products', {
method: 'POST',
body: JSON.stringify(newProduct),
}).then(r => r.json()),
onSuccess: () => {
// Invalidate the products query → triggers automatic refetch
qc.invalidateQueries({ queryKey: ['products'] });
},
}));

return (
<button
onClick={() => addMutation.mutate({ name: 'New Product', price: 10000 })}
disabled={addMutation.isPending}
>
{addMutation.isPending ? 'Adding...' : 'Add Product'}
</button>
);
}

solid-zustand

If you prefer the concise Zustand-style state management:

npm install solid-zustand zustand
import { create } from 'solid-zustand';

const useBearStore = create(set => ({
bears: 0,
fish: [],
addBear: () => set(state => ({ bears: state.bears + 1 })),
addFish: (fish) => set(state => ({ fish: [...state.fish, fish] })),
reset: () => set({ bears: 0, fish: [] }),
}));

function BearCounter() {
const bears = useBearStore(state => state.bears);
const addBear = useBearStore(state => state.addBear);

return (
<div>
<h1>{bears} bears</h1>
<button onClick={addBear}>Add Bear</button>
</div>
);
}

8. Practical Examples

Example 1: Complete Shopping Cart Implementation

import { createContext, useContext, createSignal, Show, For } from 'solid-js';
import { createStore, produce } from 'solid-js/store';

// ============ Initial State ============
const initialCartState = {
items: [],
isOpen: false,
promoCode: '',
discount: 0,
};

// ============ Create Context ============
const CartContext = createContext();

// ============ Provider ============
function CartProvider(props) {
const [cart, setCart] = createStore(initialCartState);

const totalItems = () => cart.items.reduce((sum, i) => sum + i.qty, 0);
const subtotal = () => cart.items.reduce((sum, i) => sum + i.price * i.qty, 0);
const discountAmount = () => subtotal() * cart.discount;
const total = () => subtotal() - discountAmount();

const addItem = (product) => {
setCart(produce(draft => {
const found = draft.items.find(i => i.id === product.id);
if (found) {
found.qty += 1;
} else {
draft.items.push({ ...product, qty: 1 });
}
}));
};

const removeItem = (id) =>
setCart('items', items => items.filter(i => i.id !== id));

const setQty = (id, qty) => {
if (qty < 1) { removeItem(id); return; }
setCart('items', item => item.id === id, 'qty', qty);
};

const applyPromo = (code) => {
const promos = { SAVE10: 0.1, SAVE20: 0.2 };
const discount = promos[code.toUpperCase()] ?? 0;
setCart({ promoCode: code, discount });
};

const checkout = async () => {
const orderData = {
items: cart.items,
total: total(),
promoCode: cart.promoCode,
};
const res = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(orderData),
});
if (res.ok) {
setCart({ items: [], promoCode: '', discount: 0, isOpen: false });
alert('Order placed!');
}
};

return (
<CartContext.Provider value={{
cart, totalItems, subtotal, discountAmount, total,
addItem, removeItem, setQty, applyPromo, checkout,
openCart: () => setCart('isOpen', true),
closeCart: () => setCart('isOpen', false),
}}>
{props.children}
</CartContext.Provider>
);
}

function useCart() {
return useContext(CartContext);
}

// ============ Cart UI ============
function CartDrawer() {
const { cart, total, discountAmount, removeItem, setQty, applyPromo, checkout, closeCart } = useCart();
const [promoInput, setPromoInput] = createSignal('');

return (
<Show when={cart.isOpen}>
<div class="cart-overlay" onClick={closeCart} />
<div class="cart-drawer">
<h2>Shopping Cart</h2>
<For each={cart.items} fallback={<p>Your cart is empty</p>}>
{(item) => (
<div class="cart-item">
<img src={item.image} alt={item.name} />
<div>
<p>{item.name}</p>
<p>${item.price.toLocaleString()}</p>
</div>
<div class="qty-control">
<button onClick={() => setQty(item.id, item.qty - 1)}>-</button>
<span>{item.qty}</span>
<button onClick={() => setQty(item.id, item.qty + 1)}>+</button>
</div>
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
)}
</For>

<div class="promo-section">
<input
value={promoInput()}
onInput={e => setPromoInput(e.target.value)}
placeholder="Promo code"
/>
<button onClick={() => applyPromo(promoInput())}>Apply</button>
<Show when={cart.discount > 0}>
<p>Discount: -${discountAmount().toLocaleString()}</p>
</Show>
</div>

<p class="total">Total: ${total().toLocaleString()}</p>
<button class="checkout-btn" onClick={checkout}>Checkout</button>
</div>
</Show>
);
}

// ============ Product Card ============
function ProductCard(props) {
const { addItem } = useCart();
return (
<div class="product-card">
<img src={props.product.image} alt={props.product.name} />
<h3>{props.product.name}</h3>
<p>${props.product.price.toLocaleString()}</p>
<button onClick={() => addItem(props.product)}>Add to Cart</button>
</div>
);
}

// ============ App ============
function ShopApp() {
return (
<CartProvider>
<CartNavbar />
<ProductGrid />
<CartDrawer />
</CartProvider>
);
}

Example 2: Theme Toggle — Dark/Light Mode

import { createContext, useContext, createSignal, onMount } from 'solid-js';

const ThemeContext = createContext();

function ThemeProvider(props) {
const [theme, setTheme] = createSignal(
localStorage.getItem('theme') || 'light'
);

// Apply theme class to DOM
const applyTheme = (t) => {
document.documentElement.classList.toggle('dark', t === 'dark');
localStorage.setItem('theme', t);
};

onMount(() => applyTheme(theme()));

const toggleTheme = () => {
const next = theme() === 'light' ? 'dark' : 'light';
setTheme(next);
applyTheme(next);
};

// Detect system dark mode preference
onMount(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
if (!localStorage.getItem('theme')) {
const sys = mq.matches ? 'dark' : 'light';
setTheme(sys);
applyTheme(sys);
}
const handler = (e) => {
if (!localStorage.getItem('theme')) {
const t = e.matches ? 'dark' : 'light';
setTheme(t);
applyTheme(t);
}
};
mq.addEventListener('change', handler);
onCleanup(() => mq.removeEventListener('change', handler));
});

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{props.children}
</ThemeContext.Provider>
);
}

function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme called outside ThemeProvider');
return ctx;
}

function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
aria-label="Toggle theme"
class="theme-toggle"
>
{theme() === 'light' ? '🌙 Dark Mode' : '☀️ Light Mode'}
</button>
);
}

Example 3: User Authentication State Management

import { createContext, useContext, createSignal, onMount } from 'solid-js';
import { createStore } from 'solid-js/store';

const AuthContext = createContext();

function AuthProvider(props) {
const [auth, setAuth] = createStore({
user: null,
token: null,
loading: true,
error: null,
});

// Restore token on app start
onMount(async () => {
const token = localStorage.getItem('token');
if (!token) {
setAuth('loading', false);
return;
}
try {
const res = await fetch('/api/me', {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const user = await res.json();
setAuth({ user, token, loading: false });
} else {
localStorage.removeItem('token');
setAuth('loading', false);
}
} catch {
setAuth({ loading: false, error: 'Authentication check failed' });
}
});

const login = async (email, password) => {
setAuth({ error: null });
try {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
const { message } = await res.json();
setAuth({ error: message });
return false;
}
const { user, token } = await res.json();
localStorage.setItem('token', token);
setAuth({ user, token, error: null });
return true;
} catch (e) {
setAuth({ error: 'An error occurred during login' });
return false;
}
};

const logout = () => {
localStorage.removeItem('token');
setAuth({ user: null, token: null });
};

const updateProfile = async (data) => {
const res = await fetch('/api/me', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${auth.token}`,
},
body: JSON.stringify(data),
});
if (res.ok) {
const updated = await res.json();
setAuth('user', updated);
}
};

return (
<AuthContext.Provider value={{
auth,
isAuthenticated: () => auth.user !== null,
isAdmin: () => auth.user?.role === 'admin',
login,
logout,
updateProfile,
}}>
{props.children}
</AuthContext.Provider>
);
}

function useAuth() {
return useContext(AuthContext);
}

// Protected route component
function ProtectedRoute(props) {
const { auth, isAuthenticated } = useAuth();

return (
<Show when={!auth.loading} fallback={<LoadingSpinner />}>
<Show when={isAuthenticated()} fallback={<Navigate href="/login" />}>
{props.children}
</Show>
</Show>
);
}

// Login form
function LoginForm() {
const { login, auth } = useAuth();
const [email, setEmail] = createSignal('');
const [password, setPassword] = createSignal('');

const handleSubmit = async (e) => {
e.preventDefault();
const success = await login(email(), password());
if (success) window.location.href = '/dashboard';
};

return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email()}
onInput={e => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password()}
onInput={e => setPassword(e.target.value)}
placeholder="Password"
required
/>
<Show when={auth.error}>
<p class="error">{auth.error}</p>
</Show>
<button type="submit">Login</button>
</form>
);
}

9. Pro Tips

Tip 1: Store Selectors for Fine-Grained Subscriptions

// Instead of subscribing to the whole store, extract derived signals for just what you need
const [state, setState] = createStore({ user: { name: 'John Doe', age: 30 } });

// Memo that tracks only the name
const userName = createMemo(() => state.user.name);

// Usage — only re-renders when name changes
function NameDisplay() {
return <h1>{userName()}</h1>;
}

Tip 2: Using Context Default Values for Testing

// Putting mock data in the default value allows testing without a Provider
const CartContext = createContext({
cart: { items: [] },
totalItems: () => 0,
addItem: () => {},
});

Tip 3: When You Need an Immutable Snapshot

import { unwrap } from 'solid-js/store';

// Strip the Proxy to get a plain JavaScript object
const plainObject = unwrap(state);
console.log(JSON.stringify(plainObject)); // JSON serialization works

Tip 4: Lazy Initialization of createStore

// Defer expensive initial data computation with a function
const [state, setState] = createStore(() => ({
items: computeExpensiveInitialData(),
}));

Tip 5: Granular Updates — The Power of Path Targeting

// Bad — replaces the entire user object
setState('user', { ...state.user, name: 'Jane Smith' });

// Good — updates only name; components tracking other properties don't re-render
setState('user', 'name', 'Jane Smith');

Tip 6: Know All Imports from solid-js/store

import {
createStore, // Create a reactive store
produce, // Immer-style mutations
reconcile, // Merge external data
unwrap, // Remove Proxy, extract plain object
createMutable, // Directly mutable store (advanced)
} from 'solid-js/store';

createMutable is similar to Vue's reactive. Use it for special cases such as integrating with external libraries.

import { createMutable } from 'solid-js/store';

const state = createMutable({ count: 0, name: 'test' });

// Direct mutation is possible (no setter function needed)
state.count++;
state.name = 'changed';

Summary

ToolPurpose
createStoreReactive state for nested objects/arrays
setStore (path-based)Granular updates to nested properties
produceSimplify complex updates with Immer-style mutations
reconcileSync store with server data, minimize re-renders
unwrapRemove store Proxy when serialization is needed
createContext + useContextShare state across the component tree
Module-scoped signalsSimple global state for CSR-only apps
@tanstack/solid-queryServer state, caching, and synchronization

Up Next...

18.6 SolidStart Basics covers SolidStart, the full-stack meta-framework for Solid.js. You'll learn file-based routing, server functions, SSR/SSG configuration, and deployment adapters to build complete full-stack applications.

Advertisement