Skip to content

Commit db88de1

Browse files
fix: createReaction crash when rerun tracks zero dependencies
dispose() used a do/while that called unlinkSubs once even when node._deps was null, dereferencing a null link. Guard the loop so a reaction whose invalidating rerun reads no dependencies disposes cleanly. Fixes #2763. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7a1695a commit db88de1

3 files changed

Lines changed: 31 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+
Fix `createReaction` crashing with `Cannot read properties of null (reading '_dep')` when the invalidating rerun of the tracked callback reads zero dependencies. `dispose()` now guards its dependency-unlink loop instead of unconditionally calling `unlinkSubs` once.

packages/solid-signals/src/core/owner.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ export function markDisposal(el: Owner): void {
3636
}
3737

3838
export function dispose(node: Computed<unknown>): void {
39-
let toRemove = node._deps || null;
40-
do {
41-
toRemove = unlinkSubs(toRemove!);
42-
} while (toRemove !== null);
39+
let toRemove = node._deps;
40+
while (toRemove !== null) {
41+
toRemove = unlinkSubs(toRemove);
42+
}
4343
node._deps = null;
4444
node._depsTail = null;
4545
disposeChildren(node, true);

packages/solid-signals/tests/createReaction.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,28 @@ describe("createReaction", () => {
2727
expect(count).toBe(2);
2828
});
2929

30+
test("fires and disposes cleanly when the rerun tracks zero dependencies", () => {
31+
let fired = 0;
32+
let readDep = true;
33+
const [sign, setSign] = createSignal("a");
34+
createRoot(() => {
35+
const track = createReaction(() => {
36+
fired++;
37+
});
38+
track(() => {
39+
// initial run reads a dep; the invalidating rerun reads none
40+
if (readDep) sign();
41+
});
42+
});
43+
flush();
44+
expect(fired).toBe(0);
45+
46+
readDep = false;
47+
setSign("b");
48+
expect(() => flush()).not.toThrow();
49+
expect(fired).toBe(1);
50+
});
51+
3052
test("throws on invalid cleanup values", () => {
3153
const [sign, setSign] = createSignal("thoughts");
3254
const track = createRoot(() =>

0 commit comments

Comments
 (0)