Choosing Signals or Stores in SolidJS: Where to Keep State
Learn when to use signals versus stores in SolidJS and why plain locals or destructured props break reactivity.
21 Mar 2026, 08:42 UTC

The \"Once‑Only\" Execution Trap
In SolidJS a component function runs exactly once. It builds the reactive graph and the DOM nodes, then disappears. Only the expressions inside JSX or the callbacks in createEffect run again when a signal changes. If you keep state in a plain local variable—for example let count = 0;—and update it in a click handler, the variable changes but the UI does not, because the JSX that read count during the initial execution never re‑runs. To make a value \"live\" it must be wrapped in a reactive primitive.
Signals: The Atomic Unit of State
createSignal returns a getter and a setter. Calling the getter inside a tracking scope (JSX, createEffect, createMemo) subscribes that scope to the signal. The setter uses reference equality by default, so replacing an object triggers updates while setting an equal primitive does not.
Why Destructuring Props Breaks Reactivity
In Solid, props is a reactive proxy. Destructuring it at the top of a component reads each prop once during the initial execution, severing the reactive link. Accessing props through the proxy preserves the live connection.
// ❌ WRONG: reactivity lost
const UserProfile = ({ name }) => {\n return Hello, {name};\n};
// ✅ RIGHT: access via the proxy
const UserProfile = (props) => {\n return Hello, {props.name};\n};
If you need to rename or forward props, use splitProps or mergeProps; they keep the proxy intact.
Stores: Fine‑Grained Nested State
For objects or arrays, a signal forces you to replace the whole value to trigger an update, which notifies every observer of that value. createStore from solid-js/store wraps the value in a deep proxy, allowing path‑based updates that notify only the readers of the changed path.
Signal vs Store Comparison
| Aspect | createSignal | createStore |
|---|---|---|
| Data shape | Primitives or immutable objects | Mutable objects/arrays |
| Access | Getter function: count() |
Property access: state.count |
| Update granularity | Whole value replacement | Path‑based update: setStore('todos', i, 'done', true) |
| Typical use | Simple toggles, counters | Lists, forms, nested state |
Worked Example: Todo List with a Store
import { createStore } from \"solid-js/store\";
import { For } from \"solid-js\";
function TodoApp() {\n const [todos, setTodos] = createStore([\n { id: 1, text: \"Learn SolidJS\", done: false },\n { id: 2, text: \"Build a project\", done: false },\n ]);\n
const toggle = (id) => {\n setTodos(\n (todo) => todo.id === id,\n \"done\",\n (done) => !done\n );\n };
return (\n \n \n {(todo) => (\n - \n toggle(todo.id)}\n />\n {todo.text}\n
\n )}\n \n
\n);\n}
To verify that only the changed item updates, add a console.log inside the For callback. When you toggle a checkbox, the log appears for the toggled item only; the other items do not re‑run.
Trade‑offs and Limitations
- Stores introduce a Proxy layer; for a single primitive the overhead is unnecessary.
- Because store access uses property reads, DevTools are helpful to see which path triggered an update.
- If you need a stable reference across many consumers, wrap a derived value with
createMemo; otherwise a plain function is sufficient.
Actionable Summary
- Use
createSignalfor independent primitives or when you want explicit getter/setter control. - Use
createStorefor arrays of objects or deeply nested state to gain path‑based updates. - Never destructure
propsat the top of a component; read them asprops.xor usesplitProps/mergePropswhen you need to rename or forward. - Avoid plain locals for any value that must appear in the UI; wrap it in a reactive primitive.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.