SolidJS 2.0
Solid 2.0 is not React and not Solid 1.x. Both priors are the dominant
bug sources in generated code. When in doubt, distrust pattern-matching and
check references/cheatsheet.md (official, ships with the package) or the
installed typings in node_modules.
Step 0 — confirm this is actually a v2 project
Check before applying anything below:
package.json:solid-jsmajor is2(e.g.2.0.0-rc.x), and/or@solidjs/webis a dependency.tsconfig.json:"jsxImportSource": "@solidjs/web".
If solid-js is 1.x (imports like solid-js/web, solid-js/store), stop —
these rules do not apply; that's a Solid 1.x project. If the task is to
convert it, use the solidjs-v2-migration skill instead.
Prereleases drift: when docs and the installed package disagree, trust the typings in
node_modules (solid-js, @solidjs/web, @solidjs/signals).
The ten rules that prevent most bugs
- Reads lag writes. Updates apply on the next microtask:
setCount(1); count()still returns0. Synchronous point:flush().batch()does not exist. createEffecttakes two functions —(compute, apply, options?). Compute tracks and returns a value; apply does side effects (untracked) and may return a cleanup. The 1.x single-callback form throws.on(),createComputed, initial-value args: all gone.- Never write signals/stores or invoke an action inside a reactive scope
(memo, compute, component body) — throws in dev. Define actions there if
useful, but invoke/write from event handlers, effect callbacks, actions, or
onSettled.untrack()suppresses read tracking but does not exempt writes. Derive instead of writing back. - No top-level reactive reads in component bodies and no destructured
props — warns, value goes stale. Read via
props.xinside JSX / memos / effect computes;untrack(() => ...)for deliberate one-shots. - Props are values, not accessors. Call site:
<X v={count()} />, never<X v={count} />. Child:props.v, neverfunction X({ v }). This stays reactive — the compiler turnsv={count()}into{ get v() { return count() } }, so readingprops.vin the child re-runscount()in the child's tracking scope. Passing the accessor (v={count}+props.v()) to "keep reactivity" is a misconception: props have always been getters in Solid (1.x and 2.0 alike — value-passing didn't change), so it's unnecessary and just forces every consumer to call a function. - Async is just a computation:
const user = createMemo(() => fetchUser(id()))— nocreateResource. Wrap consumers in<Loading fallback={...}>; errors go to<Errored>. In-flight-change indicators:isPending(() => user())— fires for changed inputs andaffects()declarations; a barerefresh()is normally quiet.await refresh(source)waits for the settled re-ask;until(predicate)waits for a truthy live-source acknowledgement. - Store setters take a draft:
setStore(s => { s.a.b = 1; })(produce is the default). Store APIs (createStore,reconcile,snapshot…) are exported fromsolid-js—solid-js/storedoes not exist. - List rendering is
Forwith keying modes —<Index>is gone. Callback shapes differ per mode (see references);keyed={false}gives(itemAccessor, plainIndex). Fixed-count rendering:<Repeat>. - Lifecycle:
onSettled(() => { ...; return cleanup; })replacesonMount/onCleanupfor component-level setup-and-teardown. It's a leaf owner — no primitives oronCleanupinside. - Imports moved:
@solidjs/webforrender/hydrate/Portal/Dynamic(notsolid-js/web);jsxImportSource: "@solidjs/web"; DOM attributes are lowercase (tabindex);classtakes object/array forms (classListis gone); directives areref={factory(opts)}(use:is gone).
Reference routing
Read the file matching the task before writing code in that area:
| Task touches | Read |
|---|---|
| Quick API lookup, import list, full 1.x→2.0 footgun list | references/cheatsheet.md (official) |
Signals, memos, split/render effects and paint timing, createReaction, batching/flush, lifecycle, ownership, dev diagnostics |
references/reactivity.md |
| Data fetching, loading values, async iterator completion, Loading/Errored, isPending/latest/resolve/awaitable refresh/until, action call scope/errors, optimistic UI | references/async-and-actions.md |
| createStore, reconcile, projections, nested store-view structural tracking, compiler patch-driver boundary, snapshot/deep, merge/omit, storePath | references/stores.md |
| For/Repeat/Show/Switch/Reveal, dynamic/lazy components, lazy SSR/hydration identity, class/attributes/events/refs/directives, render entries | references/control-flow-and-dom.md |
| tsconfig, JSX types, import paths, Context typing, test setup | references/typescript-setup.md |
| Composed patterns: SWR query, optimistic mutations, selection projections, global state, demand-driven resources | references/patterns.md |
Naming a primitive/composable (create* vs use*), cross-cutting conventions |
references/conventions.md |
"use server" directive, module/function wrappers, server-function addressing/invoke/live, respond/redirect/reload, GET/withMeta, fetch/prepareRequest, named single-flight, no-JS, getRequestEvent |
references/server-functions.md |
Experimental server components, frames, client slots/state preservation, installServerComponents, serverFunctions: { components: true } |
references/server-components.md |
Failure modes
- App renders nothing / mount seems stuck → pending async outside a
Loadingboundary defers the root mount; check the console forASYNC_OUTSIDE_LOADING_BOUNDARY. - Dev throws/warns with a diagnostic code (
REACTIVE_WRITE_IN_OWNED_SCOPE,STRICT_READ_UNTRACKED, …) → table of codes and fixes at the bottom ofreferences/reactivity.md. Fix the cause; never silence withownedWritefor app state. - Test asserts stale values → missing
flush()after writes, or reactive code created without an owner (createRootin tests). - An API from docs/examples doesn't exist → prereleases drift; verify against installed typings and prefer them over any doc, including these references.