From 18b5d46fbef95dfde90e3c9fabae9c721ebc3f07 Mon Sep 17 00:00:00 2001 From: fezproof <26830309+fezproof@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:23:21 +0800 Subject: [PATCH 01/13] Update useLiveQuery to be more similar to solid resource Enabled suspense on the solidjs API --- packages/solid-db/src/useLiveQuery.ts | 246 +++++++++++++++----------- 1 file changed, 144 insertions(+), 102 deletions(-) diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index d3e645871..cb1f8aa4e 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -1,6 +1,6 @@ import { batch, - createComputed, + createEffect, createMemo, createResource, createSignal, @@ -62,33 +62,46 @@ import type { * * return ( * - * + * *
Loading...
*
- * + * *
Error: {todosQuery.status()}
*
- * - * + * + * * {(todo) =>
  • {todo.text}
  • } *
    *
    *
    * ) + * + * * @example + * // Use Suspense boundaries + * const todosQuery = useLiveQuery((q) => + * q.from({ todos: todoCollection }) + * ) + * + * return ( + * Loading...}> + * + * {(todo) =>
  • {todo.text}
  • } + *
    + * + * ) */ // Overload 1: Accept just the query function export function useLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder -): { +): Accessor>> & { state: ReactiveMap> - data: Array> - collection: Accessor, string | number, {}>> - status: Accessor - isLoading: Accessor - isReady: Accessor - isIdle: Accessor - isError: Accessor - isCleanedUp: Accessor + collection: Collection, string | number, {}> + status: CollectionStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean } /** @@ -118,14 +131,14 @@ export function useLiveQuery( * })) * * return ( - * {itemsQuery.data.length} items loaded}> - * + * {itemsQuery().length} items loaded}> + * *
    Loading...
    *
    - * + * *
    Something went wrong
    *
    - * + * *
    Preparing...
    *
    *
    @@ -134,16 +147,15 @@ export function useLiveQuery( // Overload 2: Accept config object export function useLiveQuery( config: Accessor> -): { +): Accessor>> & { state: ReactiveMap> - data: Array> - collection: Accessor, string | number, {}>> - status: Accessor - isLoading: Accessor - isReady: Accessor - isIdle: Accessor - isError: Accessor - isCleanedUp: Accessor + collection: Collection, string | number, {}> + status: CollectionStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean } /** @@ -163,7 +175,7 @@ export function useLiveQuery( * * // Use collection for mutations * const handleToggle = (id) => { - * existingQuery.collection().update(id, draft => { draft.completed = !draft.completed }) + * existingQuery.collection.update(id, draft => { draft.completed = !draft.completed }) * } * * @example @@ -171,11 +183,11 @@ export function useLiveQuery( * const sharedQuery = useLiveQuery(() => sharedCollection) * * return ( - * {(item) => }}> - * + * {(item) => }}> + * *
    Loading...
    *
    - * + * *
    Error loading data
    *
    *
    @@ -188,16 +200,15 @@ export function useLiveQuery< TUtils extends Record, >( liveQueryCollection: Accessor> -): { +): Accessor> & { state: ReactiveMap - data: Array - collection: Accessor> - status: Accessor - isLoading: Accessor - isReady: Accessor - isIdle: Accessor - isError: Accessor - isCleanedUp: Accessor + collection: Collection + status: CollectionStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean } // Implementation - use function overloads to infer the actual collection type @@ -250,79 +261,110 @@ export function useLiveQuery( ) } - // Track current unsubscribe function - let currentUnsubscribe: (() => void) | null = null - - createComputed( - () => { - const currentCollection = collection() - - // Update status ref whenever the effect runs + const [getDataResource] = createResource( + () => ({ currentCollection: collection() }), + async ({ currentCollection }) => { setStatus(currentCollection.status) - + await currentCollection.toArrayWhenReady() // Initialize state with current collection data - state.clear() - for (const [key, value] of currentCollection.entries()) { - state.set(key, value) - } + batch(() => { + state.clear() + for (const [key, value] of currentCollection.entries()) { + state.set(key, value) + } + syncDataFromCollection(currentCollection) + setStatus(currentCollection.status) + }) + return data + }, + { + name: `TanstackDBData`, + deferStream: false, + initialValue: data, + } + ) - // Subscribe to collection changes with granular updates - const subscription = currentCollection.subscribeChanges( - (changes: Array>) => { - // Apply each change individually to the reactive state - batch(() => { - for (const change of changes) { - switch (change.type) { - case `insert`: - case `update`: - state.set(change.key, change.value) - break - case `delete`: - state.delete(change.key) - break - } + createEffect(() => { + const currentCollection = collection() + const subscription = currentCollection.subscribeChanges( + // Changes is fine grained, so does not work great with an array + (changes: Array>) => { + // Apply each change individually to the reactive state + batch(() => { + for (const change of changes) { + switch (change.type) { + case `insert`: + case `update`: + state.set(change.key, change.value) + break + case `delete`: + state.delete(change.key) + break } - }) + } - // Update the data array to maintain sorted order syncDataFromCollection(currentCollection) // Update status ref on every change setStatus(currentCollection.status) - }, - { - includeInitialState: true, - } - ) - - currentUnsubscribe = subscription.unsubscribe.bind(subscription) - - // Preload collection data if not already started - if (currentCollection.status === `idle`) { - createResource(() => currentCollection.preload()) + }) + }, + { + includeInitialState: true, } + ) - // Cleanup when computed is invalidated - onCleanup(() => { - if (currentUnsubscribe) { - currentUnsubscribe() - currentUnsubscribe = null - } - }) - }, - undefined, - { name: `TanstackDBSyncComputed` } - ) + onCleanup(() => { + subscription.unsubscribe() + }) + }) - return { - state, - data, - collection, - status, - isLoading: () => status() === `loading`, - isReady: () => status() === `ready`, - isIdle: () => status() === `idle`, - isError: () => status() === `error`, - isCleanedUp: () => status() === `cleaned-up`, + // We have to remove getters from the resource function so we wrap it + function getData() { + return getDataResource() } + + Object.defineProperties(getData, { + status: { + get() { + return status() + }, + }, + collection: { + get() { + return collection() + }, + }, + state: { + get() { + return state + }, + }, + isLoading: { + get() { + return status() === `loading` + }, + }, + isReady: { + get() { + return status() === `ready` + }, + }, + isIdle: { + get() { + return status() === `idle` + }, + }, + isError: { + get() { + return status() === `error` + }, + }, + isCleanedUp: { + get() { + return status() === `cleaned-up` + }, + }, + }) + return getData } From b91a676cf8b5b545af7ebfe0decbb62ce1672c90 Mon Sep 17 00:00:00 2001 From: fezproof <26830309+fezproof@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:23:32 +0800 Subject: [PATCH 02/13] Updated tests --- packages/solid-db/tests/useLiveQuery.test.tsx | 148 +++++++++--------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index 459a10bd1..76a55e463 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -105,9 +105,9 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(rendered.result.state.size).toBe(1) // Only John Smith (age 35) }) - expect(rendered.result.data).toHaveLength(1) + expect(rendered.result()).toHaveLength(1) - const johnSmith = rendered.result.data[0] + const johnSmith = rendered.result()[0] expect(johnSmith).toMatchObject({ id: `3`, name: `John Smith`, @@ -146,8 +146,8 @@ describe(`Query Collections`, () => { name: `John Smith`, }) - expect(rendered.result.data.length).toBe(1) - expect(rendered.result.data[0]).toMatchObject({ + expect(rendered.result().length).toBe(1) + expect(rendered.result()[0]).toMatchObject({ id: `3`, name: `John Smith`, }) @@ -179,8 +179,8 @@ describe(`Query Collections`, () => { name: `Kyle Doe`, }) - expect(rendered.result.data.length).toBe(2) - expect(rendered.result.data).toEqual( + expect(rendered.result().length).toBe(2) + expect(rendered.result()).toEqual( expect.arrayContaining([ expect.objectContaining({ id: `3`, @@ -216,8 +216,8 @@ describe(`Query Collections`, () => { name: `Kyle Doe 2`, }) - expect(rendered.result.data.length).toBe(2) - expect(rendered.result.data).toEqual( + expect(rendered.result().length).toBe(2) + expect(rendered.result()).toEqual( expect.arrayContaining([ expect.objectContaining({ id: `3`, @@ -250,8 +250,8 @@ describe(`Query Collections`, () => { }) expect(rendered.result.state.get(`4`)).toBeUndefined() - expect(rendered.result.data.length).toBe(1) - expect(rendered.result.data[0]).toMatchObject({ + expect(rendered.result().length).toBe(1) + expect(rendered.result()[0]).toMatchObject({ id: `3`, name: `John Smith`, }) @@ -286,7 +286,7 @@ describe(`Query Collections`, () => { .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, - name: persons.name, + name: persons!.name, })) ) }) @@ -373,11 +373,11 @@ describe(`Query Collections`, () => { }) issueCollection.utils.commit() - await new Promise((resolve) => setTimeout(resolve, 10)) - - // After deletion, issue 3 should no longer have a joined result - expect(result.state.get(`[3,1]`)).toBeUndefined() - expect(result.state.size).toBe(3) + await waitFor(() => { + // After deletion, issue 3 should no longer have a joined result + expect(result.state.get(`[3,1]`)).toBeUndefined() + expect(result.state.size).toBe(3) + }) }) it(`should recompile query when parameters change and change results`, async () => { @@ -548,7 +548,7 @@ describe(`Query Collections`, () => { const groupedLiveQuery = renderHook(() => { return useLiveQuery((q) => q - .from({ queryResult: rendered.result.collection() }) + .from({ queryResult: rendered.result.collection }) .groupBy(({ queryResult }) => queryResult.team) .select(({ queryResult }) => ({ team: queryResult.team, @@ -651,7 +651,7 @@ describe(`Query Collections`, () => { .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, - name: persons.name, + name: persons!.name, })) ) @@ -797,9 +797,9 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(result.state.size).toBe(1) // Only John Smith (age 35) }) - expect(result.data).toHaveLength(1) + expect(result()).toHaveLength(1) - const johnSmith = result.data[0] + const johnSmith = result()[0] expect(johnSmith).toMatchObject({ id: `3`, name: `John Smith`, @@ -807,7 +807,7 @@ describe(`Query Collections`, () => { }) // Verify that the returned collection is the same instance - expect(result.collection()).toBe(liveQueryCollection) + expect(result.collection).toBe(liveQueryCollection) }) it(`should switch to a different pre-created live query collection when changed`, async () => { @@ -886,7 +886,7 @@ describe(`Query Collections`, () => { id: `3`, name: `John Smith`, }) - expect(rendered.result.collection()).toBe(liveQueryCollection1) + expect(rendered.result.collection).toBe(liveQueryCollection1) // Switch to the second collection setCollection(liveQueryCollection2) @@ -903,7 +903,7 @@ describe(`Query Collections`, () => { id: `5`, name: `Bob Dylan`, }) - expect(rendered.result.collection()).toBe(liveQueryCollection2) + expect(rendered.result.collection).toBe(liveQueryCollection2) // Verify we no longer have data from the first collection expect(rendered.result.state.get(`3`)).toBeUndefined() @@ -939,9 +939,9 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(result.state.size).toBe(1) // Only John Smith (age 35) }) - expect(result.data).toHaveLength(1) + expect(result()).toHaveLength(1) - const johnSmith = result.data[0] + const johnSmith = result()[0] expect(johnSmith).toMatchObject({ id: `3`, name: `John Smith`, @@ -987,7 +987,7 @@ describe(`Query Collections`, () => { }) // Initially isLoading should be true - expect(rendered.result.isLoading()).toBe(true) + expect(rendered.result.isLoading).toBe(true) // Start sync manually collection.preload() @@ -1010,7 +1010,7 @@ describe(`Query Collections`, () => { // Wait for collection to become ready await waitFor(() => { - expect(rendered.result.isLoading()).toBe(false) + expect(rendered.result.isLoading).toBe(false) }) // Note: Data may not appear immediately due to live query evaluation timing // The main test is that isLoading transitions from true to false @@ -1046,7 +1046,7 @@ describe(`Query Collections`, () => { }) // For pre-created collections that are already syncing, isLoading should be true - expect(rendered.result.isLoading()).toBe(false) + expect(rendered.result.isLoading).toBe(false) expect(rendered.result.state.size).toBe(1) }) @@ -1085,7 +1085,7 @@ describe(`Query Collections`, () => { }) // Initially should be true - expect(rendered.result.isLoading()).toBe(true) + expect(rendered.result.isLoading).toBe(true) // Start sync manually collection.preload() @@ -1109,14 +1109,14 @@ describe(`Query Collections`, () => { await new Promise((resolve) => setTimeout(resolve, 100)) - expect(rendered.result.isLoading()).toBe(false) - expect(rendered.result.isReady()).toBe(true) + expect(rendered.result.isLoading).toBe(false) + expect(rendered.result.isReady).toBe(true) // Wait for collection to become ready await waitFor(() => { - expect(rendered.result.isLoading()).toBe(false) + expect(rendered.result.isLoading).toBe(false) }) - expect(rendered.result.status()).toBe(`ready`) + expect(rendered.result.status).toBe(`ready`) }) it(`should maintain isReady state during live updates`, async () => { @@ -1142,10 +1142,10 @@ describe(`Query Collections`, () => { // Wait for initial load await waitFor(() => { - expect(result.isLoading()).toBe(false) + expect(result.isLoading).toBe(false) }) - const initialIsReady = result.isReady() + const initialIsReady = result.isReady // Perform live updates collection.utils.begin() @@ -1168,8 +1168,8 @@ describe(`Query Collections`, () => { }) // isReady should remain true during live updates - expect(result.isReady()).toBe(true) - expect(result.isReady()).toBe(initialIsReady) + expect(result.isReady).toBe(true) + expect(result.isReady).toBe(initialIsReady) }) it(`should handle isLoading with complex queries including joins`, async () => { @@ -1224,13 +1224,13 @@ describe(`Query Collections`, () => { .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, - name: persons.name, + name: persons!.name, })) ) }) // Initially should be true - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) // Start sync for both collections personCollection.preload() @@ -1266,7 +1266,7 @@ describe(`Query Collections`, () => { // Wait for both collections to sync await waitFor(() => { - expect(result.isReady()).toBe(true) + expect(result.isReady).toBe(true) }) // Note: Joined data may not appear immediately due to live query evaluation timing // The main test is that isLoading transitions from false to true @@ -1313,7 +1313,7 @@ describe(`Query Collections`, () => { ) // Initially should be false - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) // Start sync manually collection.preload() @@ -1344,7 +1344,7 @@ describe(`Query Collections`, () => { // Wait for initial load await waitFor(() => { - expect(result.isLoading()).toBe(false) + expect(result.isLoading).toBe(false) }) // Change parameters @@ -1352,7 +1352,7 @@ describe(`Query Collections`, () => { // isReady should remain true even when parameters change await waitFor(() => { - expect(result.isReady()).toBe(true) + expect(result.isReady).toBe(true) }) // Note: Data size may not change immediately due to live query evaluation timing // The main test is that isReady remains true when parameters change @@ -1399,9 +1399,9 @@ describe(`Query Collections`, () => { }) // Initially isLoading should be true - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) expect(result.state.size).toBe(0) - expect(result.data).toEqual([]) + expect(result()).toEqual([]) // Start sync manually collection.preload() @@ -1409,7 +1409,7 @@ describe(`Query Collections`, () => { await new Promise((resolve) => setTimeout(resolve, 10)) // Still loading - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) // Add first batch of data (but don't mark ready yet) syncBegin!() @@ -1430,9 +1430,9 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(result.state.size).toBe(1) }) - expect(result.isLoading()).toBe(true) // Still loading - expect(result.data).toHaveLength(1) - expect(result.data[0]).toMatchObject({ + expect(result.isLoading).toBe(true) // Still loading + expect(result()).toHaveLength(1) + expect(result()[0]).toMatchObject({ id: `1`, name: `John Smith`, }) @@ -1456,18 +1456,18 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(result.state.size).toBe(2) }) - expect(result.isLoading()).toBe(true) // Still loading - expect(result.data).toHaveLength(2) + expect(result.isLoading).toBe(true) // Still loading + expect(result()).toHaveLength(2) // Now mark as ready syncMarkReady!() // Should now be ready await waitFor(() => { - expect(result.isLoading()).toBe(false) + expect(result.isLoading).toBe(false) }) expect(result.state.size).toBe(2) - expect(result.data).toHaveLength(2) + expect(result()).toHaveLength(2) }) it(`should show filtered results during sync with isLoading true`, async () => { @@ -1511,7 +1511,7 @@ describe(`Query Collections`, () => { await new Promise((resolve) => setTimeout(resolve, 10)) - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) // Add items from different teams syncBegin!() @@ -1554,15 +1554,15 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(result.state.size).toBe(2) }) - expect(result.isLoading()).toBe(true) - expect(result.data).toHaveLength(2) - expect(result.data.every((p) => p.team === `team1`)).toBe(true) + expect(result.isLoading).toBe(true) + expect(result()).toHaveLength(2) + expect(result().every((p) => p.team === `team1`)).toBe(true) // Mark ready syncMarkReady!() await waitFor(() => { - expect(result.isLoading()).toBe(false) + expect(result.isLoading).toBe(false) }) expect(result.state.size).toBe(2) }) @@ -1622,7 +1622,7 @@ describe(`Query Collections`, () => { .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, - userName: persons.name, + userName: persons!.name, })) ) }) @@ -1633,7 +1633,7 @@ describe(`Query Collections`, () => { await new Promise((resolve) => setTimeout(resolve, 10)) - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) // Add a person first userSyncBegin!() @@ -1652,7 +1652,7 @@ describe(`Query Collections`, () => { await new Promise((resolve) => setTimeout(resolve, 10)) - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) expect(result.state.size).toBe(0) // No joins yet // Add an issue for that person @@ -1672,9 +1672,9 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(result.state.size).toBe(1) }) - expect(result.isLoading()).toBe(true) - expect(result.data).toHaveLength(1) - expect(result.data[0]).toMatchObject({ + expect(result.isLoading).toBe(true) + expect(result()).toHaveLength(1) + expect(result()[0]).toMatchObject({ id: `1`, title: `First Issue`, userName: `John Doe`, @@ -1685,7 +1685,7 @@ describe(`Query Collections`, () => { issueSyncMarkReady!() await waitFor(() => { - expect(result.isLoading()).toBe(false) + expect(result.isLoading).toBe(false) }) expect(result.state.size).toBe(1) }) @@ -1721,10 +1721,10 @@ describe(`Query Collections`, () => { }) // Initially isLoading should be true - expect(result.isLoading()).toBe(true) - expect(result.isReady()).toBe(false) + expect(result.isLoading).toBe(true) + expect(result.isReady).toBe(false) expect(result.state.size).toBe(0) - expect(result.data).toEqual([]) + expect(result()).toEqual([]) // Start sync manually collection.preload() @@ -1732,20 +1732,20 @@ describe(`Query Collections`, () => { await new Promise((resolve) => setTimeout(resolve, 10)) // Still loading - expect(result.isLoading()).toBe(true) - expect(result.isReady()).toBe(false) + expect(result.isLoading).toBe(true) + expect(result.isReady).toBe(false) // Mark ready without any data commits syncMarkReady!() // Should now be ready, even with no data await waitFor(() => { - expect(result.isReady()).toBe(true) + expect(result.isReady).toBe(true) }) - expect(result.isLoading()).toBe(false) + expect(result.isLoading).toBe(false) expect(result.state.size).toBe(0) // Still no data - expect(result.data).toEqual([]) // Empty array - expect(result.status()).toBe(`ready`) + expect(result()).toEqual([]) // Empty array + expect(result.status).toBe(`ready`) }) }) }) From ad24cd44d85d86dfe3bcf1497d3fba03ba9560f6 Mon Sep 17 00:00:00 2001 From: fezproof <26830309+fezproof@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:44:21 +0800 Subject: [PATCH 03/13] Fix typo --- packages/solid-db/src/useLiveQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index cb1f8aa4e..d532866e6 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -87,7 +87,7 @@ import type { * * {(todo) =>
  • {todo.text}
  • } *
    - *
    + *
    * ) */ // Overload 1: Accept just the query function From f423b66296b9e6aa3a4d369b99e57a2a6a8d4dba Mon Sep 17 00:00:00 2001 From: fezproof <26830309+fezproof@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:44:29 +0800 Subject: [PATCH 04/13] Add changeset --- .changeset/cute-falcons-wear.md | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .changeset/cute-falcons-wear.md diff --git a/.changeset/cute-falcons-wear.md b/.changeset/cute-falcons-wear.md new file mode 100644 index 000000000..8ddf3b96c --- /dev/null +++ b/.changeset/cute-falcons-wear.md @@ -0,0 +1,49 @@ +--- +"@tanstack/solid-db": minor +--- + +Update solid-db to enable suspense support. +You can now run do + +```tsx +// Use Suspense boundaries +const todosQuery = useLiveQuery((q) => q.from({ todos: todoCollection })) + +return ( + <> + {/* Status and other getters don't trigger Suspense */} +
    Status {todosQuery.status}
    +
    Loading {todosQuery.isLoading ? "yes" : "no"}
    + + Loading...}> + + {(todo) =>
  • {todo.text}
  • } +
    +
    + +) +``` + +All values returned from useLiveQuery are now getters, so no longer need to be called as functions. This is a breaking change. This is to match how createResource works, and everything still stays reactive. + +```tsx +const todos = useLiveQuery(() => existingCollection) + +const handleToggle = (id) => { + // Can now access collection directly + todos.collection.update(id, (draft) => { + draft.completed = !draft.completed + }) +} + +return ( + <> + {/* Status and other getters don't trigger Suspense */} +
    Status {todos.status}
    +
    Loading {todos.isLoading ? "yes" : "no"}
    +
    Ready {todos.isReady ? "yes" : "no"}
    +
    Idle {todos.isIdle ? "yes" : "no"}
    +
    Error {todos.isError ? "yes" : "no"}
    + +) +``` From 95e41555744540afa132a64ee57102ab2a4aae06 Mon Sep 17 00:00:00 2001 From: fezproof <26830309+fezproof@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:41:36 +0800 Subject: [PATCH 05/13] Some example cleanup --- packages/solid-db/src/useLiveQuery.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index d532866e6..1632bd637 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -24,7 +24,7 @@ import type { /** * Create a live query using a query function * @param queryFn - Query function that defines what data to fetch - * @returns Object with reactive data, state, and status information + * @returns Accessor that returns data with Suspense support, with state and status infomation as properties * @example * // Basic query with object syntax * const todosQuery = useLiveQuery((q) => @@ -76,7 +76,7 @@ import type { * * ) * - * * @example + * @example * // Use Suspense boundaries * const todosQuery = useLiveQuery((q) => * q.from({ todos: todoCollection }) @@ -107,7 +107,7 @@ export function useLiveQuery( /** * Create a live query using configuration object * @param config - Configuration object with query and options - * @returns Object with reactive data, state, and status information + * @returns Accessor that returns data with Suspense support, with state and status infomation as properties * @example * // Basic config object usage * const todosQuery = useLiveQuery(() => ({ @@ -161,7 +161,7 @@ export function useLiveQuery( /** * Subscribe to an existing live query collection * @param liveQueryCollection - Pre-created live query collection to subscribe to - * @returns Object with reactive data, state, and status information + * @returns Accessor that returns data with Suspense support, with state and status infomation as properties * @example * // Using pre-created live query collection * const myLiveQuery = createLiveQueryCollection((q) => From 5e4ef8016f57f84984e6f08d5141d78985fe9d45 Mon Sep 17 00:00:00 2001 From: fezproof <26830309+fezproof@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:32:29 +0800 Subject: [PATCH 06/13] Add data back and deprecate it for smoother migration --- packages/solid-db/src/useLiveQuery.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 1632bd637..f16b0c9bc 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -94,6 +94,11 @@ import type { export function useLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder ): Accessor>> & { + /** + * @deprecated use function result instead + * query.data -> query() + */ + data: Array> state: ReactiveMap> collection: Collection, string | number, {}> status: CollectionStatus @@ -148,6 +153,11 @@ export function useLiveQuery( export function useLiveQuery( config: Accessor> ): Accessor>> & { + /** + * @deprecated use function result instead + * query.data -> query() + */ + data: Array> state: ReactiveMap> collection: Collection, string | number, {}> status: CollectionStatus @@ -201,6 +211,11 @@ export function useLiveQuery< >( liveQueryCollection: Accessor> ): Accessor> & { + /** + * @deprecated use function result instead + * query.data -> query() + */ + data: Array state: ReactiveMap collection: Collection status: CollectionStatus @@ -325,6 +340,11 @@ export function useLiveQuery( } Object.defineProperties(getData, { + data: { + get() { + return getData() + }, + }, status: { get() { return status() From 3ffc8f428bfb18b0e5c3c17093aed572fc887469 Mon Sep 17 00:00:00 2001 From: fezproof <26830309+fezproof@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:43:34 +0800 Subject: [PATCH 07/13] Fix types --- packages/solid-db/src/useLiveQuery.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 738cf7832..4fa7d264c 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -96,7 +96,7 @@ import type { */ // Overload 1: Accept query function that always returns QueryBuilder export function useLiveQuery( - queryFn: (q: InitialQueryBuilder) => QueryBuilder + queryFn: (q: InitialQueryBuilder) => QueryBuilder, ): Accessor>> & { /** * @deprecated use function result instead @@ -118,7 +118,7 @@ export function useLiveQuery( queryFn: ( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, -): { +): Accessor>> & { state: ReactiveMap> data: Array> collection: Accessor( */ // Overload 2: Accept config object export function useLiveQuery( - config: Accessor> + config: Accessor>, ): Accessor>> & { /** * @deprecated use function result instead @@ -234,7 +234,7 @@ export function useLiveQuery< TKey extends string | number, TUtils extends Record, >( - liveQueryCollection: Accessor> + liveQueryCollection: Accessor>, ): Accessor> & { /** * @deprecated use function result instead @@ -322,6 +322,9 @@ export function useLiveQuery( const [getDataResource] = createResource( () => ({ currentCollection: collection() }), async ({ currentCollection }) => { + if (!currentCollection) { + return [] + } setStatus(currentCollection.status) await currentCollection.toArrayWhenReady() // Initialize state with current collection data @@ -339,11 +342,14 @@ export function useLiveQuery( name: `TanstackDBData`, deferStream: false, initialValue: data, - } + }, ) createEffect(() => { const currentCollection = collection() + if (!currentCollection) { + return + } const subscription = currentCollection.subscribeChanges( // Changes is fine grained, so does not work great with an array (changes: Array>) => { @@ -369,7 +375,7 @@ export function useLiveQuery( }, { includeInitialState: true, - } + }, ) onCleanup(() => { From 693d9a106209de92d78f1c4547deadf8d61d1466 Mon Sep 17 00:00:00 2001 From: fezproof <26830309+fezproof@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:43:58 +0800 Subject: [PATCH 08/13] Fix solid example to work --- examples/solid/todo/src/db/validation.ts | 34 ++-- examples/solid/todo/src/main.tsx | 11 -- examples/solid/todo/src/routeTree.gen.ts | 170 +++++++++--------- examples/solid/todo/src/router.tsx | 13 +- examples/solid/todo/src/routes/__root.tsx | 28 ++- .../solid/todo/src/routes/api/config.$id.ts | 168 ++++++++--------- examples/solid/todo/src/routes/api/config.ts | 82 +++++---- .../solid/todo/src/routes/api/todos.$id.ts | 168 ++++++++--------- examples/solid/todo/src/routes/api/todos.ts | 80 +++++---- examples/solid/todo/src/routes/electric.tsx | 21 ++- examples/solid/todo/src/routes/query.tsx | 21 ++- examples/solid/todo/src/server.ts | 7 + examples/solid/todo/src/start.tsx | 7 + examples/solid/todo/vite.config.ts | 8 +- 14 files changed, 422 insertions(+), 396 deletions(-) delete mode 100644 examples/solid/todo/src/main.tsx create mode 100644 examples/solid/todo/src/server.ts create mode 100644 examples/solid/todo/src/start.tsx diff --git a/examples/solid/todo/src/db/validation.ts b/examples/solid/todo/src/db/validation.ts index a3e71c7af..2fa8408af 100644 --- a/examples/solid/todo/src/db/validation.ts +++ b/examples/solid/todo/src/db/validation.ts @@ -1,34 +1,24 @@ import { createInsertSchema, createSelectSchema } from 'drizzle-zod' -import { z } from 'zod' import { config, todos } from './schema' +import type { z } from 'zod' -// Date transformation schema - handles Date objects, ISO strings, and parseable date strings -const dateStringToDate = z - .union([ - z.date(), // Already a Date object - z - .string() - .datetime() - .transform((str) => new Date(str)), // ISO datetime string - z.string().transform((str) => new Date(str)), // Any parseable date string - ]) - .optional() - -// Auto-generated schemas from Drizzle schema with date transformation -export const insertTodoSchema = createInsertSchema(todos, { - created_at: dateStringToDate, - updated_at: dateStringToDate, +// Auto-generated schemas from Drizzle schema (omit auto-generated fields) +export const insertTodoSchema = createInsertSchema(todos).omit({ + id: true, + created_at: true, + updated_at: true, }) export const selectTodoSchema = createSelectSchema(todos) // Partial schema for updates export const updateTodoSchema = insertTodoSchema.partial().strict() -// Config schemas with date transformation -export const insertConfigSchema = createInsertSchema(config, { - created_at: dateStringToDate, - updated_at: dateStringToDate, -}).strict() +// Config schemas (omit auto-generated fields) +export const insertConfigSchema = createInsertSchema(config).omit({ + id: true, + created_at: true, + updated_at: true, +}) export const selectConfigSchema = createSelectSchema(config) export const updateConfigSchema = insertConfigSchema.partial().strict() diff --git a/examples/solid/todo/src/main.tsx b/examples/solid/todo/src/main.tsx deleted file mode 100644 index 5384d2a20..000000000 --- a/examples/solid/todo/src/main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { RouterProvider } from '@tanstack/solid-router' -import { createRouter } from './router' -import './index.css' -import { render } from 'solid-js/web' - -const router = createRouter() - -render( - () => , - document.getElementById(`root`)!, -) diff --git a/examples/solid/todo/src/routeTree.gen.ts b/examples/solid/todo/src/routeTree.gen.ts index 149aec6dc..e567fc6d2 100644 --- a/examples/solid/todo/src/routeTree.gen.ts +++ b/examples/solid/todo/src/routeTree.gen.ts @@ -8,19 +8,15 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { createServerRootRoute } from '@tanstack/solid-start/server' - import { Route as rootRouteImport } from './routes/__root' import { Route as TrailbaseRouteImport } from './routes/trailbase' import { Route as QueryRouteImport } from './routes/query' import { Route as ElectricRouteImport } from './routes/electric' import { Route as IndexRouteImport } from './routes/index' -import { ServerRoute as ApiTodosServerRouteImport } from './routes/api/todos' -import { ServerRoute as ApiConfigServerRouteImport } from './routes/api/config' -import { ServerRoute as ApiTodosIdServerRouteImport } from './routes/api/todos.$id' -import { ServerRoute as ApiConfigIdServerRouteImport } from './routes/api/config.$id' - -const rootServerRouteImport = createServerRootRoute() +import { Route as ApiTodosRouteImport } from './routes/api/todos' +import { Route as ApiConfigRouteImport } from './routes/api/config' +import { Route as ApiTodosIdRouteImport } from './routes/api/todos.$id' +import { Route as ApiConfigIdRouteImport } from './routes/api/config.$id' const TrailbaseRoute = TrailbaseRouteImport.update({ id: '/trailbase', @@ -42,25 +38,25 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) -const ApiTodosServerRoute = ApiTodosServerRouteImport.update({ +const ApiTodosRoute = ApiTodosRouteImport.update({ id: '/api/todos', path: '/api/todos', - getParentRoute: () => rootServerRouteImport, + getParentRoute: () => rootRouteImport, } as any) -const ApiConfigServerRoute = ApiConfigServerRouteImport.update({ +const ApiConfigRoute = ApiConfigRouteImport.update({ id: '/api/config', path: '/api/config', - getParentRoute: () => rootServerRouteImport, + getParentRoute: () => rootRouteImport, } as any) -const ApiTodosIdServerRoute = ApiTodosIdServerRouteImport.update({ +const ApiTodosIdRoute = ApiTodosIdRouteImport.update({ id: '/$id', path: '/$id', - getParentRoute: () => ApiTodosServerRoute, + getParentRoute: () => ApiTodosRoute, } as any) -const ApiConfigIdServerRoute = ApiConfigIdServerRouteImport.update({ +const ApiConfigIdRoute = ApiConfigIdRouteImport.update({ id: '/$id', path: '/$id', - getParentRoute: () => ApiConfigServerRoute, + getParentRoute: () => ApiConfigRoute, } as any) export interface FileRoutesByFullPath { @@ -68,12 +64,20 @@ export interface FileRoutesByFullPath { '/electric': typeof ElectricRoute '/query': typeof QueryRoute '/trailbase': typeof TrailbaseRoute + '/api/config': typeof ApiConfigRouteWithChildren + '/api/todos': typeof ApiTodosRouteWithChildren + '/api/config/$id': typeof ApiConfigIdRoute + '/api/todos/$id': typeof ApiTodosIdRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/electric': typeof ElectricRoute '/query': typeof QueryRoute '/trailbase': typeof TrailbaseRoute + '/api/config': typeof ApiConfigRouteWithChildren + '/api/todos': typeof ApiTodosRouteWithChildren + '/api/config/$id': typeof ApiConfigIdRoute + '/api/todos/$id': typeof ApiTodosIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -81,56 +85,51 @@ export interface FileRoutesById { '/electric': typeof ElectricRoute '/query': typeof QueryRoute '/trailbase': typeof TrailbaseRoute + '/api/config': typeof ApiConfigRouteWithChildren + '/api/todos': typeof ApiTodosRouteWithChildren + '/api/config/$id': typeof ApiConfigIdRoute + '/api/todos/$id': typeof ApiTodosIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/electric' | '/query' | '/trailbase' + fullPaths: + | '/' + | '/electric' + | '/query' + | '/trailbase' + | '/api/config' + | '/api/todos' + | '/api/config/$id' + | '/api/todos/$id' fileRoutesByTo: FileRoutesByTo - to: '/' | '/electric' | '/query' | '/trailbase' - id: '__root__' | '/' | '/electric' | '/query' | '/trailbase' - fileRoutesById: FileRoutesById -} -export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - ElectricRoute: typeof ElectricRoute - QueryRoute: typeof QueryRoute - TrailbaseRoute: typeof TrailbaseRoute -} -export interface FileServerRoutesByFullPath { - '/api/config': typeof ApiConfigServerRouteWithChildren - '/api/todos': typeof ApiTodosServerRouteWithChildren - '/api/config/$id': typeof ApiConfigIdServerRoute - '/api/todos/$id': typeof ApiTodosIdServerRoute -} -export interface FileServerRoutesByTo { - '/api/config': typeof ApiConfigServerRouteWithChildren - '/api/todos': typeof ApiTodosServerRouteWithChildren - '/api/config/$id': typeof ApiConfigIdServerRoute - '/api/todos/$id': typeof ApiTodosIdServerRoute -} -export interface FileServerRoutesById { - __root__: typeof rootServerRouteImport - '/api/config': typeof ApiConfigServerRouteWithChildren - '/api/todos': typeof ApiTodosServerRouteWithChildren - '/api/config/$id': typeof ApiConfigIdServerRoute - '/api/todos/$id': typeof ApiTodosIdServerRoute -} -export interface FileServerRouteTypes { - fileServerRoutesByFullPath: FileServerRoutesByFullPath - fullPaths: '/api/config' | '/api/todos' | '/api/config/$id' | '/api/todos/$id' - fileServerRoutesByTo: FileServerRoutesByTo - to: '/api/config' | '/api/todos' | '/api/config/$id' | '/api/todos/$id' + to: + | '/' + | '/electric' + | '/query' + | '/trailbase' + | '/api/config' + | '/api/todos' + | '/api/config/$id' + | '/api/todos/$id' id: | '__root__' + | '/' + | '/electric' + | '/query' + | '/trailbase' | '/api/config' | '/api/todos' | '/api/config/$id' | '/api/todos/$id' - fileServerRoutesById: FileServerRoutesById + fileRoutesById: FileRoutesById } -export interface RootServerRouteChildren { - ApiConfigServerRoute: typeof ApiConfigServerRouteWithChildren - ApiTodosServerRoute: typeof ApiTodosServerRouteWithChildren +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ElectricRoute: typeof ElectricRoute + QueryRoute: typeof QueryRoute + TrailbaseRoute: typeof TrailbaseRoute + ApiConfigRoute: typeof ApiConfigRouteWithChildren + ApiTodosRoute: typeof ApiTodosRouteWithChildren } declare module '@tanstack/solid-router' { @@ -163,63 +162,59 @@ declare module '@tanstack/solid-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } - } -} -declare module '@tanstack/solid-start/server' { - interface ServerFileRoutesByPath { '/api/todos': { id: '/api/todos' path: '/api/todos' fullPath: '/api/todos' - preLoaderRoute: typeof ApiTodosServerRouteImport - parentRoute: typeof rootServerRouteImport + preLoaderRoute: typeof ApiTodosRouteImport + parentRoute: typeof rootRouteImport } '/api/config': { id: '/api/config' path: '/api/config' fullPath: '/api/config' - preLoaderRoute: typeof ApiConfigServerRouteImport - parentRoute: typeof rootServerRouteImport + preLoaderRoute: typeof ApiConfigRouteImport + parentRoute: typeof rootRouteImport } '/api/todos/$id': { id: '/api/todos/$id' path: '/$id' fullPath: '/api/todos/$id' - preLoaderRoute: typeof ApiTodosIdServerRouteImport - parentRoute: typeof ApiTodosServerRoute + preLoaderRoute: typeof ApiTodosIdRouteImport + parentRoute: typeof ApiTodosRoute } '/api/config/$id': { id: '/api/config/$id' path: '/$id' fullPath: '/api/config/$id' - preLoaderRoute: typeof ApiConfigIdServerRouteImport - parentRoute: typeof ApiConfigServerRoute + preLoaderRoute: typeof ApiConfigIdRouteImport + parentRoute: typeof ApiConfigRoute } } } -interface ApiConfigServerRouteChildren { - ApiConfigIdServerRoute: typeof ApiConfigIdServerRoute +interface ApiConfigRouteChildren { + ApiConfigIdRoute: typeof ApiConfigIdRoute } -const ApiConfigServerRouteChildren: ApiConfigServerRouteChildren = { - ApiConfigIdServerRoute: ApiConfigIdServerRoute, +const ApiConfigRouteChildren: ApiConfigRouteChildren = { + ApiConfigIdRoute: ApiConfigIdRoute, } -const ApiConfigServerRouteWithChildren = ApiConfigServerRoute._addFileChildren( - ApiConfigServerRouteChildren, +const ApiConfigRouteWithChildren = ApiConfigRoute._addFileChildren( + ApiConfigRouteChildren, ) -interface ApiTodosServerRouteChildren { - ApiTodosIdServerRoute: typeof ApiTodosIdServerRoute +interface ApiTodosRouteChildren { + ApiTodosIdRoute: typeof ApiTodosIdRoute } -const ApiTodosServerRouteChildren: ApiTodosServerRouteChildren = { - ApiTodosIdServerRoute: ApiTodosIdServerRoute, +const ApiTodosRouteChildren: ApiTodosRouteChildren = { + ApiTodosIdRoute: ApiTodosIdRoute, } -const ApiTodosServerRouteWithChildren = ApiTodosServerRoute._addFileChildren( - ApiTodosServerRouteChildren, +const ApiTodosRouteWithChildren = ApiTodosRoute._addFileChildren( + ApiTodosRouteChildren, ) const rootRouteChildren: RootRouteChildren = { @@ -227,14 +222,19 @@ const rootRouteChildren: RootRouteChildren = { ElectricRoute: ElectricRoute, QueryRoute: QueryRoute, TrailbaseRoute: TrailbaseRoute, + ApiConfigRoute: ApiConfigRouteWithChildren, + ApiTodosRoute: ApiTodosRouteWithChildren, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() -const rootServerRouteChildren: RootServerRouteChildren = { - ApiConfigServerRoute: ApiConfigServerRouteWithChildren, - ApiTodosServerRoute: ApiTodosServerRouteWithChildren, + +import type { getRouter } from './router.tsx' +import type { startInstance } from './start.tsx' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + config: Awaited> + } } -export const serverRouteTree = rootServerRouteImport - ._addFileChildren(rootServerRouteChildren) - ._addFileTypes() diff --git a/examples/solid/todo/src/router.tsx b/examples/solid/todo/src/router.tsx index 7d743ad93..35abef268 100644 --- a/examples/solid/todo/src/router.tsx +++ b/examples/solid/todo/src/router.tsx @@ -7,20 +7,11 @@ import { NotFound } from './components/NotFound' import './styles.css' // Create a new router instance -export const createRouter = () => { - const router = createTanstackRouter({ +export function getRouter() { + return createTanstackRouter({ routeTree, scrollRestoration: true, defaultPreloadStaleTime: 0, defaultNotFoundComponent: NotFound, }) - - return router -} - -// Register the router instance for type safety -declare module '@tanstack/solid-router' { - interface Register { - router: ReturnType - } } diff --git a/examples/solid/todo/src/routes/__root.tsx b/examples/solid/todo/src/routes/__root.tsx index cf695549c..98998cda2 100644 --- a/examples/solid/todo/src/routes/__root.tsx +++ b/examples/solid/todo/src/routes/__root.tsx @@ -1,6 +1,13 @@ -import { Outlet, createRootRoute } from '@tanstack/solid-router' - +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from 'solid-js/web' import appCss from '../styles.css?url' +import type { ParentProps } from 'solid-js' + export const Route = createRootRoute({ head: () => ({ @@ -23,6 +30,21 @@ export const Route = createRootRoute({ }, ], }), - + shellComponent: RootDocument, component: () => , }) + +function RootDocument(props: ParentProps) { + return ( + + + + + + + {props.children} + + + + ) +} diff --git a/examples/solid/todo/src/routes/api/config.$id.ts b/examples/solid/todo/src/routes/api/config.$id.ts index e4a751091..160aee8b1 100644 --- a/examples/solid/todo/src/routes/api/config.$id.ts +++ b/examples/solid/todo/src/routes/api/config.$id.ts @@ -1,4 +1,4 @@ -import { createServerFileRoute } from '@tanstack/solid-start/server' +import { createFileRoute } from '@tanstack/solid-router' import { json } from '@tanstack/solid-start' import { sql } from '../../db/postgres' import { validateUpdateConfig } from '../../db/validation' @@ -16,101 +16,105 @@ async function generateTxId(tx: any): Promise { return parseInt(txid, 10) } -export const ServerRoute = createServerFileRoute(`/api/config/$id`).methods({ - GET: async ({ params }) => { - try { - const { id } = params - const [config] = await sql`SELECT * FROM config WHERE id = ${id}` - - if (!config) { - return json({ error: `Config not found` }, { status: 404 }) - } - - return json(config) - } catch (error) { - console.error(`Error fetching config:`, error) - return json( - { - error: `Failed to fetch config`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } - }, - PUT: async ({ params, request }) => { - try { - const { id } = params - const body = await request.json() - const configData = validateUpdateConfig(body) - - let txid!: Txid - const updatedConfig = await sql.begin(async (tx) => { - txid = await generateTxId(tx) - - const [result] = await tx` +export const Route = createFileRoute(`/api/config/$id`)({ + server: { + handlers: { + GET: async ({ params }) => { + try { + const { id } = params + const [config] = await sql`SELECT * FROM config WHERE id = ${id}` + + if (!config) { + return json({ error: `Config not found` }, { status: 404 }) + } + + return json(config) + } catch (error) { + console.error(`Error fetching config:`, error) + return json( + { + error: `Failed to fetch config`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) + } + }, + PUT: async ({ params, request }) => { + try { + const { id } = params + const body = await request.json() + const configData = validateUpdateConfig(body) + + let txid!: Txid + const updatedConfig = await sql.begin(async (tx) => { + txid = await generateTxId(tx) + + const [result] = await tx` UPDATE config SET ${tx(configData)} WHERE id = ${id} RETURNING * ` - if (!result) { - throw new Error(`Config not found`) + if (!result) { + throw new Error(`Config not found`) + } + + return result + }) + + return json({ config: updatedConfig, txid }) + } catch (error) { + if (error instanceof Error && error.message === `Config not found`) { + return json({ error: `Config not found` }, { status: 404 }) + } + + console.error(`Error updating config:`, error) + return json( + { + error: `Failed to update config`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) } + }, + DELETE: async ({ params }) => { + try { + const { id } = params - return result - }) - - return json({ config: updatedConfig, txid }) - } catch (error) { - if (error instanceof Error && error.message === `Config not found`) { - return json({ error: `Config not found` }, { status: 404 }) - } - - console.error(`Error updating config:`, error) - return json( - { - error: `Failed to update config`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } - }, - DELETE: async ({ params }) => { - try { - const { id } = params - - let txid!: Txid - await sql.begin(async (tx) => { - txid = await generateTxId(tx) + let txid!: Txid + await sql.begin(async (tx) => { + txid = await generateTxId(tx) - const [result] = await tx` + const [result] = await tx` DELETE FROM config WHERE id = ${id} RETURNING id ` - if (!result) { - throw new Error(`Config not found`) + if (!result) { + throw new Error(`Config not found`) + } + }) + + return json({ success: true, txid }) + } catch (error) { + if (error instanceof Error && error.message === `Config not found`) { + return json({ error: `Config not found` }, { status: 404 }) + } + + console.error(`Error deleting config:`, error) + return json( + { + error: `Failed to delete config`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) } - }) - - return json({ success: true, txid }) - } catch (error) { - if (error instanceof Error && error.message === `Config not found`) { - return json({ error: `Config not found` }, { status: 404 }) - } - - console.error(`Error deleting config:`, error) - return json( - { - error: `Failed to delete config`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } + }, + }, }, }) diff --git a/examples/solid/todo/src/routes/api/config.ts b/examples/solid/todo/src/routes/api/config.ts index 7d25c555b..cd0cd5657 100644 --- a/examples/solid/todo/src/routes/api/config.ts +++ b/examples/solid/todo/src/routes/api/config.ts @@ -1,4 +1,4 @@ -import { createServerFileRoute } from '@tanstack/solid-start/server' +import { createFileRoute } from '@tanstack/solid-router' import { json } from '@tanstack/solid-start' import { sql } from '../../db/postgres' import { validateInsertConfig } from '../../db/validation' @@ -16,49 +16,53 @@ async function generateTxId(tx: any): Promise { return parseInt(txid, 10) } -export const ServerRoute = createServerFileRoute(`/api/config`).methods({ - GET: async ({ request: _request }) => { - try { - const config = await sql`SELECT * FROM config` - return json(config) - } catch (error) { - console.error(`Error fetching config:`, error) - return json( - { - error: `Failed to fetch config`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } - }, - POST: async ({ request }) => { - try { - const body = await request.json() - console.log(`POST /api/config`, body) - const configData = validateInsertConfig(body) +export const Route = createFileRoute(`/api/config`)({ + server: { + handlers: { + GET: async ({ request: _request }) => { + try { + const config = await sql`SELECT * FROM config` + return json(config) + } catch (error) { + console.error(`Error fetching config:`, error) + return json( + { + error: `Failed to fetch config`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) + } + }, + POST: async ({ request }) => { + try { + const body = await request.json() + console.log(`POST /api/config`, body) + const configData = validateInsertConfig(body) - let txid!: Txid - const newConfig = await sql.begin(async (tx) => { - txid = await generateTxId(tx) + let txid!: Txid + const newConfig = await sql.begin(async (tx) => { + txid = await generateTxId(tx) - const [result] = await tx` + const [result] = await tx` INSERT INTO config ${tx(configData)} RETURNING * ` - return result - }) + return result + }) - return json({ config: newConfig, txid }, { status: 201 }) - } catch (error) { - console.error(`Error creating config:`, error) - return json( - { - error: `Failed to create config`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } + return json({ config: newConfig, txid }, { status: 201 }) + } catch (error) { + console.error(`Error creating config:`, error) + return json( + { + error: `Failed to create config`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) + } + }, + }, }, }) diff --git a/examples/solid/todo/src/routes/api/todos.$id.ts b/examples/solid/todo/src/routes/api/todos.$id.ts index 32d6ce9f4..3b2b742cd 100644 --- a/examples/solid/todo/src/routes/api/todos.$id.ts +++ b/examples/solid/todo/src/routes/api/todos.$id.ts @@ -1,4 +1,4 @@ -import { createServerFileRoute } from '@tanstack/solid-start/server' +import { createFileRoute } from '@tanstack/solid-router' import { json } from '@tanstack/solid-start' import { sql } from '../../db/postgres' import { validateUpdateTodo } from '../../db/validation' @@ -16,101 +16,105 @@ async function generateTxId(tx: any): Promise { return parseInt(txid, 10) } -export const ServerRoute = createServerFileRoute(`/api/todos/$id`).methods({ - GET: async ({ params }) => { - try { - const { id } = params - const [todo] = await sql`SELECT * FROM todos WHERE id = ${id}` - - if (!todo) { - return json({ error: `Todo not found` }, { status: 404 }) - } - - return json(todo) - } catch (error) { - console.error(`Error fetching todo:`, error) - return json( - { - error: `Failed to fetch todo`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } - }, - PUT: async ({ params, request }) => { - try { - const { id } = params - const body = await request.json() - const todoData = validateUpdateTodo(body) - - let txid!: Txid - const updatedTodo = await sql.begin(async (tx) => { - txid = await generateTxId(tx) - - const [result] = await tx` +export const Route = createFileRoute(`/api/todos/$id`)({ + server: { + handlers: { + GET: async ({ params }) => { + try { + const { id } = params + const [todo] = await sql`SELECT * FROM todos WHERE id = ${id}` + + if (!todo) { + return json({ error: `Todo not found` }, { status: 404 }) + } + + return json(todo) + } catch (error) { + console.error(`Error fetching todo:`, error) + return json( + { + error: `Failed to fetch todo`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) + } + }, + PUT: async ({ params, request }) => { + try { + const { id } = params + const body = await request.json() + const todoData = validateUpdateTodo(body) + + let txid!: Txid + const updatedTodo = await sql.begin(async (tx) => { + txid = await generateTxId(tx) + + const [result] = await tx` UPDATE todos SET ${tx(todoData)} WHERE id = ${id} RETURNING * ` - if (!result) { - throw new Error(`Todo not found`) + if (!result) { + throw new Error(`Todo not found`) + } + + return result + }) + + return json({ todo: updatedTodo, txid }) + } catch (error) { + if (error instanceof Error && error.message === `Todo not found`) { + return json({ error: `Todo not found` }, { status: 404 }) + } + + console.error(`Error updating todo:`, error) + return json( + { + error: `Failed to update todo`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) } + }, + DELETE: async ({ params }) => { + try { + const { id } = params - return result - }) - - return json({ todo: updatedTodo, txid }) - } catch (error) { - if (error instanceof Error && error.message === `Todo not found`) { - return json({ error: `Todo not found` }, { status: 404 }) - } - - console.error(`Error updating todo:`, error) - return json( - { - error: `Failed to update todo`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } - }, - DELETE: async ({ params }) => { - try { - const { id } = params - - let txid!: Txid - await sql.begin(async (tx) => { - txid = await generateTxId(tx) + let txid!: Txid + await sql.begin(async (tx) => { + txid = await generateTxId(tx) - const [result] = await tx` + const [result] = await tx` DELETE FROM todos WHERE id = ${id} RETURNING id ` - if (!result) { - throw new Error(`Todo not found`) + if (!result) { + throw new Error(`Todo not found`) + } + }) + + return json({ success: true, txid }) + } catch (error) { + if (error instanceof Error && error.message === `Todo not found`) { + return json({ error: `Todo not found` }, { status: 404 }) + } + + console.error(`Error deleting todo:`, error) + return json( + { + error: `Failed to delete todo`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) } - }) - - return json({ success: true, txid }) - } catch (error) { - if (error instanceof Error && error.message === `Todo not found`) { - return json({ error: `Todo not found` }, { status: 404 }) - } - - console.error(`Error deleting todo:`, error) - return json( - { - error: `Failed to delete todo`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } + }, + }, }, }) diff --git a/examples/solid/todo/src/routes/api/todos.ts b/examples/solid/todo/src/routes/api/todos.ts index 931d5996b..b28c649c1 100644 --- a/examples/solid/todo/src/routes/api/todos.ts +++ b/examples/solid/todo/src/routes/api/todos.ts @@ -1,4 +1,4 @@ -import { createServerFileRoute } from '@tanstack/solid-start/server' +import { createFileRoute } from '@tanstack/solid-router' import { json } from '@tanstack/solid-start' import { sql } from '../../db/postgres' import { validateInsertTodo } from '../../db/validation' @@ -20,48 +20,52 @@ async function generateTxId(tx: any): Promise { return parseInt(txid, 10) } -export const ServerRoute = createServerFileRoute(`/api/todos`).methods({ - GET: async ({ request: _request }) => { - try { - const todos = await sql`SELECT * FROM todos` - return json(todos) - } catch (error) { - console.error(`Error fetching todos:`, error) - return json( - { - error: `Failed to fetch todos`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } - }, - POST: async ({ request }) => { - try { - const body = await request.json() - const todoData = validateInsertTodo(body) +export const Route = createFileRoute(`/api/todos`)({ + server: { + handlers: { + GET: async ({ request: _request }) => { + try { + const todos = await sql`SELECT * FROM todos` + return json(todos) + } catch (error) { + console.error(`Error fetching todos:`, error) + return json( + { + error: `Failed to fetch todos`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) + } + }, + POST: async ({ request }) => { + try { + const body = await request.json() + const todoData = validateInsertTodo(body) - let txid!: Txid - const newTodo = await sql.begin(async (tx) => { - txid = await generateTxId(tx) + let txid!: Txid + const newTodo = await sql.begin(async (tx) => { + txid = await generateTxId(tx) - const [result] = await tx` + const [result] = await tx` INSERT INTO todos ${tx(todoData)} RETURNING * ` - return result - }) + return result + }) - return json({ todo: newTodo, txid }, { status: 201 }) - } catch (error) { - console.error(`Error creating todo:`, error) - return json( - { - error: `Failed to create todo`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } + return json({ todo: newTodo, txid }, { status: 201 }) + } catch (error) { + console.error(`Error creating todo:`, error) + return json( + { + error: `Failed to create todo`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) + } + }, + }, }, }) diff --git a/examples/solid/todo/src/routes/electric.tsx b/examples/solid/todo/src/routes/electric.tsx index 2bf391cfe..63442e719 100644 --- a/examples/solid/todo/src/routes/electric.tsx +++ b/examples/solid/todo/src/routes/electric.tsx @@ -1,5 +1,6 @@ import { createFileRoute } from '@tanstack/solid-router' import { useLiveQuery } from '@tanstack/solid-db' +import { Suspense } from 'solid-js' import { electricConfigCollection, electricTodoCollection, @@ -21,23 +22,25 @@ export const Route = createFileRoute(`/electric`)({ function ElectricPage() { // Get data using live queries with Electric collections - const { data: todos } = useLiveQuery((q) => + const todos = useLiveQuery((q) => q .from({ todo: electricTodoCollection }) .orderBy(({ todo }) => todo.created_at, `asc`), ) - const { data: configData } = useLiveQuery((q) => + const configData = useLiveQuery((q) => q.from({ config: electricConfigCollection }), ) return ( - + + + ) } diff --git a/examples/solid/todo/src/routes/query.tsx b/examples/solid/todo/src/routes/query.tsx index 11448cffd..5523668ac 100644 --- a/examples/solid/todo/src/routes/query.tsx +++ b/examples/solid/todo/src/routes/query.tsx @@ -1,5 +1,6 @@ import { createFileRoute } from '@tanstack/solid-router' import { useLiveQuery } from '@tanstack/solid-db' +import { Suspense } from 'solid-js' import { queryConfigCollection, queryTodoCollection } from '../lib/collections' import { TodoApp } from '../components/TodoApp' @@ -18,23 +19,25 @@ export const Route = createFileRoute(`/query`)({ function QueryPage() { // Get data using live queries with Query collections - const { data: todos } = useLiveQuery((q) => + const todos = useLiveQuery((q) => q .from({ todo: queryTodoCollection }) .orderBy(({ todo }) => todo.created_at, `asc`), ) - const { data: configData } = useLiveQuery((q) => + const configData = useLiveQuery((q) => q.from({ config: queryConfigCollection }), ) return ( - + + + ) } diff --git a/examples/solid/todo/src/server.ts b/examples/solid/todo/src/server.ts new file mode 100644 index 000000000..3682c04c2 --- /dev/null +++ b/examples/solid/todo/src/server.ts @@ -0,0 +1,7 @@ +import handler from '@tanstack/solid-start/server-entry' + +export default { + fetch(request: Request) { + return handler.fetch(request) + }, +} diff --git a/examples/solid/todo/src/start.tsx b/examples/solid/todo/src/start.tsx new file mode 100644 index 000000000..9c70dc5c4 --- /dev/null +++ b/examples/solid/todo/src/start.tsx @@ -0,0 +1,7 @@ +import { createStart } from '@tanstack/solid-start' + +export const startInstance = createStart(() => { + return { + defaultSsr: false, + } +}) diff --git a/examples/solid/todo/vite.config.ts b/examples/solid/todo/vite.config.ts index f66ba076c..620fb5e6d 100644 --- a/examples/solid/todo/vite.config.ts +++ b/examples/solid/todo/vite.config.ts @@ -13,11 +13,9 @@ export default defineConfig({ }), tailwindcss(), tanstackStart({ - customViteSolidPlugin: true, - spa: { - prerender: { enabled: false }, - enabled: true, - }, + srcDirectory: `src`, + start: { entry: `./start.tsx` }, + server: { entry: `./server.ts` }, }), solid({ ssr: true }), ], From af2eab3b4d01501af27f271461ffa796ca03c732 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 19:02:05 +0000 Subject: [PATCH 09/13] ci: apply automated fixes --- .changeset/cute-falcons-wear.md | 12 ++++++------ examples/solid/todo/src/routes/__root.tsx | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.changeset/cute-falcons-wear.md b/.changeset/cute-falcons-wear.md index 8ddf3b96c..85447c9dc 100644 --- a/.changeset/cute-falcons-wear.md +++ b/.changeset/cute-falcons-wear.md @@ -1,5 +1,5 @@ --- -"@tanstack/solid-db": minor +'@tanstack/solid-db': minor --- Update solid-db to enable suspense support. @@ -13,7 +13,7 @@ return ( <> {/* Status and other getters don't trigger Suspense */}
    Status {todosQuery.status}
    -
    Loading {todosQuery.isLoading ? "yes" : "no"}
    +
    Loading {todosQuery.isLoading ? 'yes' : 'no'}
    Loading...}> @@ -40,10 +40,10 @@ return ( <> {/* Status and other getters don't trigger Suspense */}
    Status {todos.status}
    -
    Loading {todos.isLoading ? "yes" : "no"}
    -
    Ready {todos.isReady ? "yes" : "no"}
    -
    Idle {todos.isIdle ? "yes" : "no"}
    -
    Error {todos.isError ? "yes" : "no"}
    +
    Loading {todos.isLoading ? 'yes' : 'no'}
    +
    Ready {todos.isReady ? 'yes' : 'no'}
    +
    Idle {todos.isIdle ? 'yes' : 'no'}
    +
    Error {todos.isError ? 'yes' : 'no'}
    ) ``` diff --git a/examples/solid/todo/src/routes/__root.tsx b/examples/solid/todo/src/routes/__root.tsx index 98998cda2..dde228f17 100644 --- a/examples/solid/todo/src/routes/__root.tsx +++ b/examples/solid/todo/src/routes/__root.tsx @@ -8,7 +8,6 @@ import { HydrationScript } from 'solid-js/web' import appCss from '../styles.css?url' import type { ParentProps } from 'solid-js' - export const Route = createRootRoute({ head: () => ({ meta: [ From c466d71e547e656845369a5ba21a7d9f500fd9e9 Mon Sep 17 00:00:00 2001 From: fezproof <26830309+fezproof@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:35:09 +0800 Subject: [PATCH 10/13] Fix tests --- packages/solid-db/src/useLiveQuery.ts | 5 ++- packages/solid-db/tests/useLiveQuery.test.tsx | 32 +++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 4fa7d264c..38479eb67 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -348,6 +348,9 @@ export function useLiveQuery( createEffect(() => { const currentCollection = collection() if (!currentCollection) { + setStatus(`disabled` as const) + state.clear() + setData([]) return } const subscription = currentCollection.subscribeChanges( @@ -416,7 +419,7 @@ export function useLiveQuery( }, isReady: { get() { - return status() === `ready` + return status() === `ready` || status() === `disabled` }, }, isIdle: { diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index 4b4641b9c..eabaf987a 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -286,7 +286,7 @@ describe(`Query Collections`, () => { .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, - name: persons.name, + name: persons?.name, })), ) }) @@ -651,7 +651,7 @@ describe(`Query Collections`, () => { .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, - name: persons.name, + name: persons?.name, })), ) @@ -1225,7 +1225,7 @@ describe(`Query Collections`, () => { .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, - name: persons.name, + name: persons?.name, })), ) }) @@ -1623,7 +1623,7 @@ describe(`Query Collections`, () => { .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, - userName: persons.name, + userName: persons?.name, })), ) }) @@ -1782,10 +1782,10 @@ describe(`Query Collections`, () => { // When callback returns undefined, should return disabled state expect(rendered.result.state.size).toBe(0) expect(rendered.result.data).toEqual([]) - expect(rendered.result.collection()).toBeNull() - expect(rendered.result.status()).toBe(`disabled`) - expect(rendered.result.isLoading()).toBe(false) - expect(rendered.result.isReady()).toBe(true) + expect(rendered.result.collection).toBeNull() + expect(rendered.result.status).toBe(`disabled`) + expect(rendered.result.isLoading).toBe(false) + expect(rendered.result.isReady).toBe(true) // Enable the query setEnabled(true) @@ -1795,14 +1795,14 @@ describe(`Query Collections`, () => { expect(rendered.result.state.size).toBe(1) // Only John Smith (age 35) }) expect(rendered.result.data).toHaveLength(1) - expect(rendered.result.isReady()).toBe(true) + expect(rendered.result.isReady).toBe(true) // Disable the query again setEnabled(false) await new Promise((resolve) => setTimeout(resolve, 10)) - expect(rendered.result.status()).toBe(`disabled`) - expect(rendered.result.isReady()).toBe(true) + expect(rendered.result.status).toBe(`disabled`) + expect(rendered.result.isReady).toBe(true) dispose() }) @@ -1839,10 +1839,10 @@ describe(`Query Collections`, () => { // When callback returns null, should return disabled state expect(rendered.result.state.size).toBe(0) expect(rendered.result.data).toEqual([]) - expect(rendered.result.collection()).toBeNull() - expect(rendered.result.status()).toBe(`disabled`) - expect(rendered.result.isLoading()).toBe(false) - expect(rendered.result.isReady()).toBe(true) + expect(rendered.result.collection).toBeNull() + expect(rendered.result.status).toBe(`disabled`) + expect(rendered.result.isLoading).toBe(false) + expect(rendered.result.isReady).toBe(true) // Enable the query setEnabled(true) @@ -1852,7 +1852,7 @@ describe(`Query Collections`, () => { expect(rendered.result.state.size).toBe(1) }) expect(rendered.result.data).toHaveLength(1) - expect(rendered.result.isReady()).toBe(true) + expect(rendered.result.isReady).toBe(true) dispose() }) From 05b02d13219c648d8ea5882da771e82356b4950e Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 13 Jan 2026 11:34:27 -0700 Subject: [PATCH 11/13] Fix typo and type inconsistency in useLiveQuery - Fix "infomation" typo in JSDoc comments - Fix Overload 1b types to match other overloads (remove unnecessary Accessor wrappers) - Add missing @deprecated JSDoc for data property in Overload 1b Co-Authored-By: Claude Opus 4.5 --- packages/solid-db/src/useLiveQuery.ts | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 38479eb67..5ed0747e0 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -28,7 +28,7 @@ import type { /** * Create a live query using a query function * @param queryFn - Query function that defines what data to fetch - * @returns Accessor that returns data with Suspense support, with state and status infomation as properties + * @returns Accessor that returns data with Suspense support, with state and status information as properties * @example * // Basic query with object syntax * const todosQuery = useLiveQuery((q) => @@ -119,25 +119,25 @@ export function useLiveQuery( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, ): Accessor>> & { - state: ReactiveMap> + /** + * @deprecated use function result instead + * query.data -> query() + */ data: Array> - collection: Accessor, - string | number, - {} - > | null> - status: Accessor - isLoading: Accessor - isReady: Accessor - isIdle: Accessor - isError: Accessor - isCleanedUp: Accessor + state: ReactiveMap> + collection: Collection, string | number, {}> | null + status: CollectionStatus | `disabled` + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean } /** * Create a live query using configuration object * @param config - Configuration object with query and options - * @returns Accessor that returns data with Suspense support, with state and status infomation as properties + * @returns Accessor that returns data with Suspense support, with state and status information as properties * @example * // Basic config object usage * const todosQuery = useLiveQuery(() => ({ @@ -196,7 +196,7 @@ export function useLiveQuery( /** * Subscribe to an existing live query collection * @param liveQueryCollection - Pre-created live query collection to subscribe to - * @returns Accessor that returns data with Suspense support, with state and status infomation as properties + * @returns Accessor that returns data with Suspense support, with state and status information as properties * @example * // Using pre-created live query collection * const myLiveQuery = createLiveQueryCollection((q) => From 10adaa40cf9526f964906ae30874fd1266c0c4e2 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 13 Jan 2026 11:44:09 -0700 Subject: [PATCH 12/13] Add error handling and Suspense integration tests - Add try-catch around toArrayWhenReady() to handle collection errors - Set status to 'error' before re-throwing for proper error state - Fix JSDoc example: status() -> status (getter, not function) - Add comment clarifying includeInitialState purpose - Add 2 Suspense boundary integration tests Co-Authored-By: Claude Opus 4.5 --- packages/solid-db/src/useLiveQuery.ts | 11 +- packages/solid-db/tests/useLiveQuery.test.tsx | 105 +++++++++++++++++- 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 5ed0747e0..7a5ba9595 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -70,7 +70,7 @@ import type { *
    Loading...
    * * - *
    Error: {todosQuery.status()}
    + *
    Error: {todosQuery.status}
    *
    * * @@ -326,7 +326,12 @@ export function useLiveQuery( return [] } setStatus(currentCollection.status) - await currentCollection.toArrayWhenReady() + try { + await currentCollection.toArrayWhenReady() + } catch (error) { + setStatus(`error`) + throw error + } // Initialize state with current collection data batch(() => { state.clear() @@ -354,7 +359,6 @@ export function useLiveQuery( return } const subscription = currentCollection.subscribeChanges( - // Changes is fine grained, so does not work great with an array (changes: Array>) => { // Apply each change individually to the reactive state batch(() => { @@ -377,6 +381,7 @@ export function useLiveQuery( }) }, { + // Include initial state to ensure immediate population for pre-created collections includeInitialState: true, }, ) diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index eabaf987a..90bd586a1 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { renderHook, waitFor } from '@solidjs/testing-library' +import { render, renderHook, waitFor } from '@solidjs/testing-library' import { Query, count, @@ -9,7 +9,13 @@ import { eq, gt, } from '@tanstack/db' -import { createComputed, createRoot, createSignal } from 'solid-js' +import { + For, + Suspense, + createComputed, + createRoot, + createSignal, +} from 'solid-js' import { useLiveQuery } from '../src/useLiveQuery' import { mockSyncCollectionOptions } from '../../db/tests/utils' import type { Accessor } from 'solid-js' @@ -1858,4 +1864,99 @@ describe(`Query Collections`, () => { }) }) }) + + describe(`Suspense Integration`, () => { + it(`should work with Suspense boundaries`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-suspense`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + function TestComponent() { + const query = useLiveQuery((q) => + q + .from({ persons: collection }) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })), + ) + + return ( +
      + + {(person) =>
    • {person.name}
    • } +
      +
    + ) + } + + const { findByTestId } = render(() => ( + Loading...}> + + + )) + + // Should eventually show the list with data + await waitFor(async () => { + const list = await findByTestId(`list`) + expect(list).toBeTruthy() + }) + + // Verify data is rendered + const person1 = await findByTestId(`person-1`) + expect(person1.textContent).toBe(`John Doe`) + + const person2 = await findByTestId(`person-2`) + expect(person2.textContent).toBe(`Jane Doe`) + + const person3 = await findByTestId(`person-3`) + expect(person3.textContent).toBe(`John Smith`) + }) + + it(`should show fallback during loading and data after ready`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-suspense-fallback`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + function TestComponent() { + const query = useLiveQuery((q) => + q + .from({ persons: collection }) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })), + ) + + return ( +
    + {query().length} +
    + ) + } + + const { findByTestId } = render(() => ( + Loading...}> + + + )) + + // Should eventually resolve and show data + await waitFor(async () => { + const content = await findByTestId(`content`) + expect(content).toBeTruthy() + }) + + const count = await findByTestId(`count`) + expect(count.textContent).toBe(`3`) + }) + }) }) From 5ed5e6860b806776eaa5d4d676ae70fe5c63e094 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:54:37 +0000 Subject: [PATCH 13/13] ci: apply automated fixes --- packages/solid-db/tests/useLiveQuery.test.tsx | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index 90bd586a1..8f9ef6ee2 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -1877,18 +1877,18 @@ describe(`Query Collections`, () => { function TestComponent() { const query = useLiveQuery((q) => - q - .from({ persons: collection }) - .select(({ persons }) => ({ - id: persons.id, - name: persons.name, - })), + q.from({ persons: collection }).select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })), ) return (
      - {(person) =>
    • {person.name}
    • } + {(person) => ( +
    • {person.name}
    • + )}
    ) @@ -1928,12 +1928,10 @@ describe(`Query Collections`, () => { function TestComponent() { const query = useLiveQuery((q) => - q - .from({ persons: collection }) - .select(({ persons }) => ({ - id: persons.id, - name: persons.name, - })), + q.from({ persons: collection }).select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })), ) return (