· frameworks · 7 min read
Harnessing the Power of Reactivity in Solid.js
Deep dive into Solid.js reactivity: custom signals, store update strategies, selectors, batching, and practical patterns that reduce overhead and maximize performance.

Why Solid’s reactivity matters
Solid.js gives you fine-grained reactivity: instead of diffing entire component trees, Solid tracks exactly which atoms (signals, memos, store fields) a computation reads and only re-runs those computations when those atoms change. That makes Solid extremely performant - but you only get that performance if you structure state and updates correctly.
This post explores advanced techniques to get the most from Solid’s reactivity system: building custom signals with comparators, efficient datastore updates with createStore
+ reconcile
, selectors for large collections, batching and untracking, and patterns to avoid common pitfalls.
Resources cited throughout: the official Solid docs and API reference are extremely helpful: https://www.solidjs.com/docs/latest/api
Quick recap of primitives (very brief)
createSignal(value)
- basic reactive primitive, returns[get, set]
.createMemo(fn)
- derived value, only recomputes when dependencies used insidefn
change.createEffect(fn)
- side-effect when dependencies change.createStore(obj)
- reactive, proxied object for nested state updates.batch(fn)
- group multiple updates into a single notification pass.untrack(fn)
- read signals without creating dependencies.
(See the Solid API docs for full reference: https://www.solidjs.com/docs/latest/api)
1) Custom signals: equality, sanitization, and instrumentation
By default, a signal will notify dependents whenever its setter is called with a new value. For objects or arrays, this can produce noisy updates when the new value is logically equal or only superficially different. A small wrapper around createSignal
gives you control.
Example: a signal with an equality comparator and optional sanitizer.
import { createSignal, batch, untrack } from 'solid-js';
function createComparableSignal(
initial,
{ equals = Object.is, sanitize } = {}
) {
const [rawGet, rawSet] = createSignal(initial);
const get = () => rawGet();
const set = v => {
const next = sanitize ? sanitize(v) : v;
const prev = untrack(rawGet);
if (equals(prev, next)) return next; // no-op if considered equal
return rawSet(next);
};
// allow batched updates by exposing a small helper
const batchSet = fn => batch(() => fn(set));
return [get, set, { batchSet }];
}
// Usage
const [user, setUser] = createComparableSignal(
{ name: 'A' },
{ equals: (a, b) => a?.id === b?.id }
);
Why this matters:
- Prevents needless recomputation if you often set the same value.
sanitize
can coerce or normalize incoming values (remove circular fields, strip reactive wrappers, or convert empty strings to null).untrack
used above ensures we read the previous value without creating a dependency on it inside the setter.
Notes: If you need deep-equality, prefer careful selection - deep equality on large objects can be costly. Often a small, domain-specific comparator (compare id
or version) is better.
2) Stores: localized updates and reconcile
createStore
is Solid’s recommended way to manage nested state while keeping fine-grained reactive updates.
Key APIs to know:
setStore(path, value)
accepts a patch path (likesetStore('todos', idx, 'done', v => !v)
) and updates only the fields touched.reconcile(next, options?)
helps apply a new object into a store while minimizing change notifications and preserving stable references where possible.
Example using reconcile
to merge fetched data into a store without tearing apart consumers that depend on unchanged parts:
import { createStore, reconcile } from 'solid-js/store';
const [state, setState] = createStore({ users: [], ui: { loading: false } });
// Patch the users array with new data from server but reconcile to keep stable items
async function refreshUsers() {
const newUsers = await fetch('/api/users').then(r => r.json());
setState('users', reconcile(newUsers));
}
Why reconcile
is useful:
- When you receive new data (e.g., from an API), you often want to update a store while preserving as many existing references as possible. That avoids re-running every memo/effect that depends on fields that didn’t actually change.
Advanced pattern - targeted updates:
If only a single nested field changes, update it directly to avoid reconstructing containers:
// Toggle a todo done flag without replacing the whole todo
setState('todos', idx, 'done', done => !done);
If you need to replace a large sub-tree, use reconcile
with a key
option or custom comparator to match and reuse items inside arrays.
Docs: https://www.solidjs.com/docs/latest/api#reconcile
3) createSelector: efficient membership checks for big sets
When you have large lists or many checks like isSelected(id)
, a naive implementation that depends on an array can cause wide invalidations. createSelector
allows creating a stable, fast selector function.
import { createSignal, createSelector } from 'solid-js';
const [selectedIds, setSelectedIds] = createSignal(new Set());
const isSelected = createSelector(selectedIds);
// Use in JSX: <div class={isSelected(id) ? 'selected' : ''}>...</div>
// Toggle
function toggle(id) {
setSelectedIds(prev => {
const s = new Set(prev);
s.has(id) ? s.delete(id) : s.add(id);
return s;
});
}
createSelector
gives you O(1) membership checks and isolates components that only care about one id from changes to other ids.
Docs: https://www.solidjs.com/docs/latest/api#createselector
4) Batching and untracking to reduce notification noise
- Use
batch(() => { setA(x); setB(y); })
to group many updates so dependents run only once. - Use
untrack()
to read signals without creating a dependency - helpful in setters or in cases where you want to derive a value once.
Example: update a store and derived signals together during a single flow.
import { batch } from 'solid-js';
function doComplexUpdate() {
batch(() => {
setUser(u => ({ ...u, count: u.count + 1 }));
setUi(s => ({ ...s, lastUpdated: Date.now() }));
});
}
This prevents multiple recomputations of memos/effects that read both user
and ui
.
5) Splitting props and isolating components
A common anti-pattern: pass a big props
object deep into children and then access many fields. Use splitProps
and mergeProps
to isolate reactive parts so children only subscribe to what they actually use.
import { splitProps } from 'solid-js';
function MyComponent(props) {
const [local, rest] = splitProps(props, ['important', 'onClick']);
// `local.important` is tracked independently from the other fields in props
}
This reduces the surface area of dependencies a child creates, meaning fewer re-runs when unrelated props change.
Docs: https://www.solidjs.com/docs/latest/api#splitprops
6) Lists, keys, and minimizing DOM churn
When rendering arrays, use keyed <For>
(the fallback
and key
pattern) and try to avoid rebuilding arrays in-place.
- Keep items stable with an
id
and prefer updating item fields via the store (e.g.,setState('items', idx, 'done', ...)
) instead of replacing the whole array. - When you must replace an array with new data, use
reconcile(newArray, { key: 'id' })
to preserve item identity and DOM nodes where possible.
Example:
<For each={state.items}>{item => <Item key={item.id} item={item} />}</For>
Preserving identity drastically reduces DOM operations and re-renders.
7) Debugging and profiling reactivity
- The Solid DevTools (browser extension) helps inspect signals and owners and visualize dependencies.
- Add lightweight logs in computed/effect functions to see when they run.
- Use micro-benchmarks in dev: create a reproducible case and measure with
console.time
orperformance.now()
when toggling many signals.
Simple benchmark scaffolding:
const N = 10000;
console.time('updateMany');
batch(() => {
for (let i = 0; i < N; i++) setSomeSignal(i);
});
console.timeEnd('updateMany');
When you see large timings, consider whether updates can be batched, whether the dependency graph is too broad, or whether you should switch to store with targeted updates.
8) Practical anti-patterns and how to avoid them
- Anti-pattern: storing large mutable objects inside a single signal and calling
setValue({...value, x: newX})
frequently. This invalidates everything that read any property. Instead, usecreateStore
and update only specific fields. - Anti-pattern: deriving state inside JSX with expensive computations. Use
createMemo
with clear inputs to memoize. - Anti-pattern: passing entire
props
object into deeply nested components. UsesplitProps
or destructure props early.
9) Example: Putting it all together (todo app patterns)
- Use
createStore
for the collection of todos:const [state, setState] = createStore({ todos: [] })
. - Use
setState('todos', idx, 'done', v => !v)
to flip a flag. - Use
createSelector
for quick membership (selected ids) - Use
reconcile
to merge server updates while preserving references:setState('todos', reconcile(newTodos, { key: 'id' }))
. - Wrap user-editable values in comparable signals to avoid propagating identical values.
This combination keeps the UI responsive, minimizes recomputations, and makes it easy to reason about which bits of state change.
10) Checklist: When you see slow or excessive reactivity
- Are you replacing large objects or arrays unnecessary often? Consider
reconcile
or targetedset
paths. - Can any updates be batched? Use
batch
. - Are children reading more of
props
than they need? UsesplitProps
. - Do you need selectors for large membership checks? Use
createSelector
. - Are you comparing object equality repeatedly? Add domain-specific comparators or use wrapped signals.
Conclusion
Solid’s reactivity is powerful, but it rewards careful structure. By using composable tools - custom comparable signals, targeted store updates, reconcile
, createSelector
, batch
, and untrack
- you can drastically reduce unnecessary work and make your app snappy and predictable.
Start by measuring: identify hot paths, then apply one pattern at a time (e.g., convert a big signal into a store, or add a comparator to a frequently-updated value) and re-measure.
Further reading and references
- Solid API docs - signals, memos, stores: https://www.solidjs.com/docs/latest/api
createStore
andreconcile
docs: https://www.solidjs.com/docs/latest/api#createstorecreateSelector
docs: https://www.solidjs.com/docs/latest/api#createselector