Skip to content

Commit 3e3676b

Browse files
fix: untrack does not exempt owned-scope writes — correct the docs, name the owner in the throw (#3157)
CHEATSHEET listed untrack/untracked blocks as an escape hatch for REACTIVE_WRITE_IN_OWNED_SCOPE, but the guard fires on the ambient owner and untrack only toggles tracking/strictRead — the documented fix throws the identical error. The docs were the wrong side, by construction: component bodies already run untracked and are exactly the scope the guard most needs to cover. Both lines now say so explicitly. Also lands the reporter's enhancement: the thrown message appends the owning scope's name, which previously reached only the diagnostics channel apps don't subscribe to by default. Reported by @antoinevanwel with the correct root-cause analysis. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0932c89 commit 3e3676b

3 files changed

Lines changed: 18 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/signals": patch
3+
---
4+
5+
The owned-scope write guard's thrown message now names the owning scope (previously only the diagnostics channel carried it); CHEATSHEET no longer claims `untrack` exempts owned-scope writes — the guard is owner-based and untrack only stops tracking (#3157)

packages/signals/src/core/core.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,10 +1256,19 @@ export function devGuardStoreSetterWrite(): void {
12561256
ownerName: (context as any)._name,
12571257
data: { operation: "setStore" }
12581258
});
1259-
throw new Error(REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE);
1259+
// the owner name reaches the THROWN message too, not just the
1260+
// diagnostics channel apps don't subscribe to by default (#3157)
1261+
throw new Error(ownedScopeWriteMessage(context));
12601262
}
12611263
}
12621264

1265+
function ownedScopeWriteMessage(owner: Owner) {
1266+
const name = (owner as any)._name;
1267+
return name
1268+
? `${REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE} (in ${name})`
1269+
: REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE;
1270+
}
1271+
12631272
export function setSignal<T>(el: Signal<T> | Computed<T>, v: T | ((prev: T) => T)): T {
12641273
if (
12651274
__DEV__ &&
@@ -1278,7 +1287,7 @@ export function setSignal<T>(el: Signal<T> | Computed<T>, v: T | ((prev: T) => T
12781287
nodeName: (el as any)._name,
12791288
data: { operation: "setSignal" }
12801289
});
1281-
throw new Error(REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE);
1290+
throw new Error(ownedScopeWriteMessage(context));
12821291
}
12831292

12841293
if (el._transition && activeTransition !== el._transition)

packages/solid/CHEATSHEET.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ import { renderToString, renderToStream, isServer, isDev } from "@solidjs/web";
512512
Common dev-mode warnings/errors you may hit:
513513

514514
- **Top-level reactive read in component body** — read inside JSX or wrap in `untrack`/`createMemo`.
515-
- **Write under owned scope** — move setters into event handlers / `onSettled` / `untrack`, or opt in with `{ ownedWrite: true }`.
515+
- **Write under owned scope** — move setters into event handlers / `onSettled` (`untrack` does NOT exempt writes: the guard is owner-based, and untrack only stops tracking), or opt in with `{ ownedWrite: true }`.
516516
- **Strict read untracked** — extract values in the compute phase; don't read store proxies inside the effect callback.
517517
- **Multiple Solid instances** — single `solid-js` install required.
518518

@@ -645,7 +645,7 @@ If your training data is 1.x, these are the corrections. **Read this before gene
645645

646646
- **`createEffect` takes two arguments now**: `(compute, apply)`. The single-arg form is gone — using it is an error.
647647
- **Setters don't update reads immediately** — values become visible after the microtask flushes (or via `flush()`).
648-
- **No writes inside owned scope** — writing a signal/store from inside a memo, effect compute, or component body throws in dev. Move writes to event handlers, `onSettled`, or untracked blocks. Opt in narrowly with `{ ownedWrite: true }` for internal state.
648+
- **No writes inside owned scope** — writing a signal/store from inside a memo, effect compute, or component body throws in dev. Move writes to event handlers or `onSettled`. (`untrack` does not help: the guard fires on the ambient OWNER, which untrack never touches — component bodies already run untracked and are exactly where the guard fires.) Opt in narrowly with `{ ownedWrite: true }` for internal state.
649649
- **No top-level reactive reads in component body** — reading signals/props directly at the top of a component warns. Read inside JSX, a memo, or `untrack`.
650650
- **Props are values, not accessors** — at the call site call accessors (`<X v={count()} />`, not `<X v={count} />`). The single most common AI-generated bug.
651651
- **Don't destructure props**`function Comp({ name })` warns; use `props.name` to keep reactivity. (Same root cause as above; see the Props section.)

0 commit comments

Comments
 (0)