Skip to content

Commit cbfc3e0

Browse files
committed
Add the guide about the on-demand sync mode
1 parent ba3f098 commit cbfc3e0

4 files changed

Lines changed: 168 additions & 7 deletions

File tree

src/collections/todoItems.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const todoItemsCollection = createCollection<TodoItemRecord>(
6565
parsed.filters.forEach(({ field, operator, value }) => {
6666
const fieldName = field.join(".");
6767

68-
// Currently only "eq" operator is supported in the API
68+
// Currently only the "eq" operator is supported by our API
6969
if (operator === "eq") {
7070
params.set(fieldName, String(value));
7171
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import {Link} from '@tanstack/react-router'
2+
import {
3+
GHLink,
4+
HighLightComponent,
5+
LinkToArticle,
6+
OpenAPIRequestsPanelLink,
7+
} from '/src/components/tutorial'
8+
9+
## Query-driven sync
10+
11+
Sometimes you don't want to load all your data into your collection, only the subset that is required by the live queries you're running.
12+
13+
For example:
14+
15+
- instead of loading all the todo items a user has when they visit a project's page,
16+
- we can just load the ones that belong to that project.
17+
18+
This can be achieved by using the `on-demand` sync mode.
19+
20+
> FYI: the docs recommend it for large datasets (>50k rows), which means that using it is not really warranted in our case (we have a bit more than 1k rows). <a target="_blank" href="https://tanstack.com/db/latest/docs/overview#sync-modes">(source)</a>
21+
22+
### Switching to `"on-demand"`
23+
24+
Every collection's sync strategy is `"eager"` by default, which loads the entire collection upfront. First, we need to change it to `"on-demand"`:
25+
26+
```ts {4}
27+
export const todoItemsCollection = createCollection<TodoItemRecord>(
28+
queryCollectionOptions({
29+
// ...
30+
syncMode: "on-demand",
31+
// ...
32+
})
33+
```
34+
35+
When the `syncMode` was eager, the function that we used to populate the collection and keep it in sync (`queryFn`) was basically one `fetch` call to the API:
36+
37+
```ts {4-11}
38+
export const todoItemsCollection = createCollection<TodoItemRecord>(
39+
queryCollectionOptions({
40+
// ...
41+
syncMode: 'eager',
42+
queryFn: async () => {
43+
// Fetch all the todo items
44+
const res = await fetch('/api/todo-items', {method: 'GET'})
45+
46+
const todoItems: TodoItemRecord[] = await res.json()
47+
48+
return todoItems
49+
},
50+
// ...
51+
}),
52+
)
53+
```
54+
55+
Using this `queryFn` for the `"on-demand"` `syncMode` would defeat the very purpose of it, because it would fetch every single todo item in the database for each query.
56+
57+
Instead of that, we want the collection to load all the todo items that are required for the queries we run. Right now, we have only one query for the `todoItemsCollection`:
58+
59+
```tsx {5}
60+
const {data: allTodoItems} = useLiveQuery(
61+
(q) =>
62+
q
63+
.from({todoItem: todoItemsCollection})
64+
.where(({todoItem}) => eq(todoItem.projectId, projectId))
65+
.orderBy(({todoItem}) => todoItem.position, {
66+
direction: 'asc',
67+
stringSort: 'lexical',
68+
}),
69+
[projectId],
70+
)
71+
```
72+
73+
The highlighted line in the code block above is the filter we need to use in the database query on the server. Luckily for us, when this live query runs, TanStack DB automatically passes down all the predicates coming from this live query to the `queryFn`.
74+
75+
```ts {22}
76+
export const todoItemsCollection = createCollection<TodoItemRecord>(
77+
queryCollectionOptions({
78+
syncMode: 'on-demand',
79+
queryFn: async ({meta}) => {
80+
const params = new URLSearchParams()
81+
82+
if (meta) {
83+
const {where} = meta.loadSubsetOptions
84+
// Parse the expressions into simple format
85+
const parsed = parseLoadSubsetOptions({where})
86+
// Build query parameters from parsed filters
87+
// Add filters
88+
parsed.filters.forEach(({field, operator, value}) => {
89+
const fieldName = field.join('.')
90+
// Currently only the "eq" operator is supported by our API
91+
if (operator === 'eq') {
92+
params.set(fieldName, String(value))
93+
}
94+
})
95+
}
96+
97+
const res = await fetch(`/api/todo-items?${params}`, {method: 'GET'})
98+
const todoItems: TodoItemRecord[] = await res.json()
99+
return todoItems
100+
},
101+
}),
102+
)
103+
```
104+
105+
To observe this behavior
106+
107+
- open the <OpenAPIRequestsPanelLink>API Request Panel</OpenAPIRequestsPanelLink>,
108+
- and [reload this page]()
109+
110+
You should see that the request to `/api/todo-items` has a search string attached to it, similar to this one: `?projectId=aQLI4Nzvmls31l4aep9LqnU`.
111+
112+
### Query predicates
113+
114+
In this app we stop here, but the predicates (`where` clauses, `orderBy`, `limit`, and `offset`) coming from the queries can cover far more advanced use cases.
115+
116+
We could use `orderBy` to order elements on the server, `offset` to support pagination and more.
117+
118+
Check out the <a target="_blank" href="https://tanstack.com/db/latest/docs/collections/query-collection#queryfn-and-predicate-push-down">documentation</a> for more info about them.
119+
120+
### Transitioning from `"eager"` to `"on-demand"`: some pitfalls
121+
122+
When I first refactor the `todoItemsCollection` to use `on-demand` sync, I ran into some issues.
123+
124+
#### `.toArrayWhenReady()` vs `.toArray`
125+
126+
For example, this line of code calling the <a target="_blank" href="https://tanstack.com/db/latest/docs/reference/interfaces/Collection#toarraywhenready">`.toArrayWhenReady()` method</a> caused the collection to fetch every single todo item from the remote source:
127+
128+
```ts
129+
// ❌ Get the current state of the collection from the server,
130+
await todoItemsCollection.toArrayWhenReady()
131+
```
132+
133+
I had to change it to <a target="_blank" href="https://tanstack.com/db/latest/docs/reference/interfaces/Collection#toarray">`toArray`</a>, to use only the data that we already have in the client-side cache:
134+
135+
```ts
136+
// ✅ Get the current state of the collection from the cache
137+
await todoItemsCollection.toArray
138+
```
139+
140+
#### `.refetch()`
141+
142+
Another line that resulted in loading all the collection data instead of the subset required by the query in `"on-demand"` mode was this one:
143+
144+
```ts
145+
// ❌ Triggers fetching every todo item in the collection
146+
todoItemsCollection.utils.refetch();
147+
```
148+
149+
The moral of the story is that you have to watch out and make sure you don't accidentally sync all the data in the collection when you use `"on-demand"` mode.
150+
151+
### Progressive mode
152+
153+
There is a third sync mode, that I haven't mentioned before called `"progressive"`. Here's how it relates to the other two:
154+
155+
- `"eager"`: load all the data in the collection
156+
- `"on-demand"`: load only the queried data
157+
- `"progressive"`: load the queried data first, then everything else in the background
158+
159+
You can read more about the sync modes <a target="_blank" href="https://tanstack.com/db/latest/docs/overview#sync-modes">in the documentation</a>.

src/data/tutorial.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import QueryDrivenSync from "@/data/deep-dives/query-driven-sync.mdx";
12
import CollectionsIntro from "@/data/tutorial/collections-intro.mdx";
23
import HowDoCollectionsWork from "@/data/tutorial/how-do-collections-work.mdx";
34
import OptimisticUpdates from "@/data/tutorial/optimistic-updates.mdx";
@@ -47,10 +48,10 @@ export const tutorialArticles: Step[] = tutorialArticlesWithoutNextSteps.map(
4748
);
4849

4950
const deepDiveArticlesWithoutNextSteps: Step[] = [
50-
// {
51-
// title: "Optimistic Actions",
52-
// file: OptimisticActions,
53-
// },
51+
{
52+
title: "Query-driven sync",
53+
file: QueryDrivenSync,
54+
},
5455
];
5556

5657
// export const deepDiveArticles: Step[] = deepDiveArticlesWithoutNextSteps.map(

src/data/tutorial/what-is-next.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import {Link} from '@tanstack/react-router'
22
import {
33
GHLink,
44
HighLightComponent,
5-
OpenAPIRequestsPanelLink
5+
OpenAPIRequestsPanelLink,
6+
LinkToArticle
67
} from '/src/components/tutorial'
78

89
## What's next?
@@ -34,7 +35,7 @@ The source code is <a target="_blank" href="https://github.com/fulopkovacs/tryta
3435
3536
### 🔬 Deep Dive Articles
3637

37-
We have a growing list of deep dive articles that explore more complex topics (such as Optimistic Actions for mutations involving multiple collections). You can view them in the TOC on the left.
38+
We have a growing list of deep dive articles that explore more complex topics (like the <LinkToArticle articleTitle="Query-driven sync">`on-demand` sync mode</LinkToArticle>). You can view them in the TOC on the left.
3839

3940
### 👋 Say hi
4041
If you liked this guide, please star (⭐) <a target="_blank" href="https://github.com/fulopkovacs/trytanstackdb.com/">the repo on GitHub</a>.

0 commit comments

Comments
 (0)