While signals are perfect for simple state, SolidJS stores provide powerful tools for managing complex, nested application state. Stores offer fine-grained reactivity with a mutable-like API while maintaining immutability under the hood.
Introduction to Stores
Stores in SolidJS are reactive state containers designed for complex, nested data structures. Unlike signals which hold a single value, stores can hold objects and arrays with deep reactivity.
When to Use Stores
- Complex Nested State: Objects with multiple levels
- Collections: Arrays of objects that need individual updates
- Global State: Application-wide state management
- Forms: Complex form data with nested fields
Creating Stores
Use createStore to create a reactive store:
import { createStore } from 'solid-js/store';
interface User {
id: number;
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
const [store, setStore] = createStore<User>({
id: 1,
name: 'John Doe',
email: 'john@example.com',
preferences: {
theme: 'light',
notifications: true,
},
});
console.log(store.name); // John DoeBasic Store Operations
Updating Top-Level Properties
function UserProfile() {
const [user, setUser] = createStore<User>({
id: 1,
name: 'John Doe',
email: 'john@example.com',
preferences: {
theme: 'light',
notifications: true,
},
});
const updateName = () => {
setUser('name', 'Jane Doe');
};
return (
<div>
<h1>{user.name}</h1>
<button onClick={updateName}>Update Name</button>
</div>
);
}Updating Nested Properties
Use path syntax to update nested properties:
const updateTheme = () => {
setUser('preferences', 'theme', 'dark');
};
const toggleNotifications = () => {
setUser('preferences', 'notifications', (notifications) => !notifications);
};Updating Array Items
interface Todo {
id: number;
text: string;
completed: boolean;
}
function TodoApp() {
const [todos, setTodos] = createStore<Todo[]>([
{ id: 1, text: 'Learn SolidJS', completed: false },
{ id: 2, text: 'Build app', completed: false },
]);
const toggleTodo = (id: number) => {
setTodos(
(todo) => todo.id === id,
'completed',
(completed) => !completed
);
};
const updateTodoText = (id: number, newText: string) => {
setTodos(
(todo) => todo.id === id,
'text',
newText
);
};
return (
<ul>
<For each={todos}>
{(todo) => (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
</li>
)}
</For>
</ul>
);
}Advanced Store Operations
Batch Updates
Update multiple properties at once:
const updateMultiple = () => {
setStore({
name: 'Jane Doe',
email: 'jane@example.com',
});
};Using produce for Complex Updates
The produce utility from Immer allows you to write mutable-style code:
import { produce } from 'solid-js/store';
const addTodo = (text: string) => {
setTodos(
produce((todos) => {
todos.push({
id: Date.now(),
text,
completed: false,
});
})
);
};
const removeTodo = (id: number) => {
setTodos(
produce((todos) => {
const index = todos.findIndex((t) => t.id === id);
if (index !== -1) {
todos.splice(index, 1);
}
})
);
};Merging Updates
Merge multiple updates efficiently:
const updateUserPreferences = () => {
setUser('preferences', {
theme: 'dark',
notifications: false,
});
};Store with Arrays
Appending Items
const addTodo = (text: string) => {
setTodos((todos) => [
...todos,
{ id: Date.now(), text, completed: false }
]);
};Updating Multiple Items
const markAllComplete = () => {
setTodos(
(todo) => true, // Match all items
'completed',
true
);
};
const markRangeComplete = (start: number, end: number) => {
setTodos(
{ from: start, to: end }, // Match range
'completed',
true
);
};Filtering Arrays
const removeCompleted = () => {
setTodos((todos) => todos.filter((todo) => !todo.completed));
};Nested State Management
Deep Nested Updates
interface AppState {
user: {
profile: {
name: string;
email: string;
address: {
street: string;
city: string;
country: string;
};
};
settings: {
theme: 'light' | 'dark';
language: string;
};
};
}
function App() {
const [state, setState] = createStore<AppState>({
user: {
profile: {
name: 'John Doe',
email: 'john@example.com',
address: {
street: '123 Main St',
city: 'New York',
country: 'USA',
},
},
settings: {
theme: 'light',
language: 'en',
},
},
});
const updateCity = (newCity: string) => {
setState('user', 'profile', 'address', 'city', newCity);
};
const updateTheme = (theme: 'light' | 'dark') => {
setState('user', 'settings', 'theme', theme);
};
return <div>{/* render app */}</div>;
}Conditional Updates
const updateIfMatch = (id: number, newText: string) => {
setTodos(
(todo) => todo.id === id,
produce((todo) => {
if (todo.text.length < 100) {
todo.text = newText;
}
})
);
};Store Utilities
Using Arrays
import { produce } from 'solid-js/store';
function ShoppingList() {
const [items, setItems] = createStore<ShoppingItem[]>([]);
const addItem = (name: string, quantity: number) => {
setItems(
produce((items) => {
items.push({ id: Date.now(), name, quantity, purchased: false });
})
);
};
const updateQuantity = (id: number, quantity: number) => {
setItems(
(item) => item.id === id,
'quantity',
quantity
);
};
const togglePurchased = (id: number) => {
setItems(
(item) => item.id === id,
'purchased',
(purchased) => !purchased
);
};
const clearPurchased = () => {
setItems((items) => items.filter((item) => !item.purchased));
};
return (
<div>
<For each={items}>
{(item) => (
<div>
<input
type="checkbox"
checked={item.purchased}
onChange={() => togglePurchased(item.id)}
/>
<span>{item.name}</span>
<input
type="number"
value={item.quantity}
onInput={(e) => updateQuantity(item.id, parseInt(e.currentTarget.value))}
/>
</div>
)}
</For>
</div>
);
}Using Objects
function FormState() {
const [form, setForm] = createStore<FormData>({
firstName: '',
lastName: '',
email: '',
address: {
street: '',
city: '',
zipCode: '',
},
});
const setFieldValue = (field: string, value: string) => {
const path = field.split('.');
if (path.length === 1) {
setForm(path[0] as keyof FormData, value);
} else {
setForm(path[0] as keyof FormData, path[1], value);
}
};
const resetForm = () => {
setForm({
firstName: '',
lastName: '',
email: '',
address: {
street: '',
city: '',
zipCode: '',
},
});
};
return (
<form>
<input
value={form.firstName}
onInput={(e) => setFieldValue('firstName', e.currentTarget.value)}
/>
<input
value={form.lastName}
onInput={(e) => setFieldValue('lastName', e.currentTarget.value)}
/>
<input
value={form.email}
onInput={(e) => setFieldValue('email', e.currentTarget.value)}
/>
<input
value={form.address.street}
onInput={(e) => setFieldValue('address.street', e.currentTarget.value)}
/>
<button type="button" onClick={resetForm}>
Reset
</button>
</form>
);
}Store Modifiers
readOnly
Create a read-only view of a store:
import { createStore, produce } from 'solid-js/store';
import { readOnly } from 'solid-js/store';
function ReadOnlyExample() {
const [store, setStore] = createStore({ count: 0 });
const readOnlyStore = readOnly(store);
// This will throw an error
// readOnlyStore.count = 1;
return <div>{readOnlyStore.count}</div>;
}modify
Create a mutable version of a store:
import { modify } from 'solid-js/store';
const [store, setStore] = createStore({ items: [] });
const mutableStore = modify(store);
// Direct mutations are possible
mutableStore.items.push({ id: 1, name: 'Item' });Store Patterns
Global Store
Create a global store for app-wide state:
// store.ts
import { createStore } from 'solid-js/store';
interface AppState {
user: User | null;
isAuthenticated: boolean;
theme: 'light' | 'dark';
}
const [appState, setAppState] = createStore<AppState>({
user: null,
isAuthenticated: false,
theme: 'light',
});
export { appState, setAppState };Context Store
Share store through context:
import { createContext, useContext } from 'solid-js';
import { createStore } from 'solid-js/store';
const StoreContext = createContext<ReturnType<typeof createStore>>();
function StoreProvider(props: { children: any }) {
const [store, setStore] = createStore({ /* initial state */ });
return (
<StoreContext.Provider value={[store, setStore]}>
{props.children}
</StoreContext.Provider>
);
}
function useStore() {
const context = useContext(StoreContext);
if (!context) throw new Error('StoreContext not found');
return context;
}Custom Store Hook
Create reusable store logic:
function createTodoStore() {
const [todos, setTodos] = createStore<Todo[]>([]);
const [filter, setFilter] = createSignal<Filter>('all');
const addTodo = (text: string) => {
setTodos((todos) => [
...todos,
{ id: Date.now(), text, completed: false }
]);
};
const toggleTodo = (id: number) => {
setTodos(
(todo) => todo.id === id,
'completed',
(completed) => !completed
);
};
const removeTodo = (id: number) => {
setTodos((todos) => todos.filter((todo) => todo.id !== id));
};
const filteredTodos = createMemo(() => {
const currentFilter = filter();
return todos().filter((todo) => {
if (currentFilter === 'active') return !todo.completed;
if (currentFilter === 'completed') return todo.completed;
return true;
});
});
const stats = createMemo(() => {
const todosList = todos();
return {
total: todosList.length,
active: todosList.filter((t) => !t.completed).length,
completed: todosList.filter((t) => t.completed).length,
};
});
return {
get todos() { return todos(); },
get filteredTodos() { return filteredTodos(); },
get stats() { return stats(); },
get filter() { return filter(); },
setFilter,
addTodo,
toggleTodo,
removeTodo,
};
}Store Persistence
LocalStorage Store
function createPersistedStore<T>(key: string, initialValue: T) {
const storedValue = localStorage.getItem(key);
const initial = storedValue ? JSON.parse(storedValue) : initialValue;
const [store, setStore] = createStore<T>(initial);
// Persist to localStorage on changes
createEffect(() => {
localStorage.setItem(key, JSON.stringify(store));
});
return [store, setStore] as const;
}
// Usage
const [preferences, setPreferences] = createPersistedStore('preferences', {
theme: 'light',
language: 'en',
});SessionStorage Store
function createSessionStore<T>(key: string, initialValue: T) {
const storedValue = sessionStorage.getItem(key);
const initial = storedValue ? JSON.parse(storedValue) : initialValue;
const [store, setStore] = createStore<T>(initial);
createEffect(() => {
sessionStorage.setItem(key, JSON.stringify(store));
});
return [store, setStore] as const;
}Performance Optimization
Shallow Comparison
const shallowEqual = (a: any, b: any) => {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every((key) => a[key] === b[key]);
};Selective Updates
const updateSingleField = (id: number, field: string, value: any) => {
setItems(
(item) => item.id === id,
field,
value
);
};Testing Stores
import { describe, it, expect } from 'vitest';
import { createStore } from 'solid-js/store';
describe('Todo Store', () => {
it('should add todo', () => {
const [todos, setTodos] = createStore<Todo[]>([]);
setTodos((todos) => [
...todos,
{ id: 1, text: 'Test', completed: false }
]);
expect(todos.length).toBe(1);
});
it('should toggle todo', () => {
const [todos, setTodos] = createStore<Todo[]>([
{ id: 1, text: 'Test', completed: false }
]);
setTodos(
(todo) => todo.id === 1,
'completed',
(completed) => !completed
);
expect(todos[0].completed).toBe(true);
});
});Best Practices
- 1Use Stores for Complex State: Objects with nested properties
- 2Keep Stores Focused: Single responsibility
- 3Use TypeScript: Type safety for stores
- 4Immutable Updates: Use setters and produce
- 5Optimize Updates: Fine-grained updates when possible
- 6Test Store Logic: Unit test store operations
Conclusion
SolidJS stores provide a powerful and efficient way to manage complex application state. With fine-grained reactivity, immutable updates, and a mutable-like API, stores make it easy to work with nested data structures while maintaining performance.
By mastering stores, you can build applications with complex state management needs without sacrificing performance or developer experience. The combination of signals and stores gives you the flexibility to handle any state management scenario in SolidJS.