Using SolidJS createStore for Fine‑Grained Mutable State
Learn how SolidJS’s createStore gives a mutable‑feeling API with fine‑grained reactivity, reducing unnecessary re‑renders in components.
13 Sept 2025, 05:19 UTC

The problem: mutable state that over‑renders
When you keep component state in a plain JavaScript object and mutate it directly, SolidJS cannot tell which properties changed. The framework may re‑run effects for the whole object, causing unnecessary DOM work and making debugging harder.
Thesis: createStore gives a mutable‑feeling API while preserving SolidJS’s fine‑grained reactivity
SolidJS’s createStore wraps an object in a Proxy‑like getter/setter pair. Reads track dependencies, and writes produce a new immutable version internally, but unchanged parts keep their reference. This lets you write code that feels like normal mutation while still getting precise subscriptions.
How createStore works under the hood
- The returned tuple
[store, setStore]gives you a readablestoreproxy and a settersetStore(also exposed asstore.set). - Reading a property (e.g.,
store.todos[0].completed) registers that property as a dependency of the current effect. - Calling
setStorewith a path and a new value creates a shallow copy of the object tree, replacing only the mutated branch. Unchanged branches retain their original references, so only subscribers that depend on the changed path re‑run. - Direct property assignment on the proxy (e.g.,
store.todos = newArray) is ignored by the Proxy and does not trigger updates; you must use the setter.
Worked example: a todo list with toggled completion
import { createStore, createEffect } from 'solid-js'
function TodoList() {
const [todos, setTodos] = createStore({
todos: [
{ id: 1, text: 'Learn Solid', completed: false },
{ id: 2, text: 'Try createStore', completed: false }
]
})
// Effect that logs whenever a todo's completed flag changes
createEffect(() => {
todos.todos.forEach(t => {
// accessing t.completed creates a dependency
console.log(`Todo ${t.id} completed=${t.completed}`)
})
})
function toggle(id) {
const idx = todos.todos.findIndex(t => t.id === id)
// Use the setter with a path expression
setTodos('todos.' + idx + '.completed', !todos.todos[idx].completed)
}
return (
{todos.todos.map(t => (
{t.text}
toggle(t.id)}>
{t.completed ? 'Undo' : 'Do'}
))}
)
}
In this example, clicking the button updates only the completed property of the corresponding todo. The effect logs only for the changed todo because the unchanged todos retain their original references.
Trade‑offs and limitations
- Runtime overhead: The Proxy adds a small cost (typically <1 KB gzipped) compared with a plain object. For most applications this is negligible, but it should be considered in ultra‑size‑critical bundles.
- API adjustment: Existing code that mutates state directly must be rewritten to use
setStoreor the.mutatehelper. Direct assignments likestore.todos = newArraywill silently fail to update the UI. - Environment support: createStore relies on ES Proxies, which are unavailable in IE11. A polyfill can add support but increases bundle size.
Actionable closing
- Wrap any object‑based state with
createStoreat the top of your component or store module. - Replace every direct mutation (
obj.prop = value,arr.push, etc.) with the appropriatesetStorecall or usestore.mutatefor batch updates. - Verify the granularity by adding a temporary
createEffectthat logs a counter; only the relevant subscribers should increment on each interaction. - Check your bundle size with a tool like
webpack-bundle-analyzerto confirm the Proxy overhead stays within your budget.
By adopting createStore, you keep the ergonomics of mutable state while gaining SolidJS’s precision‑reactivity guarantees.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.