From 0ca5bc7950a5b979416d82d3cfac5e159c6af19d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 00:17:55 +0100
Subject: [PATCH 01/18] Fix some grammar errors
---
src/data/tutorial/collections-intro.mdx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/data/tutorial/collections-intro.mdx b/src/data/tutorial/collections-intro.mdx
index 0710744..5e4a177 100644
--- a/src/data/tutorial/collections-intro.mdx
+++ b/src/data/tutorial/collections-intro.mdx
@@ -5,7 +5,7 @@ import {GHLink, HighLightComponent} from "/src/components/tutorial"
### A quick demo
-We have a `projects` table in our database. If you click the button below, you can see all the components that to display data from this table:
+Let's start with a quick demo and it'll all make sense. We have a `projects` table in our database. If you click the button below, you can see all the components that display data from this table:
Highlight components
@@ -54,13 +54,13 @@ Thankfully, no, and there are two reasons why.
- To check it, follow these steps:
1. go to the `Network` tab in your browser's dev tools
2. reload the page (click here)
- 3. see that there is only one `GET` request to the `/api/projects` endpoint, despite having 2 components using data from the `projectCollection`
- 4. if you navigate to any other project page, you shouldn't see more requests made
+ 3. see that there is only one `GET` request to the `/api/projects` endpoint, despite having 2 components using data from the `projectsCollection`
+ 4. If you navigate to any other project page, you shouldn't see more requests made
> Unless the cache needs to be revalidated (because the state has been mutated or went stale).
-2. Live queries are updated incrementally, (they use `d2ts`, a differential dataflow library)
+2. Live queries are updated incrementally (they use `d2ts`, a differential dataflow library)
- this results in _sub-millisecond_ execution times (even for complex queries)
- which means that it's BLAZING FAST!!! ⚡⚡⚡
From dda2925cebe2e0eef50b8569cd35ddb671ac7836 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 00:36:10 +0100
Subject: [PATCH 02/18] Update the info about observing requests
---
src/components/ApiPanelToggle.tsx | 43 ++++----
src/components/ApiRequestsPanel.tsx | 101 +++++++++---------
src/components/tutorial/index.tsx | 65 ++++++++++-
src/data/tutorial/collections-intro.mdx | 4 +-
.../highlight-collection-related-info.tsx | 2 +-
5 files changed, 142 insertions(+), 73 deletions(-)
diff --git a/src/components/ApiPanelToggle.tsx b/src/components/ApiPanelToggle.tsx
index fac605c..26257b6 100644
--- a/src/components/ApiPanelToggle.tsx
+++ b/src/components/ApiPanelToggle.tsx
@@ -4,6 +4,7 @@ import { userPreferencesCollection } from "@/collections/UserPreferences";
import { USER_PLACEHOLDER } from "@/utils/USER_PLACEHOLDER_CONSTANT";
import { Button } from "./ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
+import { HighlightWrapper } from "@/utils/highlight-collection-related-info";
export function ApiPanelToggle() {
const { data: userPreferences } = useLiveQuery((q) =>
@@ -20,25 +21,27 @@ export function ApiPanelToggle() {
const isOpen = userPreferences?.networkPanel === "open";
return (
-
-
-
-
-
-
{isOpen ? "Hide API requests" : "Show API requests"}
-
-
+
+
+
+
+
+
+
{isOpen ? "Hide API requests" : "Show API requests"}
+
+
+
);
}
diff --git a/src/components/ApiRequestsPanel.tsx b/src/components/ApiRequestsPanel.tsx
index 56e196e..c5eb9b9 100644
--- a/src/components/ApiRequestsPanel.tsx
+++ b/src/components/ApiRequestsPanel.tsx
@@ -13,6 +13,7 @@ import {
} from "@/collections/apiRequests";
import { userPreferencesCollection } from "@/collections/UserPreferences";
import { cn } from "@/lib/utils";
+import { HighlightWrapper } from "@/utils/highlight-collection-related-info";
import { USER_PLACEHOLDER } from "@/utils/USER_PLACEHOLDER_CONSTANT";
import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
@@ -196,58 +197,60 @@ export function ApiRequestsPanel() {
className="flex flex-col h-full max-h-full overflow-hidden bg-background"
style={{ width: API_PANEL_WIDTH }}
>
-
- {/* Header */}
-
-
-
API Requests
-
+
+ {/* Header */}
+
+
+
API Requests
+
+ {requests.length}
+
+
+
-
-
- {/* Request list */}
-
- {requests.length === 0 ? (
-
-
- No requests yet
-
-
- API requests will appear here
-
-
- ) : (
-
- {requests.map((request) => (
-
- ))}
-
- )}
-
-
+ {/* Request list */}
+
+ {requests.length === 0 ? (
+
+
+ No requests yet
+
+
+ API requests will appear here
+
+
+ ) : (
+
+ {requests.map((request) => (
+
+ ))}
+
+ )}
+
+
+
)}
diff --git a/src/components/tutorial/index.tsx b/src/components/tutorial/index.tsx
index 2016499..46d67df 100644
--- a/src/components/tutorial/index.tsx
+++ b/src/components/tutorial/index.tsx
@@ -1,7 +1,9 @@
import { Link } from "@tanstack/react-router";
import { GithubIcon, SearchIcon } from "lucide-react";
-import { type ReactNode, useMemo } from "react";
+import { type ReactNode, useCallback, useMemo } from "react";
import z from "zod";
+import { userPreferencesCollection } from "@/collections/UserPreferences";
+import { USER_PLACEHOLDER } from "@/utils/USER_PLACEHOLDER_CONSTANT";
import { Button } from "../ui/button";
/**
@@ -17,6 +19,8 @@ export const highlightParamSchema = z.object({
"board",
"editProject",
"apiLatencyConfigurator",
+ "networkPanel_toggle",
+ "networkPanel_panel",
])
.optional(),
});
@@ -91,6 +95,38 @@ export function HighLightComponent({
);
}
+export function HighlightLink({
+ h_id: newHighLightGroupId,
+ children,
+}: {
+ h_id: string;
+ children: ReactNode;
+}) {
+ const highlight = useMemo(() => {
+ try {
+ return highlightParamSchema.parse({
+ highlight: newHighLightGroupId,
+ }).highlight;
+ } catch (e) {
+ return undefined;
+ }
+ }, [newHighLightGroupId]);
+
+ return (
+ ({
+ ...s,
+ highlight,
+ })}
+ >
+ {children}
+
+ );
+}
+
export function LinkToArticle({
children,
articleTitle,
@@ -115,3 +151,30 @@ export function LinkToArticle({
);
}
+
+export function OpenAPIRequestsPanelLink({
+ children,
+}: {
+ children: ReactNode;
+}) {
+ const openThePanel = useCallback(() => {
+ if (typeof window !== "undefined") {
+ userPreferencesCollection.update(USER_PLACEHOLDER.id, (draft) => {
+ draft.networkPanel = "open";
+ });
+ }
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/data/tutorial/collections-intro.mdx b/src/data/tutorial/collections-intro.mdx
index 5e4a177..96a4711 100644
--- a/src/data/tutorial/collections-intro.mdx
+++ b/src/data/tutorial/collections-intro.mdx
@@ -1,5 +1,5 @@
import {Link} from '@tanstack/react-router'
-import {GHLink, HighLightComponent} from "/src/components/tutorial"
+import {GHLink, HighLightComponent, OpenAPIRequestsPanelLink} from "/src/components/tutorial"
## What are collections?
@@ -52,7 +52,7 @@ Thankfully, no, and there are two reasons why.
1. Multiple queries using the same collection will sync the collection with the backend only once, and use the cache the rest of the times.
- To check it, follow these steps:
- 1. go to the `Network` tab in your browser's dev tools
+ 1. open the _"API Requests"_ panel by clicking here
2. reload the page (click here)
3. see that there is only one `GET` request to the `/api/projects` endpoint, despite having 2 components using data from the `projectsCollection`
4. If you navigate to any other project page, you shouldn't see more requests made
diff --git a/src/utils/highlight-collection-related-info.tsx b/src/utils/highlight-collection-related-info.tsx
index 8c01d06..78a44f1 100644
--- a/src/utils/highlight-collection-related-info.tsx
+++ b/src/utils/highlight-collection-related-info.tsx
@@ -33,7 +33,7 @@ export function HighlightWrapper({
return (
Date: Sun, 4 Jan 2026 00:50:06 +0100
Subject: [PATCH 03/18] Some rollbacks were not immediately triggered when an
API request failed
---
src/collections/projects.ts | 2 ++
src/collections/todoItems.ts | 4 +++-
src/components/ApiPanelToggle.tsx | 2 +-
3 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/collections/projects.ts b/src/collections/projects.ts
index 1775fe3..9de74a0 100644
--- a/src/collections/projects.ts
+++ b/src/collections/projects.ts
@@ -34,6 +34,8 @@ export const projectsCollection = createCollection(
// TODO: handle error
console.error("Failed to update todo item:", error);
}
+ // Re-throw to trigger immediate rollback of optimistic update
+ throw error;
}
},
getKey: (item) => item.id,
diff --git a/src/collections/todoItems.ts b/src/collections/todoItems.ts
index f94caeb..034766b 100644
--- a/src/collections/todoItems.ts
+++ b/src/collections/todoItems.ts
@@ -90,6 +90,7 @@ export const todoItemsCollection = createCollection(
} catch (error) {
toast.error(`Failed to insert todo item "${newTodoItem.title}"`);
console.error("Failed to insert todo item:", error);
+ throw error;
}
},
onUpdate: async ({ transaction }) => {
@@ -133,7 +134,7 @@ export const todoItemsCollection = createCollection(
);
},
);
- } catch (_) {
+ } catch (error) {
toast.error(`Failed to update todo item "${original.title}"`);
// TODO: handle this one later properly
@@ -143,6 +144,7 @@ export const todoItemsCollection = createCollection(
// // Sync back the server's data
// todoItemsCollection.utils.refetch();
// }
+ throw error;
}
// Do not sync back the server's data by default
diff --git a/src/components/ApiPanelToggle.tsx b/src/components/ApiPanelToggle.tsx
index 26257b6..851045e 100644
--- a/src/components/ApiPanelToggle.tsx
+++ b/src/components/ApiPanelToggle.tsx
@@ -1,10 +1,10 @@
import { eq, useLiveQuery } from "@tanstack/react-db";
import { ActivityIcon } from "lucide-react";
import { userPreferencesCollection } from "@/collections/UserPreferences";
+import { HighlightWrapper } from "@/utils/highlight-collection-related-info";
import { USER_PLACEHOLDER } from "@/utils/USER_PLACEHOLDER_CONSTANT";
import { Button } from "./ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
-import { HighlightWrapper } from "@/utils/highlight-collection-related-info";
export function ApiPanelToggle() {
const { data: userPreferences } = useLiveQuery((q) =>
From cbf70bf872f608e130e0d9cb3b94d0231634e82f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 00:59:27 +0100
Subject: [PATCH 04/18] Update the optimistic updates article
---
src/data/tutorial/optimistic-updates.mdx | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/src/data/tutorial/optimistic-updates.mdx b/src/data/tutorial/optimistic-updates.mdx
index 424a622..f86587f 100644
--- a/src/data/tutorial/optimistic-updates.mdx
+++ b/src/data/tutorial/optimistic-updates.mdx
@@ -3,6 +3,7 @@ import {
GHLink,
HighLightComponent,
LinkToArticle,
+ OpenAPIRequestsPanelLink
} from '/src/components/tutorial'
## Optimistic updates
@@ -34,20 +35,21 @@ Set the API latency to 5s and reload the page. You'll see that th
Now rename the project again. It still changes immediately!
-The reason why we see the change so fast is that the displayed data is from the cache which gets updated basically instantly, while the changes are synced back to the remote db in the background.
+The reason why we see the change so fast is that the displayed data is from the cache which gets updated basically instantly, while the changes are synced back to the remote database in the background.
### What happens when the data in the remote db can't be updated?
Let's rename our current project to the name of another existing project (like `Project Beta`).
-1. at first, everything is the same (fast update)
-2. when the http request fails in the background (because all project names
+1. At first, everything is the same (fast update)
+2. When the HTTP request fails in the background (because all project names
must be unique)
- - the changes are immediately rolled back,
- - and a toast error message appears in the top right corner
+ - The changes are immediately rolled back,
+ - And an error message appears in the top right corner
+ - A new `GET` request to `/api/projects` syncs the local state with the remote database (check the _API Requests_ panel)
-When an update fails, the mutations are rolled back. Rollbacks work seamlessly even for mutations that involve multiple collections (see the tutorial article on Optimistic Actions).
+The rule is simple: when an update fails, the mutation is rolled back and the collection's state is synced with the remote data source.
**"Okay, but how does the syncing happen? Is it magic???"**. Glad you asked.
-Everything is magic if you just believe in it, let me show you how it works in
+Everything is magic if you just believe in it; let me show you how it works in
the next section. ✨
From 34cc85548c52a3fde030033c1c23ae42e87b0c98 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 00:59:48 +0100
Subject: [PATCH 05/18] Remove the optimistic-actions article (it's not used)
---
src/data/tutorial.ts | 9 +-
src/data/tutorial/optimistic-actions.mdx | 134 -----------------------
2 files changed, 4 insertions(+), 139 deletions(-)
delete mode 100644 src/data/tutorial/optimistic-actions.mdx
diff --git a/src/data/tutorial.ts b/src/data/tutorial.ts
index 9811056..7ca1763 100644
--- a/src/data/tutorial.ts
+++ b/src/data/tutorial.ts
@@ -1,6 +1,5 @@
import CollectionsIntro from "@/data/tutorial/collections-intro.mdx";
import HowDoCollectionsWork from "@/data/tutorial/how-do-collections-work.mdx";
-import OptimisticActions from "@/data/tutorial/optimistic-actions.mdx";
import OptimisticUpdates from "@/data/tutorial/optimistic-updates.mdx";
import WhatIsNext from "@/data/tutorial/what-is-next.mdx";
@@ -48,10 +47,10 @@ export const tutorialArticles: Step[] = tutorialArticlesWithoutNextSteps.map(
);
const deepDiveArticlesWithoutNextSteps: Step[] = [
- {
- title: "Optimistic Actions",
- file: OptimisticActions,
- },
+ // {
+ // title: "Optimistic Actions",
+ // file: OptimisticActions,
+ // },
];
// export const deepDiveArticles: Step[] = deepDiveArticlesWithoutNextSteps.map(
diff --git a/src/data/tutorial/optimistic-actions.mdx b/src/data/tutorial/optimistic-actions.mdx
deleted file mode 100644
index 0a84b83..0000000
--- a/src/data/tutorial/optimistic-actions.mdx
+++ /dev/null
@@ -1,134 +0,0 @@
-import {Link} from '@tanstack/react-router'
-import {
- GHLink,
- HighLightComponent,
-} from '/src/components/tutorial'
-
-## Optimistic Actions
-
-### Moving task between boards
-
-In our highly sophisticated todo app, the positions of the todo items are stored in a column of the `projects` table. It's a json array:
-
-
-```json
-[
- "board-1": ["task-1", "task-2", ...],
- "board-2": [...],
- "board-2": [...]
-]
-```
-
-
-
-
-When new task is created, we need to update two different tables in our database:
-1. `todo-items`: we insert the new task's record into this table
-1. `projects`: the json array in the `item_positions_in_the_project` column must be updated to reflect the new orders of the tasks on the boards
-
-### The naive approach
-
-At first, you might want to do something like this:
-
-```ts
-todoItemsCollection.insert(/*... */);
-projectsCollection.update(/* ... */);
-```
-
-There's a problem with this approach: what happens if one of the requests fail? For example:
-
-- ✅ the new todo item gets inserted successfully,
-- ❌but the project can't be updated for some reason
- - this means that the changes made to the project will be rolled back
-
-Oh no! 💀 Now we don't know where the task is on the board.
-
-### The solution: `createOptimisticAction()`
-
-At this point, what we want is an action that does the following:
-- optimistically updates both collections in the cache
-- sends one request to the api, that attempts to update both tables (in a db transaction on the backend)
-- rolls back all optimistic changes if any of the tables can't be updated
-
-This can be all done with optimistic actions:
-
-```ts
-const createTodoItem = createOptimisticAction({
- id: "create-todo-item",
- autoCommit: true,
- onMutate: (newItemData: CreateTodoItemInput) => {
- // This insert statement here will not actually trigger
- // the `onInsert` function we defined when we created
- // the `todoItemsCollection`.
- // It'll only update the local cache optimistically.
- todoItemsCollection.insert({
- createdAt: new Date(),
- ...newItemData,
- priority: 0,
- });
-
- // Just like the `insert()` method above, this will also
- // not run the `onUpdate` function on the `projectsCollection`.
- projectsCollection.update(newItemData.projectId, (project) => {
- project.itemPositionsInTheProject[newItemData.boardId].unshift(
- newItemData.id,
- );
- });
- },
- mutationFn: async (newItemData: CreateTodoItemInput) => {
- // send a fetch request to the server
- await insertTodoItem({
- data: newItemData,
- });
- // We need to manually sync the data back from the server
- await todoItemsCollection.utils.refetch();
- await projectsCollection.utils.refetch();
- },
-});
-```
-
-
-The `insertTodoItem` is basically a `fetch` request to a server endpoint:
-
-```ts
-export async function insertTodoItem({
- data,
-}: {
- data: TodoItemCreateDataType;
-}) {
- const res = await fetch("/api/todo-items", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(data),
- });
-
- if (!res.ok) {
- throw new Error("Failed to insert todo item");
- }
-
- const response: TodoItemRecord = await res.json();
-
- return response;
-}
-```
-
-
-
-> You can check the backend's endpoint in the repo if you're interested.
-
-Now you can use `createTodoItem()` when you want to insert a new todo items and be sure that the two tables will always be in sync with each other:
-
-
-```ts
-createTodoItem({ /* the data of the todo item */ });
-```
-
-
-
-Read more about optimistic actions in the docs.
-
-### More complex mutations
-
-If you need even more control (e.g.: opting out from the optimistic updates, etc.), check out the Mutations page in the docs.
From 251f8aba57f1e40876936065acb50837bf836567 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 11:58:42 +0100
Subject: [PATCH 06/18] Update how-db-collections-work.mdx
---
src/data/tutorial/how-do-collections-work.mdx | 31 ++++++++++---------
1 file changed, 16 insertions(+), 15 deletions(-)
diff --git a/src/data/tutorial/how-do-collections-work.mdx b/src/data/tutorial/how-do-collections-work.mdx
index 4c985bb..a1734f4 100644
--- a/src/data/tutorial/how-do-collections-work.mdx
+++ b/src/data/tutorial/how-do-collections-work.mdx
@@ -6,27 +6,28 @@ import {
## How do collections work?
-By default, collections are synced with the remote source when they are first used in a live query, and every time a mutation occurs (update, insert, delete, etc).
+By default, collections sync with your remote data source when first used in a live query and whenever mutations occur (updates, inserts, deletes, etc.).
-For most use cases, collections live on the front end. They don't know how to fetch or update remote data on their own, so you need to use a sync engine that handles that part.
+When you create a collection, you need to specify how it can sync itself with the server.
+For simple use cases, this can be accomplished through API calls (managed by TanStack Query), but more advanced scenarios (e.g. two-way sync, multiple clients, etc.) might require sync engines like ElectricSQL.
-### Query Collections
+Our todo app is very simple: there's only one user who can edit the projects, so we can use a good ol' REST API to keep our collections in sync.
-_Query Collections_ are using `TanStack Query` (you've probably heard of at least the React lib `@tanstack/react-query`) to sync with your backend.
+### Query Collections
-It's important to note that collections are part of your front-end code. They don't know how to receive or send data to the server themselves, you need a sync engine for that.
+_Query Collections_ are using `TanStack Query` (you've probably heard of the React library `@tanstack/react-query`) to sync with your backend.
Check out the definition of the `projectsCollection` below for an example:
```ts
export const projectsCollection = createCollection(
- // We use `@tanstack/react-query` to sync the local cache with the remote db
+ // We use `@tanstack/react-query` to sync the local cache with the remote database
queryCollectionOptions({
queryKey: ['projects'],
// This is the function that loads all the projects
- // from the remote db and populates the local cache
+ // from the remote database and populates the local cache
queryFn: getProjects,
- getKey: (item) => item.id,
+ getKey: (item) => item.id, // Unique identifier for each item
queryClient: TanstackQuery.getContext().queryClient,
// This function tells tanstack-db what to do when
// an item is updated in the collection
@@ -46,7 +47,7 @@ export const projectsCollection = createCollection(
Using `queryCollection`-s doesn't place any constraints on your backend. **You can use whatever database or language you want!**
-The only thing `queryCollection`-s need are API endpoint it can call in the data fetching method (`queryFn`), and additional endpoints that handle the mutations (`onUpdate`, `onInsert`, `onDelete`, etc).
+The only thing `queryCollection`-s need are API endpoints it can call in the data fetching method (`queryFn`), and additional endpoints that handle the mutations (`onUpdate`, `onInsert`, `onDelete`, etc).
The `projectsCollection`'s `queryFn` (`getProjects`) is basically just a simple fetch request:
@@ -65,15 +66,15 @@ async function getProjects() {
### Uni-directional Data Flow
-TanStack DB implements a uni-directional data flow, where the source of truth is always the remote data source. Even in the case of mutations (e.g.: renaming projects), we've seen that if the mutation failed remotely, the local changes were rolled back automatically.
+TanStack DB implements a uni-directional data flow, where the source of truth is always the remote data source. Even in the case of mutations (e.g. renaming projects), we see that if mutations fail remotely, the local changes are rolled back automatically.

(This image is from the docs.)
-### Other sync engines
+### Other syncing strategies
Sometimes Query Collections are too little or too much for a specific use case. Luckily, there are other ones that can cover these situations:
-- `LocalOnly`: for in-memory data
-- `LocalStorage`: data stored in local storage
-- `Electric`: real-time sync using Electric's sync engine
-- and more!
+- `LocalOnly`: For in-memory data
+- `LocalStorage`: For data stored in local storage
+- `Electric`: For real-time sync using Electric's sync engine
+- And more!
From d59e748c80a31a1b9d5ed516eee12e0c3ab34a0b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 11:59:11 +0100
Subject: [PATCH 07/18] Wording
---
src/data/tutorial/optimistic-updates.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/data/tutorial/optimistic-updates.mdx b/src/data/tutorial/optimistic-updates.mdx
index f86587f..57fe95b 100644
--- a/src/data/tutorial/optimistic-updates.mdx
+++ b/src/data/tutorial/optimistic-updates.mdx
@@ -46,7 +46,7 @@ Let's rename our current project to the name of another existing project (like `
must be unique)
- The changes are immediately rolled back,
- And an error message appears in the top right corner
- - A new `GET` request to `/api/projects` syncs the local state with the remote database (check the _API Requests_ panel)
+ - A new `GET` request to `/api/projects` syncs the local state with the remote database. You can watch this happen in the _API Requests_ panel
The rule is simple: when an update fails, the mutation is rolled back and the collection's state is synced with the remote data source.
From ff0c93c5b341f23718c184e3c22b9a2e1d4838e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 12:23:25 +0100
Subject: [PATCH 08/18] Update what-is-next.mdx
---
src/data/tutorial/what-is-next.mdx | 28 ++++++++++++++++++++--------
1 file changed, 20 insertions(+), 8 deletions(-)
diff --git a/src/data/tutorial/what-is-next.mdx b/src/data/tutorial/what-is-next.mdx
index 63f95aa..f02f280 100644
--- a/src/data/tutorial/what-is-next.mdx
+++ b/src/data/tutorial/what-is-next.mdx
@@ -2,6 +2,7 @@ import {Link} from '@tanstack/react-router'
import {
GHLink,
HighLightComponent,
+ OpenAPIRequestsPanelLink
} from '/src/components/tutorial'
## What's next?
@@ -10,21 +11,32 @@ You might still have a lot of questions.
What if you don't want to download all of your data at once, because your db is too big (tip: use the `on-demand` sync strategy)? How to do complex mutations on multiple collections at once and still have instant rollbacks (tip: use `createOptimisticAction`)? And probably many more.
-Well, that's what the docs are for. They still might be incomplete at places, but the project is moving at a very fast pace and we have some very dedicated folks working on it. In the meanwhile, check out the Discord channel if you have questions.
+## Useful resources
+I recommend checking out:
+- The official Quick Start guide if you want to write some code yourself
+- The Discord channel channel if you have questions
## Play with this project
Until then, you can play with this project.
-The source code is on GitHub, and you can keep monitoring the requests in the Network tab.
+The source code is on GitHub, and you can keep monitoring the requests in the _API Requests_ panel.
-> Note that requests won't show up in the Network tab of the DevTools until they are completed (no PENDING state).
-> This is because currently they are handled locally by a service worker and this implementation comes with this limitation.
+> Keep in mind that this project's architecture is a bit special.
+> We had some special constraints:
+> - the db had to be local to keep the project financially viable and secure
+> - but the API requests had to be observable (for educational purposes).
+> Thanks to these, we ended up having:
+> - a PGLite db on the client (a Postgres instance in WASM)
+> - a service worker that intercepts API requests and forwards them to the API routes that are defined locally (these API routes receive HTTP Requests and give HTTP Responses, just like a normal API would)
+>
+> All of this architectural complexity is specific to this project, and not required by TanStack DB.
-## Check out the official _"Quick Start"_ guide
+## Deep Dive Articles
-If you want get your hands dirty and write some code, you can check out the official Quick Start guide.
+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.
-## Deep Dive Articles
+## Say hi 👋
+If you liked this guide, please star (⭐) the repo on GitHub.
-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.
+I want to make this guide the best resource for beginners to learn TanStack DB, so **if you have any feedback, please don't hold it in!** I'm @notacheetah on X. 🫡
From 7783dd50920c92b4b40ebc64e651cdc1291793f4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 12:40:19 +0100
Subject: [PATCH 09/18] Update what-is-next.mdx
---
src/data/tutorial/what-is-next.mdx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/data/tutorial/what-is-next.mdx b/src/data/tutorial/what-is-next.mdx
index f02f280..bf39dfc 100644
--- a/src/data/tutorial/what-is-next.mdx
+++ b/src/data/tutorial/what-is-next.mdx
@@ -11,12 +11,12 @@ You might still have a lot of questions.
What if you don't want to download all of your data at once, because your db is too big (tip: use the `on-demand` sync strategy)? How to do complex mutations on multiple collections at once and still have instant rollbacks (tip: use `createOptimisticAction`)? And probably many more.
-## Useful resources
+### 📚 Useful resources
I recommend checking out:
- The official Quick Start guide if you want to write some code yourself
- The Discord channel channel if you have questions
-## Play with this project
+### 🛝 Play with this project
Until then, you can play with this project.
@@ -32,11 +32,11 @@ The source code is the repo on GitHub.
I want to make this guide the best resource for beginners to learn TanStack DB, so **if you have any feedback, please don't hold it in!** I'm @notacheetah on X. 🫡
From b36655bf0fa1275d7a6782ab6fda9b16ea5375b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 12:56:34 +0100
Subject: [PATCH 10/18] Make highlighting lines in code blocks possible
---
package.json | 1 +
pnpm-lock.yaml | 11 +++++++++++
src/styles.css | 26 ++++++++++++++++++++++++++
vite.config.ts | 2 ++
4 files changed, 40 insertions(+)
diff --git a/package.json b/package.json
index 2dd16c5..4464c1f 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@shikijs/rehype": "^3.20.0",
+ "@shikijs/transformers": "^3.20.0",
"@tailwindcss/vite": "^4.0.6",
"@tanstack/db": "^0.5.16",
"@tanstack/query-db-collection": "^1.0.12",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cea2722..c2a5503 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -74,6 +74,9 @@ importers:
'@shikijs/rehype':
specifier: ^3.20.0
version: 3.20.0
+ '@shikijs/transformers':
+ specifier: ^3.20.0
+ version: 3.20.0
'@tailwindcss/vite':
specifier: ^4.0.6
version: 4.1.17(vite@7.2.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))
@@ -1881,6 +1884,9 @@ packages:
'@shikijs/themes@3.20.0':
resolution: {integrity: sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==}
+ '@shikijs/transformers@3.20.0':
+ resolution: {integrity: sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g==}
+
'@shikijs/types@3.20.0':
resolution: {integrity: sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==}
@@ -5559,6 +5565,11 @@ snapshots:
dependencies:
'@shikijs/types': 3.20.0
+ '@shikijs/transformers@3.20.0':
+ dependencies:
+ '@shikijs/core': 3.20.0
+ '@shikijs/types': 3.20.0
+
'@shikijs/types@3.20.0':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
diff --git a/src/styles.css b/src/styles.css
index 73601b7..4d764f3 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -51,6 +51,32 @@ html.dark .shiki span {
background-color: var(--shiki-dark-bg) !important;
}
+/* Shiki line highlighting */
+.shiki code {
+ display: grid;
+}
+
+.shiki .line.highlighted {
+ margin-left: -1rem;
+ margin-right: -1rem;
+ padding-left: calc(1rem - 3px);
+ padding-right: 1rem;
+ border-left: 3px solid var(--primary);
+ background-color: oklch(from var(--accent) l c h / 0.3) !important;
+}
+
+html:not(.dark) .shiki .line.highlighted span {
+ background-color: transparent !important;
+}
+
+html.dark .shiki .line.highlighted {
+ background-color: oklch(from var(--accent) l c h / 0.1) !important;
+}
+
+html.dark .shiki .line.highlighted span {
+ background-color: transparent !important;
+}
+
:root {
--background:
oklch(0.9789 0.0082 121.6272);
diff --git a/vite.config.ts b/vite.config.ts
index 1736327..982ae23 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,6 +1,7 @@
import { cloudflare } from "@cloudflare/vite-plugin";
import mdx from "@mdx-js/rollup";
import rehypeShiki from "@shikijs/rehype";
+import { transformerMetaHighlight } from "@shikijs/transformers";
import tailwindcss from "@tailwindcss/vite";
import { devtools } from "@tanstack/devtools-vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
@@ -37,6 +38,7 @@ const config = defineConfig({
},
defaultColor: false,
transformers: [
+ transformerMetaHighlight(),
{
name: "add-language-data-attribute",
pre(node: { properties: Record }) {
From 3f59ec2c33d9e326c1394c2095a81f57e1359ed7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 15:35:44 +0100
Subject: [PATCH 11/18] Fix capitalization
---
src/data/tutorial/how-do-collections-work.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/data/tutorial/how-do-collections-work.mdx b/src/data/tutorial/how-do-collections-work.mdx
index a1734f4..a67875f 100644
--- a/src/data/tutorial/how-do-collections-work.mdx
+++ b/src/data/tutorial/how-do-collections-work.mdx
@@ -64,7 +64,7 @@ async function getProjects() {
```
-### Uni-directional Data Flow
+### Uni-directional data flow
TanStack DB implements a uni-directional data flow, where the source of truth is always the remote data source. Even in the case of mutations (e.g. renaming projects), we see that if mutations fail remotely, the local changes are rolled back automatically.
From 2df47dacc576009a81140625cc6f022b5fd5c8bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 16:15:27 +0100
Subject: [PATCH 12/18] Ensure that empty lines are preserved in code blocks
---
src/styles.css | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/styles.css b/src/styles.css
index 4d764f3..81bf38a 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -56,6 +56,11 @@ html.dark .shiki span {
display: grid;
}
+/* Ensure empty lines are preserved */
+.shiki .line:empty::before {
+ content: " ";
+}
+
.shiki .line.highlighted {
margin-left: -1rem;
margin-right: -1rem;
From 5b8227b3ffc0446b1ff9443086cc37ffc26d4fde Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 17:02:33 +0100
Subject: [PATCH 13/18] Don't format the deep dive articles with prettier
---
.prettierignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.prettierignore b/.prettierignore
index 8ef93e8..6316d7e 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1 +1,2 @@
src/data/tutorial/*.mdx
+src/data/deep-dives/*.mdx
From 9648a0161f85a32d64ab3d0e1f60de9e75e6db25 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 20:21:17 +0100
Subject: [PATCH 14/18] Add the guide about the on-demand sync mode
---
src/collections/todoItems.ts | 2 +-
src/data/deep-dives/query-driven-sync.mdx | 159 ++++++++++++++++++++++
src/data/tutorial.ts | 9 +-
src/data/tutorial/what-is-next.mdx | 5 +-
4 files changed, 168 insertions(+), 7 deletions(-)
create mode 100644 src/data/deep-dives/query-driven-sync.mdx
diff --git a/src/collections/todoItems.ts b/src/collections/todoItems.ts
index 034766b..f3b522e 100644
--- a/src/collections/todoItems.ts
+++ b/src/collections/todoItems.ts
@@ -65,7 +65,7 @@ export const todoItemsCollection = createCollection(
parsed.filters.forEach(({ field, operator, value }) => {
const fieldName = field.join(".");
- // Currently only "eq" operator is supported in the API
+ // Currently only the "eq" operator is supported by our API
if (operator === "eq") {
params.set(fieldName, String(value));
}
diff --git a/src/data/deep-dives/query-driven-sync.mdx b/src/data/deep-dives/query-driven-sync.mdx
new file mode 100644
index 0000000..6bcc6e7
--- /dev/null
+++ b/src/data/deep-dives/query-driven-sync.mdx
@@ -0,0 +1,159 @@
+import {Link} from '@tanstack/react-router'
+import {
+ GHLink,
+ HighLightComponent,
+ LinkToArticle,
+ OpenAPIRequestsPanelLink,
+} from '/src/components/tutorial'
+
+## Query-driven sync
+
+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.
+
+For example:
+
+- instead of loading all the todo items a user has when they visit a project's page,
+- we can just load the ones that belong to that project.
+
+This can be achieved by using the `on-demand` sync mode.
+
+> 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). (source)
+
+### Switching to `"on-demand"`
+
+Every collection's sync strategy is `"eager"` by default, which loads the entire collection upfront. First, we need to change it to `"on-demand"`:
+
+```ts {4}
+export const todoItemsCollection = createCollection(
+ queryCollectionOptions({
+ // ...
+ syncMode: "on-demand",
+ // ...
+ })
+```
+
+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:
+
+```ts {4-11}
+export const todoItemsCollection = createCollection(
+ queryCollectionOptions({
+ // ...
+ syncMode: 'eager',
+ queryFn: async () => {
+ // Fetch all the todo items
+ const res = await fetch('/api/todo-items', {method: 'GET'})
+
+ const todoItems: TodoItemRecord[] = await res.json()
+
+ return todoItems
+ },
+ // ...
+ }),
+)
+```
+
+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.
+
+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`:
+
+```tsx {5}
+const {data: allTodoItems} = useLiveQuery(
+ (q) =>
+ q
+ .from({todoItem: todoItemsCollection})
+ .where(({todoItem}) => eq(todoItem.projectId, projectId))
+ .orderBy(({todoItem}) => todoItem.position, {
+ direction: 'asc',
+ stringSort: 'lexical',
+ }),
+ [projectId],
+)
+```
+
+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`.
+
+```ts {22}
+export const todoItemsCollection = createCollection(
+ queryCollectionOptions({
+ syncMode: 'on-demand',
+ queryFn: async ({meta}) => {
+ const params = new URLSearchParams()
+
+ if (meta) {
+ const {where} = meta.loadSubsetOptions
+ // Parse the expressions into simple format
+ const parsed = parseLoadSubsetOptions({where})
+ // Build query parameters from parsed filters
+ // Add filters
+ parsed.filters.forEach(({field, operator, value}) => {
+ const fieldName = field.join('.')
+ // Currently only the "eq" operator is supported by our API
+ if (operator === 'eq') {
+ params.set(fieldName, String(value))
+ }
+ })
+ }
+
+ const res = await fetch(`/api/todo-items?${params}`, {method: 'GET'})
+ const todoItems: TodoItemRecord[] = await res.json()
+ return todoItems
+ },
+ }),
+)
+```
+
+To observe this behavior
+
+- open the API Request Panel,
+- and [reload this page]()
+
+You should see that the request to `/api/todo-items` has a search string attached to it, similar to this one: `?projectId=aQLI4Nzvmls31l4aep9LqnU`.
+
+### Query predicates
+
+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.
+
+We could use `orderBy` to order elements on the server, `offset` to support pagination and more.
+
+Check out the documentation for more info about them.
+
+### Transitioning from `"eager"` to `"on-demand"`: some pitfalls
+
+When I first refactor the `todoItemsCollection` to use `on-demand` sync, I ran into some issues.
+
+#### `.toArrayWhenReady()` vs `.toArray`
+
+For example, this line of code calling the `.toArrayWhenReady()` method caused the collection to fetch every single todo item from the remote source:
+
+```ts
+// ❌ Get the current state of the collection from the server,
+await todoItemsCollection.toArrayWhenReady()
+```
+
+I had to change it to `toArray`, to use only the data that we already have in the client-side cache:
+
+```ts
+// ✅ Get the current state of the collection from the cache
+await todoItemsCollection.toArray
+```
+
+#### `.refetch()`
+
+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:
+
+```ts
+// ❌ Triggers fetching every todo item in the collection
+todoItemsCollection.utils.refetch();
+```
+
+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.
+
+### Progressive mode
+
+There is a third sync mode, that I haven't mentioned before called `"progressive"`. Here's how it relates to the other two:
+
+- `"eager"`: load all the data in the collection
+- `"on-demand"`: load only the queried data
+- `"progressive"`: load the queried data first, then everything else in the background
+
+You can read more about the sync modes in the documentation.
diff --git a/src/data/tutorial.ts b/src/data/tutorial.ts
index 7ca1763..685fd4c 100644
--- a/src/data/tutorial.ts
+++ b/src/data/tutorial.ts
@@ -1,3 +1,4 @@
+import QueryDrivenSync from "@/data/deep-dives/query-driven-sync.mdx";
import CollectionsIntro from "@/data/tutorial/collections-intro.mdx";
import HowDoCollectionsWork from "@/data/tutorial/how-do-collections-work.mdx";
import OptimisticUpdates from "@/data/tutorial/optimistic-updates.mdx";
@@ -47,10 +48,10 @@ export const tutorialArticles: Step[] = tutorialArticlesWithoutNextSteps.map(
);
const deepDiveArticlesWithoutNextSteps: Step[] = [
- // {
- // title: "Optimistic Actions",
- // file: OptimisticActions,
- // },
+ {
+ title: "Query-driven sync",
+ file: QueryDrivenSync,
+ },
];
// export const deepDiveArticles: Step[] = deepDiveArticlesWithoutNextSteps.map(
diff --git a/src/data/tutorial/what-is-next.mdx b/src/data/tutorial/what-is-next.mdx
index bf39dfc..e3d3c79 100644
--- a/src/data/tutorial/what-is-next.mdx
+++ b/src/data/tutorial/what-is-next.mdx
@@ -2,7 +2,8 @@ import {Link} from '@tanstack/react-router'
import {
GHLink,
HighLightComponent,
- OpenAPIRequestsPanelLink
+ OpenAPIRequestsPanelLink,
+ LinkToArticle
} from '/src/components/tutorial'
## What's next?
@@ -34,7 +35,7 @@ The source code is `on-demand` sync mode). You can view them in the TOC on the left.
### 👋 Say hi
If you liked this guide, please star (⭐) the repo on GitHub.
From 751e53f83155696d5765cd321642b391390b41b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 20:49:56 +0100
Subject: [PATCH 15/18] Udpate the query-driven-sync article
---
src/data/deep-dives/query-driven-sync.mdx | 52 ++++++++++++++++-------
1 file changed, 36 insertions(+), 16 deletions(-)
diff --git a/src/data/deep-dives/query-driven-sync.mdx b/src/data/deep-dives/query-driven-sync.mdx
index 6bcc6e7..fdd808e 100644
--- a/src/data/deep-dives/query-driven-sync.mdx
+++ b/src/data/deep-dives/query-driven-sync.mdx
@@ -8,14 +8,11 @@ import {
## Query-driven sync
-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.
+Sometimes you don't want to load all your data into your collection (e.g. when you have a lot of data), only the subset required by the live queries you're running.
-For example:
+In this demo app, some projects have many more todo items than others. Currently, we load all todo items regardless of which project page the user visits. But what if we could load only items from the currently viewed project?
-- instead of loading all the todo items a user has when they visit a project's page,
-- we can just load the ones that belong to that project.
-
-This can be achieved by using the `on-demand` sync mode.
+Well, this is what the `on-demand` sync mode is for.
> 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). (source)
@@ -54,7 +51,7 @@ export const todoItemsCollection = createCollection(
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.
-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`:
+Instead, 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`:
```tsx {5}
const {data: allTodoItems} = useLiveQuery(
@@ -70,7 +67,7 @@ const {data: allTodoItems} = useLiveQuery(
)
```
-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`.
+The highlighted line in the code block above is the filter we need to use in the database query on the server. Fortunately, when this live query runs, TanStack DB automatically passes all query predicates to the `queryFn`.
```ts {22}
export const todoItemsCollection = createCollection(
@@ -102,35 +99,58 @@ export const todoItemsCollection = createCollection(
)
```
-To observe this behavior
+Now the `queryFn` will only attempt to fetch the todo items whose projectId matches the one in the live query. You can quickly check it yourself:
- open the API Request Panel,
- and [reload this page]()
You should see that the request to `/api/todo-items` has a search string attached to it, similar to this one: `?projectId=aQLI4Nzvmls31l4aep9LqnU`.
+The only thing left to do now is to use the project's id from the search params in the API route's handler:
+```ts {6,9-12}
+export default {
+ GET: async ({ request }) => {
+ const url = new URL(request.url);
+
+ // Currently we only support filtering by projectId
+ const projectId = url.searchParams.get("projectId");
+
+ if (projectId) {
+ const results = await db
+ .select()
+ .from(todoItemsTable)
+ .where(eq(todoItemsTable.projectId, projectId));
+ return json(results);
+ } else {
+ const results = await db.select().from(todoItemsTable);
+ return json(results);
+ }
+ },
+}
+```
+
### Query predicates
-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.
+In this app, we'll stop here, but the predicates (`where` clauses, `orderBy`, `limit`, and `offset`) coming from the queries can cover far more advanced use cases.
-We could use `orderBy` to order elements on the server, `offset` to support pagination and more.
+We could use `orderBy` to order elements on the server, `offset` to support pagination, and more.
Check out the documentation for more info about them.
### Transitioning from `"eager"` to `"on-demand"`: some pitfalls
-When I first refactor the `todoItemsCollection` to use `on-demand` sync, I ran into some issues.
+When I first refactored `todoItemsCollection` to use `on-demand` sync, I ran into some issues.
#### `.toArrayWhenReady()` vs `.toArray`
For example, this line of code calling the `.toArrayWhenReady()` method caused the collection to fetch every single todo item from the remote source:
```ts
-// ❌ Get the current state of the collection from the server,
+// ❌ Get the current state of the collection from the server
await todoItemsCollection.toArrayWhenReady()
```
-I had to change it to `toArray`, to use only the data that we already have in the client-side cache:
+I had to change it to `toArray`, to use only existing data in the client-side cache:
```ts
// ✅ Get the current state of the collection from the cache
@@ -146,11 +166,11 @@ Another line that resulted in loading all the collection data instead of the sub
todoItemsCollection.utils.refetch();
```
-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.
+The key takeaway is that you have to watch out and ensure you don't accidentally sync all the data in the collection when you use `"on-demand"` mode.
### Progressive mode
-There is a third sync mode, that I haven't mentioned before called `"progressive"`. Here's how it relates to the other two:
+There is a third sync mode that I haven't previously mentioned called `"progressive"`. Here's how it relates to the other two:
- `"eager"`: load all the data in the collection
- `"on-demand"`: load only the queried data
From 606e405cc0cad9b169c0326580782f113e3ef98c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 20:58:29 +0100
Subject: [PATCH 16/18] Update the github links in collections-intro
---
src/data/tutorial/collections-intro.mdx | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/src/data/tutorial/collections-intro.mdx b/src/data/tutorial/collections-intro.mdx
index 96a4711..6b7c5df 100644
--- a/src/data/tutorial/collections-intro.mdx
+++ b/src/data/tutorial/collections-intro.mdx
@@ -22,18 +22,17 @@ In the `AppSidebar` component, we use this query to get the list of projects:
}),
);
```
-
+Highlight `AppSidebar`
-On the project's page, the `RouteComponent` loads only the data relevant for the currently viewed project:
+On the project's page, the `EditableProjectDetails` component loads only the data relevant for the currently viewed project:
```tsx
const {
data: [project],
} = useLiveQuery(
- (q) =>
- q
+ (q) => q
.from({ project: projectsCollection })
.where(({ project }) => eq(project.id, projectId)),
// This hook has a dependency array as seen below.
@@ -42,7 +41,7 @@ On the project's page, the `RouteComponent` loads only the data relevant for the
[projectId],
);
```
-
+Highlight `RouteComponent`
From a8405a3636e5bb505fdffb18c817ff6ce56aa043 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 21:13:44 +0100
Subject: [PATCH 17/18] Fix loading steps from the search params
---
src/components/tutorial/TutorialWindow.tsx | 22 +++++++++++++++++++---
1 file changed, 19 insertions(+), 3 deletions(-)
diff --git a/src/components/tutorial/TutorialWindow.tsx b/src/components/tutorial/TutorialWindow.tsx
index c595ee4..2664f3f 100644
--- a/src/components/tutorial/TutorialWindow.tsx
+++ b/src/components/tutorial/TutorialWindow.tsx
@@ -486,6 +486,7 @@ export function TutorialWindow({
const [isClosed, setIsClosed] = useState(tutorialData.isClosed);
const { article: activeArticleFromSearch } = useSearch({ strict: false });
+ const navigate = useNavigate();
const [activeStep, setActiveStep] = useState(
tutorialData.tutorialStep || articles[0].title,
@@ -498,13 +499,28 @@ export function TutorialWindow({
) {
const articleInSearch = decodeURIComponent(
activeArticleFromSearch.toLowerCase(),
+ ).trim();
+
+ console.info({
+ articleInSearch,
+ });
+
+ const article = articles.find(
+ (a) => a.title.toLowerCase().trim() === articleInSearch,
);
- if (articles.find((a) => a.title === articleInSearch)) {
- setActiveStep(articleInSearch);
+ if (article) {
+ setActiveStep(article.title);
+ navigate({
+ to: ".",
+ search: ({ article: _, ...old }) => {
+ return { ...old };
+ },
+ replace: true,
+ });
}
}
- }, [activeArticleFromSearch]);
+ }, [activeArticleFromSearch, navigate]);
const router = useRouter();
From cfabc321640899fc4ccb9399adb7462414f08d05 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=BCl=C3=B6p=20Kov=C3=A1cs?=
<43729152+fulopkovacs@users.noreply.github.com>
Date: Sun, 4 Jan 2026 21:31:49 +0100
Subject: [PATCH 18/18] Remove a console.info
---
src/components/tutorial/TutorialWindow.tsx | 4 ----
1 file changed, 4 deletions(-)
diff --git a/src/components/tutorial/TutorialWindow.tsx b/src/components/tutorial/TutorialWindow.tsx
index 2664f3f..611aaef 100644
--- a/src/components/tutorial/TutorialWindow.tsx
+++ b/src/components/tutorial/TutorialWindow.tsx
@@ -501,10 +501,6 @@ export function TutorialWindow({
activeArticleFromSearch.toLowerCase(),
).trim();
- console.info({
- articleInSearch,
- });
-
const article = articles.find(
(a) => a.title.toLowerCase().trim() === articleInSearch,
);