From 2d7155f23d23ddc3f2ecba5829f3a5d3a01bbdbf Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Mon, 13 Jul 2026 13:58:05 -0600 Subject: [PATCH 1/4] docs(query-db-collection): document scoped collection factories --- docs/collections/query-collection.md | 74 ++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/docs/collections/query-collection.md b/docs/collections/query-collection.md index 1c75f022d..f8a37bc46 100644 --- a/docs/collections/query-collection.md +++ b/docs/collections/query-collection.md @@ -112,25 +112,79 @@ 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 defines a **business scope**: it determines which server resource the collection represents. Put that scope in both the Query key and the query function. This extends the [runtime `QueryClient` factory pattern](#creating-collection-options-from-a-runtime-queryclient) with another explicit factory 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, + staleTime: 30_000, + }) + ) } ``` -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< + typeof createProjectTodosCollection +> + +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 a collision-safe stable key (or nested maps) containing every value. Do not call the factory on each render. In a long-lived client, scopes such as user-selected projects may grow without bound; evict entries that are no longer needed and call `await collection.cleanup()` when your application owns their lifecycle. Request-local maps can instead be discarded with the request. + +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. The collection passes those options to `queryFn` through `ctx.meta.loadSubsetOptions` and uses them to manage subset Query keys. See [QueryFn and Predicate Push-Down](#queryfn-and-predicate-push-down). + +Do not create another collection for every `where`, `orderBy`, or `limit`. Reuse the project-scoped collection and let on-demand subset loading represent those relational queries. Create separate collections only when the server resources are genuinely distinct business scopes. ### Query Options From 6f51ade8daef3ca1bc1cb8188169bda68af48cc5 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Mon, 13 Jul 2026 15:05:26 -0600 Subject: [PATCH 2/4] docs: simplify scoped collection guidance --- docs/collections/query-collection.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/collections/query-collection.md b/docs/collections/query-collection.md index f8a37bc46..c2635453b 100644 --- a/docs/collections/query-collection.md +++ b/docs/collections/query-collection.md @@ -116,7 +116,7 @@ This keeps SSR and request-scoped code from sharing a global `QueryClient` while ### Business-Scoped Collection Factories -A tenant, project, account, or route parameter defines a **business scope**: it determines which server resource the collection represents. Put that scope in both the Query key and the query function. This extends the [runtime `QueryClient` factory pattern](#creating-collection-options-from-a-runtime-queryclient) with another explicit factory parameter: +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 interface Todo { @@ -140,7 +140,6 @@ export function createProjectTodosCollection( queryFn: () => fetchProjectTodos(projectId), queryClient, getKey: (todo) => todo.id, - staleTime: 30_000, }) ) } @@ -149,9 +148,7 @@ export function createProjectTodosCollection( 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: ```typescript -type ProjectTodosCollection = ReturnType< - typeof createProjectTodosCollection -> +type ProjectTodosCollection = ReturnType const projectCollections = new WeakMap< QueryClient, @@ -180,11 +177,11 @@ export function getProjectTodosCollection( } ``` -For multiple scope values, use a collision-safe stable key (or nested maps) containing every value. Do not call the factory on each render. In a long-lived client, scopes such as user-selected projects may grow without bound; evict entries that are no longer needed and call `await collection.cleanup()` when your application owns their lifecycle. Request-local maps can instead be discarded with the request. +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. -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. The collection passes those options to `queryFn` through `ctx.meta.loadSubsetOptions` and uses them to manage subset Query keys. See [QueryFn and Predicate Push-Down](#queryfn-and-predicate-push-down). +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 another collection for every `where`, `orderBy`, or `limit`. Reuse the project-scoped collection and let on-demand subset loading represent those relational queries. Create separate collections only when the server resources are genuinely distinct business scopes. +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 From 3623b312f94fd0a8bc4dccaa47aea2756cdc431a Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Mon, 13 Jul 2026 15:07:48 -0600 Subject: [PATCH 3/4] docs: add scoped factory changeset --- .changeset/document-scoped-query-factories.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/document-scoped-query-factories.md diff --git a/.changeset/document-scoped-query-factories.md b/.changeset/document-scoped-query-factories.md new file mode 100644 index 000000000..a6afbb3e1 --- /dev/null +++ b/.changeset/document-scoped-query-factories.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-db-collection': patch +--- + +Document business-scoped Query Collection factories, including stable memoization and lifecycle guidance, while distinguishing business scope from relational subset loading. From b8812112532d3f88019230676cce27d8ca698c1e Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Thu, 16 Jul 2026 14:02:17 -0600 Subject: [PATCH 4/4] docs: remove unnecessary changeset --- .changeset/document-scoped-query-factories.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/document-scoped-query-factories.md diff --git a/.changeset/document-scoped-query-factories.md b/.changeset/document-scoped-query-factories.md deleted file mode 100644 index a6afbb3e1..000000000 --- a/.changeset/document-scoped-query-factories.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@tanstack/query-db-collection': patch ---- - -Document business-scoped Query Collection factories, including stable memoization and lifecycle guidance, while distinguishing business scope from relational subset loading.