|
| 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>. |
0 commit comments