From 12656346308cd9277e0928a4cb50a2eab6f44c96 Mon Sep 17 00:00:00 2001 From: tsushanth <78000697+tsushanth@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:04:23 -0700 Subject: [PATCH] fix(reconcile): force replacement when nested value type changes between array and object When a tracked store property holds an array and reconcile replaces it with a plain object (or vice versa), the previous code recursively called applyState on the same-typed proxy, leaving the signal node pointing at the old array-shaped wrapper even after STORE_VALUE was updated. Reading the property through a memo therefore still returned Array.isArray() === true for the new object value. The fix adds Array.isArray(previousValue) !== Array.isArray(nextValue) to the early-replacement guard in both applyStateFast and applyStateSlow so a type switch always forces the signal node to be rewritten with a fresh wrapped value rather than patched in place. Fixes #2774 --- ...fix-reconcile-array-object-type-mismatch.md | 5 +++++ packages/solid-signals/src/store/reconcile.ts | 2 ++ .../tests/store/reconcile.test.ts | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 .changeset/fix-reconcile-array-object-type-mismatch.md diff --git a/.changeset/fix-reconcile-array-object-type-mismatch.md b/.changeset/fix-reconcile-array-object-type-mismatch.md new file mode 100644 index 000000000..2c945331f --- /dev/null +++ b/.changeset/fix-reconcile-array-object-type-mismatch.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +reconcile: force wholesale replacement when nested value type changes between array and object diff --git a/packages/solid-signals/src/store/reconcile.ts b/packages/solid-signals/src/store/reconcile.ts index 3cc81ff34..930c88c82 100644 --- a/packages/solid-signals/src/store/reconcile.ts +++ b/packages/solid-signals/src/store/reconcile.ts @@ -170,6 +170,7 @@ function applyStateFast(next: any, target: any, keyFn: (item: NonNullable) !previousValue || !isWrappable(previousValue) || !isWrappable(nextValue) || + Array.isArray(previousValue) !== Array.isArray(nextValue) || (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue)) ) { tracked && setSignal(tracked, void 0); @@ -314,6 +315,7 @@ function applyStateSlow(next: any, target: any, keyFn: (item: NonNullable) !previousValue || !isWrappable(previousValue) || !isWrappable(nextValue) || + Array.isArray(previousValue) !== Array.isArray(nextValue) || (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue)) ) { tracked && setSignal(tracked, void 0); diff --git a/packages/solid-signals/tests/store/reconcile.test.ts b/packages/solid-signals/tests/store/reconcile.test.ts index d4c45557f..70e9aae86 100644 --- a/packages/solid-signals/tests/store/reconcile.test.ts +++ b/packages/solid-signals/tests/store/reconcile.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "vitest"; +import { createMemo, createRoot, flush } from "../../src/index.js"; import { createStore, reconcile, snapshot } from "../../src/index.js"; describe("setState with reconcile", () => { @@ -158,6 +159,23 @@ describe("setState with reconcile", () => { setStore(reconcile({ value: { q: "aa" } }, "id")); expect(store.value).toEqual({ q: "aa" }); }); + + test("Reconcile overwrite tracked array with object updates the signal node", () => { + const [store, setStore] = createStore<{ value: any }>({ value: [1, 2] }); + let derived: any; + + // Establish a tracking subscription on store.value so a signal node is created for it + createRoot(() => { + derived = createMemo(() => store.value); + }); + expect(Array.isArray(derived())).toBe(true); + + setStore(reconcile({ value: { a: 1 } }, "id")); + flush(); + + expect(Array.isArray(derived())).toBe(false); + expect((derived() as any).a).toBe(1); + }); }); // type tests