UNPKG

wcz-layout

Version:

133 lines (114 loc) 5.08 kB
--- name: client-db description: "Use when reading or writing client data with TanStack DB: collections, live queries, route preloading, optimistic writes, eager/on-demand sync, or browser SQLite persistence." metadata: type: convention library: wcz-layout --- > Mechanics (the live-query builder, `createOptimisticAction`, collection adapters, > browser persistence) are covered by TanStack's own skills. List the skills shipped by > `@tanstack/db` and `@tanstack/react-db` and load whichever cover the task at hand. > This skill adds the wcz-layout conventions on top, and overrides upstream where noted. ## Rules - One `DbClient` per request, built in `src/router.tsx` and put on the router context next to `queryClient`. Components reach it with `useDbClient()`, loaders with `context.dbClient`. - Declare collections as descriptors: `collectionOptions(id, (client) => ...)`. Take the query client from the factory with `client.requireDependency<QueryClient>("queryClient")`, never a module-scope one — it is built per request and a singleton leaks across SSR. - Export a `use<Table>Collection` hook next to each collection. It is the only way components get a live collection to write to. - Define every live query once in `src/db/queries/<table>.ts` as `{ query: (q: InitialQueryBuilder) => ... }`. Reuse that definition in loaders and components. - Preload in the route loader and return the promise: `loader: ({ context }) => context.dbClient.preloadLiveQuery(<x>QueryOptions)`. This transports the query result. Explicit collection preload transports source rows and is a no-op for on-demand collections. - `useLiveQuery` for list pages, `useLiveSuspenseQuery` for detail components under suspense. Suspense data is initialized, but `.findOne()` still returns `T | undefined`; handle not-found explicitly. - Shape data in the query: filter, order, group and aggregate only the fields the consumer needs. Do not load broad results and transform them in component code. - Mutations go through `createOptimisticAction`: mirror the server write in `onMutate`, call the server function in `mutationFn`, then `await utils.refetch({ throwOnError: true })` on every collection that function writes. Await `transaction.isPersisted.promise` at the call site and surface failures with `alert()`. - `syncMode: "eager"` for top-level collections, `"on-demand"` for child data and anything expected past ~10k rows. - Increment persisted `schemaVersion` whenever the row shape changes. ## Examples ```ts // src/db/collections/<table>.ts import { persistedCollectionOptions } from "@tanstack/browser-db-sqlite-persistence"; import { queryCollectionOptions } from "@tanstack/query-db-collection"; import { collectionOptions, useDbClient } from "@tanstack/react-db"; import type { QueryClient } from "@tanstack/react-query"; import { browserPersistence } from "../persistence"; import { TodoSchema } from "~/lib/schemas/todo"; import { selectTodos } from "~/server/actions/todo"; const TODOS_ID = "todos"; export const todoCollection = collectionOptions(TODOS_ID, (client) => { const options = queryCollectionOptions({ id: TODOS_ID, queryKey: [TODOS_ID], queryFn: () => selectTodos(), getKey: ({ id }) => id, schema: TodoSchema, queryClient: client.requireDependency<QueryClient>("queryClient"), syncMode: "eager", }); const persistence = browserPersistence(); if (!persistence) return options; return persistedCollectionOptions({ ...options, persistence, schemaVersion: 1, // Increment when the persisted row shape changes. }) as typeof options; }); export const useTodoCollection = () => useDbClient().collection(todoCollection); ``` ```ts // src/db/queries/<table>.ts import type { InitialQueryBuilder } from "@tanstack/react-db"; import { eq } from "@tanstack/react-db"; import { todoCollection } from "../collections/todo"; export const todosQueryOptions = { query: (q: InitialQueryBuilder) => q.from({ todo: todoCollection }).orderBy(({ todo }) => todo.id, "desc"), }; export const todoByIdQueryOptions = (id: string) => ({ query: (q: InitialQueryBuilder) => q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, id)) .findOne(), }); ``` ```tsx // route + component export const Route = createFileRoute("/todos/$id")({ beforeLoad: requireAuth("admin"), loader: ({ context, params }) => context.dbClient.preloadLiveQuery(todoByIdQueryOptions(params.id)), }); const { id } = Route.useParams(); const todos = useTodoCollection(); const { data } = useLiveSuspenseQuery(todoByIdQueryOptions(id)); const handleOnDelete = createOptimisticAction<string>({ onMutate: (id) => { todos.delete(id); }, mutationFn: async (id) => { await deleteTodo({ data: { id } }); await todos.utils.refetch({ throwOnError: true }); }, }); try { const transaction = handleOnDelete(id); await transaction.isPersisted.promise; } catch (error) { if (error instanceof Error) await alert(error.message); } if (!data) return null; ```