Skip to content

solid-db: useLiveQuery mutates the collection's row objects in place #2305

Closed

Description

@asterikx
  • I've validated the bug against the latest version of DB packages

Describe the bug

useLiveQuery in @tanstack/solid-db keeps one store per row: createStore(value) wraps the row object it receives from the live-query collection, and each update runs setRow(reconcile(nextValue, { key: null })) on that store (useLiveQuery.ts). A Solid store writes into the object it wraps, so every update changes the earlier row object in place, together with each nested object it shares with the source collection. Code that holds a row, such as a value from collection.get(), a mutation's original or a change event's previousValue, sees it change afterwards. In our app this broke undo/redo, which replays mutation.original.

To Reproduce

import { createCollection, localOnlyCollectionOptions, useLiveQuery } from "@tanstack/solid-db";
import { createRoot } from "solid-js";
import { expect, test, vi } from "vitest";

type Row = { id: string; config: { value: number } };

test("useLiveQuery leaves the collection's rows alone", async () => {
  const collection = createCollection(
    localOnlyCollectionOptions<Row>({
      id: "rows",
      getKey: (row) => row.id,
      initialData: [{ id: "a", config: { value: 1 } }],
    }),
  );
  const original = collection.get("a");

  await createRoot(async (dispose) => {
    const rows = useLiveQuery((q) => q.from({ row: collection }).select(({ row }) => ({ ...row })));
    await vi.waitFor(() => expect(rows()).toHaveLength(1));
    collection.update("a", (draft) => {
      draft.config = { value: 2 };
    });
    await vi.waitFor(() => expect(rows()[0]?.config.value).toBe(2));
    dispose();
  });

  expect(original?.config.value).toBe(1); // fails: 2
});

Expected behavior

The row objects the collection hands out stay as they were: original.config.value is still 1 after the update.

Additional context

  • Versions: @tanstack/solid-db and @tanstack/db 1.0.0-rc.1, solid-js 1.9.18. main has the same code, and 0.2.45 had the same problem in its setData(reconcile(...)) call.
  • Environment: vitest in Node; nothing here is browser-specific.
  • Cloning before the store takes the value fixes it for us:
-        const [row, setRow] = createStore(value)
+        const [row, setRow] = createStore(structuredClone(value))
         rowsByKey.set(key, {
           value: row,
-          update: (nextValue) => setRow(reconcile(nextValue, { key: null })),
+          update: (nextValue) => setRow(reconcile(structuredClone(nextValue), { key: null })),
         })

Activity

  1. KyleAMathews commented on Nov 6, 2026

    @KyleAMathews
    Collaborator

    🎉 The fix for this issue has been released!

    Fixing PRs: #2307

    Backfilled notification for published release candidates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions