···11+---
22+title: Live Mode
33+description: How we leverage TanStack Infinite Query for implementing live mode
44+author:
55+ name: Maximilian Kaske
66+ url: https://x.com/mxkaske
77+ avatar: /assets/authors/max.png
88+publishedAt: 2025-03-15
99+image: /assets/posts/live-mode-infinite-query/tanstack.png
1010+tag: engineering
1111+---
1212+1313+While TanStack provides excellent [documentation on Infinite Queries](https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries), this article offers an additional practical example focusing on implementing live data updates.
1414+1515+## Basic Concept
1616+1717+Infinite queries work with "pages" of data. Each time you load new data, a new "page" is either appended or prepended to the `data.pages` array. To get a flat list of all data in the correct order, you simply use `flatMap`:
1818+1919+```ts
2020+const { data, fetchNextPage, fetchPreviousPage } =
2121+ useInfiniteQuery(dataOptions);
2222+2323+const flatData = React.useMemo(
2424+ () => data?.pages?.flatMap((page) => page.data ?? []) ?? [],
2525+ [data?.pages],
2626+);
2727+```
2828+2929+
3030+3131+Once we understand the _load more_ (append data) functionality, we can mirror this approach to implement _live mode_ (prepend data).
3232+3333+Each query requires two key parameters:
3434+3535+1. A `cursor` - a pointer indicating a position in the dataset
3636+2. A `direction` - specifying whether to fetch data before or after the cursor ("prev" or "next")
3737+3838+In our implementation, the `cursor` is a timestamp representing the last checked date:
3939+4040+```ts
4141+const dataOptions = {
4242+ queryKey: "data-table",
4343+ queryFn: async ({ pageParam }) => {
4444+ const { cursor, direction } = pageParam;
4545+ const res = await fetch(
4646+ `/api/get/data?cursor=${cursor}&direction=${direction}`,
4747+ );
4848+ const json = await res.json();
4949+ // For direction "next": { data: [...], nextCursor: 1741526294, prevCursor: null }
5050+ // For direction "prev": { data: [...], nextCursor: null, prevCursor: 1741526295 }
5151+ return json as ReturnType;
5252+ },
5353+ // Initialize with current timestamp for "load more" functionality
5454+ initialPageParam: { cursor: new Date().getTime(), direction: "next" },
5555+ getPreviousPageParam: (firstPage, allPages) => {
5656+ if (!firstPage.prevCursor) return null;
5757+ return { cursor: firstPage.prevCursor, direction: "prev" };
5858+ },
5959+ getNextPageParam: (lastPage, allPages) => {
6060+ if (!lastPage.nextCursor) return null;
6161+ return { cursor: lastPage.nextCursor, direction: "next" };
6262+ },
6363+};
6464+```
6565+6666+The `getPreviousPageParam` and `getNextPageParam` functions receive the first and last pages respectively as their first parameter. This allows us to access the `prevCursor` and `nextCursor` values to track our position when loading more items or checking for updates in live mode.
6767+6868+TanStack provides helpful states like `isFetchingNextPage` and `isFetchingPreviousPage` for loading indicators, as well as `hasNextPage` and `hasPreviousPage` to check for available pages - especially useful for as we can hit the end of the load more values.
6969+7070+Your API endpoint should return at minimum:
7171+7272+```ts
7373+type ReturnType<T> = {
7474+ data: T[];
7575+ nextCursor?: number | null;
7676+ prevCursor?: number | null;
7777+};
7878+```
7979+8080+When fetching older pages ("next" direction), we set a `LIMIT` (e.g., 40 items). However, when fetching newer data ("prev" direction), we return all data between the `prevCursor` and `Date.now()`.
8181+8282+Example API implementation:
8383+8484+```ts
8585+export async function GET(req: NextRequest) {
8686+ const searchParams = request.nextUrl.searchParams;
8787+ const cursor = searchParams.get("cursor");
8888+ const direction = searchParams.get("direction");
8989+9090+ // Live mode
9191+ if (direction === "prev") {
9292+ const prevCursor = Date.now();
9393+ const data = await sql`
9494+ SELECT * FROM table
9595+ WHERE timestamp > ${cursor} AND timestamp <= ${prevCursor}
9696+ ORDER BY timestamp DESC
9797+ `;
9898+ const res: ReturnType<MyData> = { data, prevCursor, nextCursor: null };
9999+ return Response.json(res);
100100+ // Load more
101101+ } else {
102102+ const data = await sql`
103103+ SELECT * FROM table
104104+ WHERE timestamp < ${cursor}
105105+ ORDER BY timestamp DESC
106106+ LIMIT 40
107107+ `;
108108+ const nextCursor = data.length > 0 ? data[data.length - 1].timestamp : null;
109109+ const res: ReturnType<MyData> = { data, nextCursor, prevCursor: null };
110110+ return Response.json(res);
111111+ }
112112+}
113113+```
114114+115115+Key points:
116116+117117+- Live mode ("prev" direction): Returns all new data between `Date.now()` and the `cursor`
118118+- Load more ("next" direction): Returns 40 items before the `cursor` and updates `nextCursor`
119119+120120+> Important: Be careful with timestamp boundaries. If items share the same timestamp, you might miss data because of the `>` comparison. To prevent data loss, include all items sharing the same timestamp as the last item in your query.
121121+122122+### Avoiding OFFSET with Frequent Data Updates
123123+124124+While it might be tempting to use `OFFSET` for pagination (without having live mode active), this approach can cause problems when data is frequently prepended:
125125+126126+```ts
127127+const data = await sql`
128128+ SELECT * FROM table
129129+ ORDER BY timestamp DESC
130130+ LIMIT ${limit}
131131+ OFFSET ${offset}
132132+`;
133133+```
134134+135135+
136136+137137+When new items are prepended, they shift the offset values, potentially causing duplicate items in subsequent queries.
138138+139139+### Implementing Auto-Refresh
140140+141141+While TanStack Query provides a `refetchInterval` option, it would refetch all pages, growing increasingly expensive as more pages are loaded. Instead, we implement a custom refresh mechanism for fetching only new data:
142142+143143+```tsx
144144+const REFRESH_INTERVAL = 5_000; // 5 seconds
145145+146146+React.useEffect(() => {
147147+ let timeoutId: NodeJS.Timeout;
148148+149149+ async function fetchData() {
150150+ // isLive is a simple boolean from React.useState<boolean>()
151151+ // or nuqs useQueryState("live", parseAsBoolean)
152152+ if (isLive) {
153153+ await fetchPreviousPage?.();
154154+ timeoutId = setTimeout(fetchData, REFRESH_INTERVAL);
155155+ } else {
156156+ clearTimeout(timeoutId);
157157+ }
158158+ }
159159+160160+ fetchData();
161161+162162+ return () => {
163163+ clearTimeout(timeoutId);
164164+ };
165165+}, [isLive, fetchPreviousPage]);
166166+```
167167+168168+We use `setTimeout` with recursion rather than `setInterval` to ensure each refresh only starts after the previous one completes. This prevents multiple simultaneous fetches when network latency exceeds the refresh interval and is a better UX.
169169+170170+For more details about our data table implementation, check out [The React data-table I always wanted](https://www.openstatus.dev/blog/data-table-redesign).