Repository navigation
Description
- 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-dband@tanstack/db1.0.0-rc.1,solid-js1.9.18.mainhas the same code, and 0.2.45 had the same problem in itssetData(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
KyleAMathews commentedon Nov 6, 2026
🎉 The fix for this issue has been released!
Fixing PRs: #2307
Backfilled notification for published release candidates.