From 54b2175cfd1991a633d3316f9fea08fedd0f1c74 Mon Sep 17 00:00:00 2001 From: tsushanth <78000697+tsushanth@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:59:47 -0700 Subject: [PATCH] fix(reconcile): notify ownKeys subscribers on pure keyed trailing removal When keyed reconcile removes only trailing items, the early-exit branch skipped notifySelf because no element was rewritten (changed stayed false). Widen the guard to also fire when the array length changed, ensuring $TRACK subscribers are invalidated in both applyStateFast and applyStateSlow. Fixes #2773 --- .../fix-reconcile-trailing-removal-notify.md | 5 ++++ packages/solid-signals/src/store/reconcile.ts | 4 ++-- .../tests/store/reconcile.test.ts | 23 ++++++++++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-reconcile-trailing-removal-notify.md diff --git a/.changeset/fix-reconcile-trailing-removal-notify.md b/.changeset/fix-reconcile-trailing-removal-notify.md new file mode 100644 index 000000000..4d78567db --- /dev/null +++ b/.changeset/fix-reconcile-trailing-removal-notify.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +reconcile: notify ownKeys subscribers on pure keyed trailing removal diff --git a/packages/solid-signals/src/store/reconcile.ts b/packages/solid-signals/src/store/reconcile.ts index 3cc81ff34..6ad7d6693 100644 --- a/packages/solid-signals/src/store/reconcile.ts +++ b/packages/solid-signals/src/store/reconcile.ts @@ -99,7 +99,7 @@ function applyStateFast(next: any, target: any, keyFn: (item: NonNullable) applyState(next[j], wrapped, keyFn); } - changed && notifySelf(target); + (changed || prevLength !== next.length) && notifySelf(target); prevLength !== next.length && arrayNodes?.length && setSignal(arrayNodes.length, next.length); @@ -244,7 +244,7 @@ function applyStateSlow(next: any, target: any, keyFn: (item: NonNullable) } const nextLength = next.length; - changed && notifySelf(target); + (changed || prevLength !== nextLength) && notifySelf(target); prevLength !== nextLength && nodes?.length && setSignal(nodes.length, nextLength); return; } diff --git a/packages/solid-signals/tests/store/reconcile.test.ts b/packages/solid-signals/tests/store/reconcile.test.ts index d4c45557f..3e687f438 100644 --- a/packages/solid-signals/tests/store/reconcile.test.ts +++ b/packages/solid-signals/tests/store/reconcile.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { createStore, reconcile, snapshot } from "../../src/index.js"; +import { createStore, reconcile, snapshot, $TRACK, createMemo, createRoot, createEffect, createRenderEffect, flush } from "../../src/index.js"; describe("setState with reconcile", () => { test("Reconcile a simple object", () => { @@ -158,6 +158,27 @@ describe("setState with reconcile", () => { setStore(reconcile({ value: { q: "aa" } }, "id")); expect(store.value).toEqual({ q: "aa" }); }); + test("Reconcile keyed trailing removal notifies $TRACK subscribers", () => { + let effectRunCount = 0; + const [state, setState] = createStore({ arr: [{ id: 1 }, { id: 2 }, { id: 3 }] }); + createRoot(() => { + createRenderEffect(() => { + effectRunCount++; + // accessing $TRACK subscribes to ownKeys notifications on arr + (state.arr as any)[$TRACK]; + return undefined; + }, () => undefined); + }); + // flush to run the effect initially + flush(); + const runsBefore = effectRunCount; + setState(s => { + reconcile([{ id: 1 }, { id: 2 }], "id")(s.arr); + }); + // flush to propagate invalidation and re-run the effect + flush(); + expect(effectRunCount).toBeGreaterThan(runsBefore); + }); }); // type tests