SolidJS State Management: Choosing Between createSignal and createStore
Deciding between createSignal and createStore in SolidJS depends on data complexity. Learn when to use atomic signals versus proxy-based stores for granular reactivity.
02 Mar 2026, 07:51 UTC

The State Selection Problem
In SolidJS, the primary challenge isn't finding a state management library, but deciding which built-in primitive to use for specific data shapes. Choosing createSignal for a complex nested object leads to verbose "spread-operator hell," while using createStore for a simple toggle introduces unnecessary proxy overhead.
The key takeaway: Use Signals for atomic values and independent state; use Stores for nested data structures where you need to update a specific property without replacing the entire object.
Comparison of State Primitives
| Feature | createSignal | createStore |
|---|---|---|
| Data Shape | Primitives, simple arrays | Nested objects, collections |
| Update Mechanism | Setter function (replacement) | Proxy-based (path-based mutation) |
| Reactivity Grain | Atomic (whole value) | Granular (property-level) |
| Overhead | Minimal | Moderate (Proxy initialization) |
Trade-offs and Engineering Constraints
The Signal Bottleneck
Signals are the foundation of Solid's reactivity. However, because they treat the state as a single unit, updating one property in a large object requires you to clone the rest of the object to trigger a change. This is inefficient for deep trees and results in code that is difficult to maintain.
The Store Proxy
createStore uses JavaScript Proxies to track access. This allows Solid to know exactly which property in a nested object is being read. When you update store.user.settings.theme, only the components specifically reading that theme property will re-run. The rest of the user object remains untouched by the reactivity system.
Global vs. Local Scope
Unlike React, Solid's reactivity is independent of the component lifecycle. To create global state, you define your signal or store in a separate module (e.g., state.ts) and export the getter and setter. This removes the need for complex Context providers for simple global synchronization.
Implementation: Nested Updates
Consider a scenario where you have a user profile with nested preferences. Here is how the implementation differs between the two primitives.
Using createSignal (The Manual Way)
// Run in component or module
const [user, setUser] = createSignal({
name: "Alice",
prefs: { theme: "dark", lang: "en" }
});
// To update the theme, you must spread every level
const updateTheme = (newTheme) => {
setUser(prev => ({
...prev,
prefs: { ...prev.prefs, theme: newTheme }
}));
};
Using createStore (The Granular Way)
// Run in component or module
const [user, setUser] = createStore({
name: "Alice",
prefs: { theme: "dark", lang: "en" }
});
// Path-based update: only the 'theme' property triggers updates
const updateTheme = (newTheme) => {
setUser("prefs", "theme", newTheme);
};
Validation and Risks
Verifying Granularity
To verify that createStore is providing granular updates, place a console.log inside a component that reads user.name and another that reads user.prefs.theme. When calling updateTheme(), only the log for the theme should trigger. If you used a Signal, both logs would trigger because the entire user object was replaced.
Critical Limitations
- Direct Mutation: Never mutate a store directly (e.g.,
user.prefs.theme = "light"). This bypasses the Proxy and will not trigger UI updates. Always use thesetStorehelper. - Proxy Overhead: Avoid using Stores for simple booleans or counters. The memory overhead of a Proxy is negligible for a few objects, but significant when managing thousands of small, independent state pieces.
- Traceability: Global signals are convenient, but in large-scale apps, they can make it hard to track which component triggered a state change. Document your state mutations in a centralized service file.
Rollback Strategy
If you realize a createSignal has grown too complex, migrate to createStore by replacing the setter logic. Since both provide a way to read the current value, the UI templates typically remain unchanged, provided you maintain the same object structure.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.