diff --git a/docs/collections/query-collection.md b/docs/collections/query-collection.md index 1c75f022d..c2635453b 100644 --- a/docs/collections/query-collection.md +++ b/docs/collections/query-collection.md @@ -112,25 +112,76 @@ export function getTodosCollection( Avoid calling `createCollection(todoCollectionOptions(queryClient))` independently during render or in each consumer. Share the stable collection instance for the lifetime of that `QueryClient` scope. -The same pattern works for scoped or parameterized collections. Pass route params, a tenant ID, or filters into the factory alongside the `QueryClient`: +This keeps SSR and request-scoped code from sharing a global `QueryClient` while keeping each collection instance stable within its scope. + +### Business-Scoped Collection Factories + +A tenant, project, account, or route parameter can define a **business scope**: the server resource that a collection represents. Include the scope in both the Query key and `queryFn`. This extends the [runtime `QueryClient` factory pattern](#creating-collection-options-from-a-runtime-queryclient) with an explicit scope parameter: ```typescript -export function projectTodosCollectionOptions( +interface Todo { + id: string + title: string + projectId: string +} + +async function fetchProjectTodos(projectId: string): Promise> { + const response = await fetch(`/api/projects/${projectId}/todos`) + return response.json() +} + +export function createProjectTodosCollection( queryClient: QueryClient, projectId: string, ) { - return queryCollectionOptions({ - queryKey: ["projects", projectId, "todos"], - queryFn: () => fetchProjectTodos(projectId), - queryClient, - getKey: (todo) => todo.id, - }) + return createCollection( + queryCollectionOptions({ + queryKey: ["projects", projectId, "todos"], + queryFn: () => fetchProjectTodos(projectId), + queryClient, + getKey: (todo) => todo.id, + }) + ) } ``` -For parameterized collections, memoize by both the scoped `QueryClient` and the parameter tuple. If a long-lived scope can create unbounded parameter sets, add eviction or dispose collections when they are no longer needed. +The scope is part of the collection's identity. Memoize by both the `QueryClient` and a stable scope key so consumers of the same project share one collection: -This keeps SSR and request-scoped code from sharing a global `QueryClient` while keeping each collection instance stable within its scope. +```typescript +type ProjectTodosCollection = ReturnType + +const projectCollections = new WeakMap< + QueryClient, + Map +>() + +export function getProjectTodosCollection( + queryClient: QueryClient, + projectId: string, +): ProjectTodosCollection { + let collectionsByProject = projectCollections.get(queryClient) + + if (!collectionsByProject) { + collectionsByProject = new Map() + projectCollections.set(queryClient, collectionsByProject) + } + + let collection = collectionsByProject.get(projectId) + + if (!collection) { + collection = createProjectTodosCollection(queryClient, projectId) + collectionsByProject.set(projectId, collection) + } + + return collection +} +``` + +For multiple scope values, use nested maps or a collision-safe stable key that includes every value. Do not call the factory on each render. In a long-lived client, user-selected scopes can make the map grow without bound. Remove unused entries and call `await collection.cleanup()` when your application owns their lifecycle. Request-local maps can be discarded with the request. + +A business scope is separate from a **relational subset** requested by a live query. With `syncMode: "on-demand"`, `LoadSubsetOptions` describes predicates, ordering, limits, and offsets within one business-scoped collection. These options reach `queryFn` through `ctx.meta.loadSubsetOptions` and determine the subset Query keys. See [QueryFn and Predicate Push-Down](#queryfn-and-predicate-push-down). + +Do not create a collection for each `where`, `orderBy`, or `limit`. Reuse the business-scoped collection and let on-demand loading represent those subsets. Create separate collections only for distinct server resources. ### Query Options