SolidJS Expert
Overview
The #1 trap is treating Solid like React. Components run ONCE. There is no re-render, no virtual DOM, no dependency arrays. Reactivity is fine-grained: reading a signal inside a tracking scope (JSX, createMemo, createEffect) subscribes that exact spot, and only that spot updates. Most "Solid isn't updating" bugs come from breaking the subscription — destructuring props or reading a signal in code that runs once.
Quick Reference
| Goal | Do | Avoid |
|---|---|---|
| Read a signal | call it: count() |
count (that's the getter, not the value) |
| Use props | props.x lazily, or splitProps/mergeProps |
destructuring { x } (kills reactivity) |
| Derived/expensive value | createMemo(() => …) |
recompute inline every read |
| Side effect | createEffect (runs after render, tracks deps) |
doing effects in component body |
| Render a list | <For each={items()}> |
items().map(...) |
| Conditional | <Show when={ok()}> |
{ok() && …} ternary in once-run code |
| Nested reactive state | createStore + path setters |
a signal holding a big object |
| Explicit dependencies | on(dep, handler) |
relying on implicit tracking |
| Opt out of tracking | untrack(() => …) |
reading then ignoring |
| Async data | createResource(source, fetcher) |
manual fetch in createEffect |
Core Patterns
Don't destructure props — it snapshots the value once and breaks reactivity:
// ❌ name is read once at call time; later updates never reach the DOM
function Hello({ name }: { name: string }) {
return <h1>Hello {name}</h1>
}
// ✅ access lazily, or splitProps to keep reactive proxies intact
function Hello(props: { name: string; class?: string }) {
const [local, rest] = splitProps(props, ['name'])
return <h1 {...rest}>Hello {local.name}</h1> // re-reads on change
}
Signals are getters — call them, and call them inside a tracking scope:
// ❌ destructured + read once in body; the <p> never updates
function Counter() {
const [count] = createSignal(0)
const doubled = count() * 2 // runs once, frozen
return <p>{doubled}</p>
}
// ✅ read inside JSX (a tracking scope) or a memo — see runnable example
Runnable:
examples/counter.tsx
<For> over .map() — keyed by reference, rows persist instead of re-creating:
// ❌ .map() is evaluated once; the list never re-renders on change
<ul>{users().map(u => <li>{u.name}</li>)}</ul>
// ✅ <For> diffs by reference (use <Index> when keyed by position instead)
<For each={users()}>{(u) => <li>{u.name}</li>}</For>
<For> is keyed by item identity (reorders move DOM nodes); <Index> is keyed by position (the item is a signal () => T) — use it for primitives or fixed-length lists.
createStore for nested objects — fine-grained, only touched paths update:
setState('user', 'name', 'Grace') // path setter, surgical
setState('user', 'tags', produce(t => t.push('b'))) // mutate-style via produce
Runnable:
examples/store-nested.ts
createResource for async — the idiomatic data fetch, not fetch in an effect:
// source is a signal/accessor; when it changes (and isn't false/null/undefined)
// the fetcher re-runs. Returns [resource, { mutate, refetch }].
const [user] = createResource(userId, (id) => fetch(`/u/${id}`).then(r => r.json()))
// user() is the data; .loading / .error are reactive
Runnable:
examples/resource-fetch.tsx
mutate(v) writes optimistically; refetch() reloads. Pairs with <Suspense> and <ErrorBoundary>.
Common Mistakes
- Destructuring props — the canonical Solid bug. Keep
propswhole; usemergePropsfor defaults,splitPropsto forward. - Reading a signal without calling it —
{count}renders the function, never updates. It's{count()}. .map()/ternary instead of<For>/<Show>— runs once, won't react. Control-flow components subscribe.- Expecting the component body to re-run — it runs once. Put reactive reads in JSX,
createMemo, orcreateEffect. - Effects to compute values — use
createMemo. ReservecreateEffectfor genuine side effects (DOM, logging, fetch). createEffectwriting a signal it reads — infinite loops; reach foron()withdefer, oruntrack. Batch grouped writes withbatch.
When NOT to over-engineer
createMemo has bookkeeping cost — don't wrap cheap expressions (a() + b()); only memo expensive work or values feeding many subscribers. A plain () => … derived accessor is often enough. Use createStore for genuinely nested state; a single signal is fine for flat values. Reach for on()/untrack/batch only when implicit tracking actually misbehaves, not preemptively.
Reusable primitives
primitives.ts (same dir) — typed, SSR-safe createPersistedSignal (localStorage-backed signal, cross-tab sync) and createEventListener (auto-cleaned binding). Both use onCleanup so listeners die with their owner; copy/adapt rather than re-deriving the lifecycle each time.
Version note
Stable is Solid 1.9.x (everything above targets it). Solid 2.0 is in beta (solid-js@next): async becomes first-class (computations/createMemo can return Promises, reworked <Suspense>, deterministic batching). The 1.9 mental model — components run once, signals are getters, don't destructure props — carries forward; don't adopt 2.0 APIs in production yet.