git:20260730.fc8a6f8 to git:20260827.94895ae

12 added, 17 removed. Audit A to A.

---
name: solidjs-v2-reviewer
description: Review SolidJS 2.0 code for React-isms, Solid 1.x-isms, and reactivity bugs. Use when reviewing diffs, PRs, or files in a project that depends on solid-js 2.x / @solidjs/web β€” including self-review after generating Solid 2.0 code.
---
# Review Solid 2.0 code
Hunt the two prior-knowledge bug classes β€” **React reflexes** and **Solid 1.x
reflexes** β€” plus 2.0-specific reactivity mistakes. Severity guide:
πŸ”΄ broken behavior, 🟑 dev-mode diagnostic / lost reactivity, πŸ”΅ style drift.
Confirm the project is actually v2 first (`solid-js` major 2 in package.json /
`@solidjs/web` in deps). Reviewing a 1.x project against this list produces
garbage findings.
## Pass 1 β€” greppable smells
Run these over the changed files; each hit needs a fix or a justification.
### Solid 1.x-isms
| Grep | Verdict | Fix |
|---|---|---|
| `from ['"]solid-js/(web\|store\|h\|html\|universal)` | πŸ”΄ module not found | `@solidjs/web`, store APIs from `solid-js`, `@solidjs/h`… |
| `createResource\|useTransition\|startTransition` | πŸ”΄ removed | async memo + `<Loading>`; built-in transitions / `isPending` |
| `\bbatch\s*\(` | πŸ”΄ removed | delete wrapper; `flush()` only for sync read-after-write |
| `createComputed\|createMutable\|modifyMutable\|createDeferred` | πŸ”΄ removed | memo / split effect / `createSignal(fn)`; `createStore` drafts |
| `\bon\s*\(` as effect dep helper, `onMount\|onError\|catchError` | πŸ”΄ removed | split effect compute; `onSettled`; `<Errored>` / effect `error` |
| `<Suspense\|<SuspenseList\|<ErrorBoundary\|<Index\b` | πŸ”΄ removed | `<Loading>` / `<Reveal>` / `<Errored>` / `<For keyed={false}>` |
| `mergeProps\|splitProps\|unwrap\s*\(\|createSelector` | πŸ”΄ removed | `merge` / `omit` / `snapshot` / `createProjection` |
| `\.Provider\b` | πŸ”΄ removed | `<Ctx value={...}>` β€” context is the provider |
| `classList=` | πŸ”΄ removed | `class={{...}}` / `class={[...]}` |
| `use:[a-zA-Z]\|attr:\|bool:\|on:[a-z]\|oncapture:` in JSX | πŸ”΄ removed | ref factories; standard attributes; `onClick` + ref for native opts |
| `produce\s*\(` in setters | 🟑 redundant | drafts are the default |
| `setStore\s*\(\s*["']` (path-style first arg) | πŸ”΄ wrong API | draft setter or `storePath(...)` |
| `reconcile\([^)]*,\s*\{` | πŸ”΄ 1.x options object | pass the key directly; omit for `"id"`, use `null` for positional |
| `markRaw` imported from `solid-js` | πŸ”΄ fake public API | no root export; use `{ shallow: true }` / replace the slot |
| `/\*@once\*/` | 🟑 ignored marker | reactive read / `defaultValue` / `untrack` |
| `\.loading\b\|\.error\b` on async values | πŸ”΄ no such props | `<Loading>`/`isPending(() => x())` for loading (bare `refresh()` is silent β€” pair with `affects()` for a loud reload) / `<Errored>` for error |
### React-isms
| Grep / pattern | Verdict | Fix |
|---|---|---|
| `function \w+\(\s*\{` (destructured props) | 🟑 reactivity dead + warns | `props.x` access |
| `useState\|useEffect\|useMemo\|useRef\|useCallback` | πŸ”΄ wrong framework | Solid primitives |
| `<X value={count} />` passing an accessor where a value is expected | πŸ”΄ child gets a function | `value={count()}` β€” collapse at the JSX boundary |
| `key=` prop on list items | 🟑 no-op | `<For keyed={...}>` modes |
| `` className\|`${...}` ``/`.join(" ")` class building | πŸ”΅ reflex | `class` array/object form |
| deps-array thinking: effect re-created per "render" | 🟑 model error | components run once; compute phase = deps |
### 2.0-specific
| Pattern | Verdict | Fix |
|---|---|---|
| Single-callback `createEffect(fn)` | πŸ”΄ throws | split `(compute, apply)` |
| `createEffect(fn, 0)` / `createMemo(fn, 0)` initial values | πŸ”΄ wrong arg | options object; `prev` default parameter |
| Setter then immediate read of same signal/DOM | πŸ”΄ stale read | `flush()` or restructure |
| Signal/store write inside memo/compute/component body | πŸ”΄ throws in dev | derive, or move write to handler/action |
- | `actionFn()` invoked inside memo/compute/component body | πŸ”΄ dev error (`ACTION_CALLED_IN_OWNED_SCOPE`, beta.17); may livelock in prod | invoke from handler/effect callback/`onSettled` |
+ | `actionFn()` invoked inside memo/compute/component body | πŸ”΄ dev error (`ACTION_CALLED_IN_OWNED_SCOPE`); may livelock in prod | invoke from handler/effect callback/`onSettled` |
| `ownedWrite: true` on app state | 🟑 escape-hatch abuse | derive instead; ownedWrite is for internal flags |
| Top-level `const x = props.x` / store read in component body | 🟑 warns, stale | read in JSX/memo; `untrack` if deliberate |
| `onCleanup` inside `onSettled`/`createTrackedEffect` | πŸ”΄ throws | return cleanup |
- | Cleanup returned from `onSettled` fired out of band (event handler/tracked effect/nested `onSettled`) | πŸ”΄ dev error (beta.16), dropped in prod | call the setup helper from the component body (owned scope) |
+ | Cleanup returned from `onSettled` fired out of band (event handler/tracked effect/nested `onSettled`) | πŸ”΄ dev error, dropped in prod | call the setup helper from the component body (owned scope) |
| Primitives created inside `onSettled`/tracked effect | πŸ”΄ throws | create in component body |
| Store proxy passed computeβ†’apply, read in apply | 🟑 warns, won't re-run | extract plain values / `deep(store)` in compute |
| Async read with no `<Loading>` ancestor | 🟑 root mount deferred | add boundary where fallback UI is wanted |
| `async function*` memo over a socket/emitter/observable with no up-front `onCleanup` | πŸ”΄ leaks on dispose/re-run | `onCleanup` (before the first `await`/`yield`) that cancels the source; `try/finally`/`.return()` can't unwind a parked generator |
| `refresh()` called inside a computation | πŸ”΄ throws | call from handlers/actions |
| `serverFn.GET` property access, `serverFn.withOptions(` on a server function reference | πŸ”΄ removed | `GET(fn)` wrapper at declaration site; `withMeta(fn, meta)` for metadata; `prepareRequest` for session-dynamic transport (see `solidjs-v2` skill, references/server-functions.md) |
- | `isRefreshing(` call (or imported from `solid-js`) | πŸ”΄ removed in beta.15 | gone from `solid-js` exports; detect a refresh re-run by key comparison, or use `isPending`/`<Loading>` |
+ | `renderToStringAsync` | πŸ”΄ no such export | `await renderToStream(code, options)` |
+ | rich server-function args without `enableRichArguments()` | πŸ”΄ transport throws | call it once from `@solidjs/web/server-functions/rich-args` |
| `<For>` callback shape vs keying mode mismatch (`item()` on keyed, `i()` on `keyed={false}`) | πŸ”΄ type/runtime error | check the mode table |
| Dynamic boolean `keyed={cond()}` with function children | 🟑 ambiguous shape | literal mode or key function |
| `useX`-with-throw context wrapper hooks | πŸ”΅ dead boilerplate | direct `useContext` (throws by itself) |
| camelCase DOM attributes (`tabIndex`, `readOnly`) | 🟑 wrong attribute | lowercase; handlers stay camelCase |
| `merge(..., maybeUndefined)` assuming skip semantics | πŸ”΄ silently overrides | filter keys or restructure defaults |
## Pass 2 β€” judgement checks (not greppable)
- **Derive vs write-back**: any effect whose apply phase sets reactive state is
suspect β€” usually a memo/projection in disguise.
- **Boundary ownership**: `isPending` reads placed under the `Loading` boundary
that owns the data read? Pending indicators outside can never fire.
- **Mutation shape**: server writes wrapped in `action()` with optimistic
state and a final `refresh()`? Ad-hoc async handlers flipping flags are the
1.x smell in new clothes.
- **Action call site**: an action may be defined in a component, but is it
invoked only from an imperative scope? A component-body/computation call is
- a transaction-starting write and throws in dev mode (since beta.17).
+ a transaction-starting write and throws in dev mode.
- **Optimistic spinner off `isPending`**: a "Saving…" indicator driven by
`isPending` on data the same action just wrote optimistically can never show β€”
- not because the optimistic write masks it (that mask is removed as of
- beta.21; optimistic writes are verdict-inert), but because a bare
+ not because the optimistic write masks it (optimistic writes are
+ verdict-inert), but because a bare
`refresh()` after the write is a silent same-question re-ask and was never
going to flip `isPending`. The flag belongs in the data (co-written
`pending: true` or a separate `createOptimistic(false)`); if the reload
itself should read pending, that needs an explicit `affects(target)` before
the `refresh()`.
- - **Stale beta.17–20 mask assumptions**: code (or comments) that reason about
- an optimistic write "masking" `isPending` store-wide, or that treat a bare
- `refresh()` as if it were pending on its own β€” both were beta.17–beta.20
- behavior, removed/superseded in beta.21 (`question-scoped-pending-affects`).
- On beta.21+ typings this silently changes UI (a spinner that used to show now
- doesn't, or vice versa) with no compiler error to catch it β€” flag any
- `isPending` use next to an optimistic write and check it against the current
- rule, not habit.
+ - **SSR setter writes**: signal/store setters in server render are deprecated
+ and warn; optimistic server setters are no-ops. Model incoming changes as
+ async sources instead of pushing through setters.
- **Granularity**: selection/derived caches notifying whole collections β†’
`createProjection`. Fixed-slot lists diffed with `<For>` β†’ `<Repeat>`.
- **Shallow-store writes**: with `{ shallow: true }`, are nested raw records
mutated in place? That is inert; replace the root property/array slot by
reference. When refreshes rebuild row objects, use consumer keying such as
`<For keyed={row => row.id}>` if row DOM identity must survive.
- **Reconcile model**: omission means key `"id"`, `null` means fully
positional, and missing item keys fall back positionally. Shape mismatch at
a nested array/object slot replaces it. Do not approve claims that standalone
`reconcile` silently swaps a different root entity (it is strict), or the
inverse claim that projection/derived-store returned roots cannot perform an
authoritative swap. At a shallow boundary reconciliation compares records
by reference rather than mutating their fields.
- **Raw-object assumptions**: platform/native host objects are raw by default;
their slot reassignment is reactive but internal mutation is not. User class
instances remain wrappable, and `markRaw` is not a public root API.
- **Ownership**: module-scope effects/roots intentional? Detached lifetime must
be explicit (`runWithOwner(null, ...)`).
- **Composable naming**: a `createX`/`useX` prefix should match lifecycle, not
React habit β€” `createX` makes a fresh instance owned by the caller, `useX` is a
shared singleton or accesses an already-created thing (`useContext`). `useX` is
not wrong by itself (singletons are legit); flag only a per-call instance named
`useX`, or every composable defaulting to `useX` out of reflex.
- **Layout lane**: DOM-geometry reads (`getBoundingClientRect`/`offset*`) belong
in a `createRenderEffect` (render lane), not in a `ref` callback (node may be
pre-insert/pre-layout there). Beware the inverse "fix" too: moving a layout
measure out of `createRenderEffect` into `createEffect`/a ref on the false
theory that render effects read a disconnected node β€” they don't; the trigger
is flush-scheduled and runs after insertion.
- **Tests**: `flush()` after writes; `createRoot` wrappers; `resolve()` for
async settling.
## Reporting
Report findings ordered by severity with `file:line`, the broken expectation
(one line), and the concrete 2.0 fix. Note clean areas that were checked.
For deep API verification during review, the `solidjs-v2` skill's references
- cover signatures; installed typings in `node_modules` are final word β€” the
- betas churn the public API freely (e.g. `isRefreshing` was a public `solid-js`
- export from beta.0 through beta.14, *then* removed in beta.15).
+ cover signatures; installed typings in `node_modules` are the final word for a
+ moving prerelease API.