diff --git a/.changeset/cute-falcons-wear.md b/.changeset/cute-falcons-wear.md
new file mode 100644
index 000000000..85447c9dc
--- /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'}
+ >
+)
+```
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..dde228f17 100644
--- a/examples/solid/todo/src/routes/__root.tsx
+++ b/examples/solid/todo/src/routes/__root.tsx
@@ -1,6 +1,12 @@
-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 +29,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 }),
],
diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts
index b804d3d5f..4fa7d264c 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,
@@ -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 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) =>
@@ -66,33 +66,51 @@ 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 query function that always returns QueryBuilder
export function useLiveQuery(
queryFn: (q: InitialQueryBuilder) => QueryBuilder,
-): {
- state: ReactiveMap>
+): Accessor>> & {
+ /**
+ * @deprecated use function result instead
+ * query.data -> query()
+ */
data: Array>
- collection: Accessor, string | number, {}>>
- status: Accessor
- isLoading: Accessor
- isReady: Accessor
- isIdle: Accessor
- isError: Accessor
- isCleanedUp: Accessor
+ state: ReactiveMap>
+ collection: Collection, string | number, {}>
+ status: CollectionStatus
+ isLoading: boolean
+ isReady: boolean
+ isIdle: boolean
+ isError: boolean
+ isCleanedUp: boolean
}
// Overload 1b: Accept query function that can return undefined/null
@@ -100,7 +118,7 @@ export function useLiveQuery(
queryFn: (
q: InitialQueryBuilder,
) => QueryBuilder | undefined | null,
-): {
+): Accessor>> & {
state: ReactiveMap>
data: Array>
collection: Accessor(
/**
* 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(() => ({
@@ -143,14 +161,14 @@ export function useLiveQuery(
* }))
*
* return (
- * {itemsQuery.data.length} items loaded}>
- *
+ * {itemsQuery().length} items loaded}>
+ *
* Loading...
*
- *
+ *
* Something went wrong
*
- *
+ *
* Preparing...
*
*
@@ -159,22 +177,26 @@ export function useLiveQuery(
// Overload 2: Accept config object
export function useLiveQuery(
config: Accessor>,
-): {
- state: ReactiveMap>
+): Accessor>> & {
+ /**
+ * @deprecated use function result instead
+ * query.data -> query()
+ */
data: Array>
- collection: Accessor, string | number, {}>>
- status: Accessor
- isLoading: Accessor
- isReady: Accessor
- isIdle: Accessor
- isError: Accessor
- isCleanedUp: Accessor
+ state: ReactiveMap>
+ collection: Collection, string | number, {}>
+ status: CollectionStatus
+ isLoading: boolean
+ isReady: boolean
+ isIdle: boolean
+ isError: boolean
+ isCleanedUp: boolean
}
/**
* 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) =>
@@ -188,7 +210,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
@@ -196,11 +218,11 @@ export function useLiveQuery(
* const sharedQuery = useLiveQuery(() => sharedCollection)
*
* return (
- * {(item) => }}>
- *
+ * {(item) => }}>
+ *
* Loading...
*
- *
+ *
* Error loading data
*
*
@@ -213,16 +235,20 @@ export function useLiveQuery<
TUtils extends Record,
>(
liveQueryCollection: Accessor>,
-): {
- state: ReactiveMap
+): Accessor> & {
+ /**
+ * @deprecated use function result instead
+ * query.data -> query()
+ */
data: Array
- collection: Accessor>
- status: Accessor
- isLoading: Accessor
- isReady: Accessor
- isIdle: Accessor
- isError: Accessor
- isCleanedUp: Accessor
+ state: ReactiveMap
+ collection: Collection
+ status: CollectionStatus
+ isLoading: boolean
+ isReady: boolean
+ isIdle: boolean
+ isError: boolean
+ isCleanedUp: boolean
}
// Implementation - use function overloads to infer the actual collection type
@@ -293,91 +319,121 @@ export function useLiveQuery(
)
}
- // Track current unsubscribe function
- let currentUnsubscribe: (() => void) | null = null
-
- createComputed(
- () => {
- const currentCollection = collection()
-
- // Handle null collection (disabled query)
+ const [getDataResource] = createResource(
+ () => ({ currentCollection: collection() }),
+ async ({ currentCollection }) => {
if (!currentCollection) {
- setStatus(`disabled` as const)
- state.clear()
- setData([])
- if (currentUnsubscribe) {
- currentUnsubscribe()
- currentUnsubscribe = null
- }
- return
+ return []
}
-
- // Update status ref whenever the effect runs
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()
+ if (!currentCollection) {
+ 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(() => {
+ 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,
- },
- )
+ })
+ },
+ {
+ includeInitialState: true,
+ },
+ )
- currentUnsubscribe = subscription.unsubscribe.bind(subscription)
+ onCleanup(() => {
+ subscription.unsubscribe()
+ })
+ })
- // Preload collection data if not already started
- if (currentCollection.status === `idle`) {
- createResource(() => currentCollection.preload())
- }
+ // We have to remove getters from the resource function so we wrap it
+ function getData() {
+ return getDataResource()
+ }
- // Cleanup when computed is invalidated
- onCleanup(() => {
- if (currentUnsubscribe) {
- currentUnsubscribe()
- currentUnsubscribe = null
- }
- })
+ Object.defineProperties(getData, {
+ data: {
+ get() {
+ return getData()
+ },
},
- undefined,
- { name: `TanstackDBSyncComputed` },
- )
-
- return {
- state,
- data,
- collection,
- status,
- isLoading: () => status() === `loading`,
- isReady: () => status() === `ready` || status() === `disabled`,
- isIdle: () => status() === `idle`,
- isError: () => status() === `error`,
- isCleanedUp: () => status() === `cleaned-up`,
- }
+ 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
}
diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx
index 2afca16d9..4b4641b9c 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`,
})
@@ -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,
@@ -798,9 +798,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`,
@@ -808,7 +808,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 () => {
@@ -887,7 +887,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)
@@ -904,7 +904,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()
@@ -940,9 +940,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`,
@@ -988,7 +988,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()
@@ -1011,7 +1011,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
@@ -1047,7 +1047,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)
})
@@ -1086,7 +1086,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()
@@ -1110,14 +1110,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 () => {
@@ -1143,10 +1143,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()
@@ -1169,8 +1169,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 () => {
@@ -1231,7 +1231,7 @@ describe(`Query Collections`, () => {
})
// Initially should be true
- expect(result.isLoading()).toBe(true)
+ expect(result.isLoading).toBe(true)
// Start sync for both collections
personCollection.preload()
@@ -1267,7 +1267,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
@@ -1314,7 +1314,7 @@ describe(`Query Collections`, () => {
)
// Initially should be false
- expect(result.isLoading()).toBe(true)
+ expect(result.isLoading).toBe(true)
// Start sync manually
collection.preload()
@@ -1345,7 +1345,7 @@ describe(`Query Collections`, () => {
// Wait for initial load
await waitFor(() => {
- expect(result.isLoading()).toBe(false)
+ expect(result.isLoading).toBe(false)
})
// Change parameters
@@ -1353,7 +1353,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
@@ -1400,9 +1400,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()
@@ -1410,7 +1410,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!()
@@ -1431,9 +1431,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`,
})
@@ -1457,18 +1457,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 () => {
@@ -1512,7 +1512,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!()
@@ -1555,15 +1555,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)
})
@@ -1634,7 +1634,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!()
@@ -1653,7 +1653,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
@@ -1673,9 +1673,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`,
@@ -1686,7 +1686,7 @@ describe(`Query Collections`, () => {
issueSyncMarkReady!()
await waitFor(() => {
- expect(result.isLoading()).toBe(false)
+ expect(result.isLoading).toBe(false)
})
expect(result.state.size).toBe(1)
})
@@ -1722,10 +1722,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()
@@ -1733,20 +1733,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`)
})
})