Fine-Grained Reactivity in SolidJS
I profiled the same todo-list component in React and SolidJS on a list with 10,000 items, toggling one checkbox. React re-rendered the list component, diffed 10,000 virtual nodes, and committed one changed <input>. SolidJS updated that single checkbox attribute. No diff. No re-render. The component function never ran again after initial mount.
That difference is fine-grained reactivity — SolidJS's core design bet. Instead of re-executing components and diffing trees, Solid wires each DOM node directly to the signals it reads. When a signal changes, the runtime updates exactly those nodes. The model feels like React on the surface (JSX, components, props) but executes like a spreadsheet engine under the hood.
Signals: the atomic unit of state
Everything reactive in SolidJS starts with a signal — a getter/setter pair that tracks dependencies:
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Clicked {count()} times
</button>
);
}
Notice count() — signals are functions. Calling count() inside JSX registers a subscription: when count changes, only the text node "Clicked N times" updates. The <button> element itself is created once. The onClick handler closes over setCount but doesn't re-bind on each update.
This is the mental model shift from React: your component body is a setup function, not a render function. It runs once. Reactive updates happen at the DOM binding level, not by re-invoking the component.
Memos and derived state
Derived values use createMemo, which caches its computation and only recalculates when dependencies change:
import { createSignal, createMemo, For } from "solid-js";
function FilteredList() {
const [items, setItems] = createSignal(["apple", "banana", "cherry"]);
const [filter, setFilter] = createSignal("");
const filtered = createMemo(() =>
items().filter(i => i.includes(filter()))
);
return (
<>
<input value={filter()} onInput={e => setFilter(e.target.value)} />
<ul>
<For each={filtered()}>
{(item) => <li>{item}</li>}
</For>
</ul>
</>
);
}
When filter changes, filtered recalculates. The <For> component diffs the list by reference identity — only added or removed items touch the DOM. Typing in the filter box doesn't re-create the entire <ul>; Solid's list reconciliation handles insertions and removals surgically.
Effects for side work
createEffect runs a function whenever its signal dependencies change — useful for logging, fetching, or syncing to external systems:
createEffect(() => {
console.log("Count is now:", count());
document.title = `Count: ${count()}`;
});
Effects run after the DOM update for the current change, similar to React's useEffect but with automatic dependency tracking. You don't write dependency arrays — Solid tracks which signals were read inside the effect body.
Why no virtual DOM is a feature
Virtual DOM frameworks pay a tax on every update: serialize the component output to a tree, diff against the previous tree, compute patches, apply patches. For small apps the tax is invisible. For dashboards with hundreds of live-updating cells, or lists with thousands of rows, the tax dominates.
SolidJS compiles JSX at build time into _el$ template functions that create DOM nodes and register reactive bindings:
// What you write:
<div>{name()}</div>
// What Solid compiles (simplified):
const _el$ = template(`<div></div>`);
// On creation: bind text node to name signal
// On name change: textNode.data = newValue
The compiler knows exactly which DOM node each expression binds to. Runtime work is O(changed bindings), not O(tree size).
Stores for nested state
Flat signals work for simple state. Nested objects use createStore, which applies fine-grained reactivity to property access:
import { createStore } from "solid-js/store";
const [state, setState] = createStore({ user: { name: "Ada", age: 36 } });
// Only components reading state.user.name update:
setState("user", "name", "Grace");
// Deep partial update:
setState("user", { age: 37 });
Store property reads create granular subscriptions. A component that only reads state.user.name won't re-run when state.user.age changes — even though they're the same object tree.
Practical patterns and pitfalls
Don't destructure signals. const { count } = ... breaks reactivity because you capture the value once. Always call signal getters in reactive contexts.
Use <Show> and <For> for control flow. Solid provides reactive control-flow components that handle conditional rendering and list diffing without destroying and recreating DOM subtrees unnecessarily.
Batch updates. Multiple setSignal calls in the same synchronous block batch into one DOM flush, avoiding intermediate repaints.
Server-side rendering. Solid supports SSR with streaming hydration. The server renders HTML; the client hydrates by attaching reactive bindings to existing DOM nodes rather than replacing them.
Common production mistakes
Teams get solidjs fine grained reactivity wrong in predictable ways:
- Skipping failure-mode rehearsal — run a game day or fault injection exercise before peak traffic, not after the first outage.
- Missing correlation context — every error path should carry request, trace, or tenant identifiers so incidents are debuggable.
- Optimizing for demo, not steady state — load tests, cache warm-up, and cold-start paths matter more than local dev latency.
- Undocumented trade-offs — if you chose speed over strict correctness (or vice versa), write that down for the next engineer.
Production implementations of solidjs fine grained reactivity fail when staging mirrors production topology poorly, rollback is untested, and on-call runbooks describe the happy path only.
Debugging and triage workflow
When solidjs fine grained reactivity misbehaves in production, work top-down instead of guessing:
- Confirm scope — one tenant, region, or deployment stage? Narrow blast radius before deep diving.
- Check recent changes — deploys, flag flips, config pushes, and schema migrations in the last 24 hours.
- Compare golden signals — latency, error rate, saturation, and traffic for the affected surface vs. baseline.
- Reproduce minimally — smallest input or scenario that triggers the failure; capture traces/logs with correlation IDs.
- Fix forward or rollback — if rollback is faster than root-cause during incident, rollback first, postmortem second.
- Add a guard — alert, integration test, or circuit breaker so the same class of failure is caught earlier next time.
Document the timeline during triage. Future you (and on-call) will need timestamps, not just conclusions.
Resources
- SolidJS documentation — reactivity guide
- SolidJS signals API reference
- Fine-grained reactivity — Ryan Carniato (Solid creator)
- SolidJS vs React benchmarks
- SolidJS Playground for live experimentation
Frequently asked questions
Does SolidJS use a virtual DOM?
No. SolidJS compiles JSX into real DOM operations wired directly to reactive subscriptions. When a signal changes, only the specific text node or attribute bound to that signal updates — no tree diffing, no reconciliation pass. This is why SolidJS benchmarks often beat React and Vue on update-heavy workloads: the work scales with what changed, not with component tree size.
How is SolidJS reactivity different from React hooks?
React re-runs component functions on state change and diffs the resulting virtual DOM. SolidJS runs each component function once at creation time; the JSX inside sets up reactive subscriptions. State reads inside those subscriptions re-execute only the affected DOM update, not the whole component. Think of Solid components as setup functions that wire DOM nodes to signals, not render functions that produce trees.
Can I use SolidJS with existing React knowledge?
The JSX and component model feel familiar, but the mental model is different. You don't put reactive reads inside a render function that re-executes — you use signals and memos, and the compiler handles binding. Hooks like useState map to createSignal; useMemo maps to createMemo. The biggest shift: variables from signals are accessed by calling them as functions — count(), not count.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →