wcz-layout
Version:
110 lines (95 loc) • 4.03 kB
Markdown
name: client-db
description: "Use when: creating or modifying TanStack DB collections, live queries, optimistic collection writes, subset loading, collection preloading."
> Mechanics (`createCollection`, `queryCollectionOptions`, the live-query builder, `createOptimisticAction`) are covered by TanStack's own skills — load `@tanstack/db#db-core/live-queries`, `#db-core/mutations-optimistic`, and `@tanstack/react-db` for the API details. This skill adds only the wcz-layout conventions on top.
## Rules
- Use `eager` syncMode for top-level collections, `on-demand` for child/relational data.
- Always preload every `eager` collection used by a route, including collections used only by components rendered under that route. Do not preload `on-demand` collections.
- Use `useLiveQuery` for list/grid pages and `useLiveSuspenseQuery` for detail components wrapped in suspense.
- Shape data in the collection query whenever possible: filter, order, group and aggregate only the fields the consumer needs. Do not load broad results and further filter or transform them in component code.
- For mutations use `createOptimisticAction` — mirror the server mutation in `onMutate`, then call the server function and `collection.utils.refetch()` in `mutationFn`.
- For on-demand subset loading, convert `meta.loadSubsetOptions` with `fromTanDb()` (from `agnostic-query/tanstack-db`) before passing it to the server fn, and set `autoIndex: "eager"` with `defaultIndexType: BasicIndex` (from `@tanstack/react-db`).
## File Placement
```
src/db-collections/ — DB collections
src/server/actions/ — server functions (select/update/insert/delete)
src/lib/schemas/ — Zod schemas (shared between client and server)
src/lib/auth/scopes.ts — API scope keys
```
## Examples
```ts
// src/db-collections/<feature>.ts
export const librariesCollection = createCollection(
queryCollectionOptions({
queryKey: ["libraries"],
queryFn: () => selectLibraries(),
getKey: ({ id }) => id,
schema: LibrarySchema,
queryClient: queryClient,
syncMode: "eager",
}),
);
// src/db-collections/<feature>.ts — child/relational (on-demand)
import { BasicIndex } from "@tanstack/react-db";
import { fromTanDb } from "agnostic-query/tanstack-db";
export const bookCollection = createCollection(
queryCollectionOptions({
queryKey: ["books"],
queryFn: ({ meta }) => selectBooks({ data: fromTanDb(meta?.loadSubsetOptions) }),
getKey: ({ id }) => id,
schema: BookSchema,
queryClient: queryClient,
syncMode: "on-demand",
autoIndex: "eager",
defaultIndexType: BasicIndex,
}),
);
// Preload
export const Route = createFileRoute("/")({
loader: () => {
void librariesCollection.preload();
},
});
// simple query
const { data, isLoading } = useLiveQuery((q) =>
q.from({ library: libraryCollection }).orderBy(({ library }) => library.name, "asc"),
);
// advanced query with relational data
const { data } = useLiveQuery((q) =>
q
.from({ library: libraryCollection })
.where(({ library }) => eq(library.id, id))
.findOne()
.select(({ library }) => ({
...library,
books: toArray(
q
.from({ book: bookCollection })
.where(({ book }) => eq(book.libraryId, library.id))
.orderBy(({ book }) => book.title, "asc")
.select(({ book }) => ({
id: book.id,
title: book.title,
})),
),
})),
);
// form submission handler
const handleOnSubmit = createOptimisticAction<Library>({
onMutate: (formValues) => {
libraryCollection.update(id, (prev) => Object.assign(prev, formValues));
},
mutationFn: async (formValues) => {
await updateLibrary({ data: formValues });
await libraryCollection.utils.refetch();
},
});
// calling the handler
try {
const transaction = handleOnSubmit(formValues);
await transaction.isPersisted.promise;
} catch (error) {
if (error instanceof Error) alert(error.message);
}
```