# Implementing Optimistic UI Updates with useDbSync Polling in Agent-Native

> Learn to implement optimistic UI updates in Agent-Native using useDbSync polling and TanStack Query. Achieve instant local state changes and prevent stale data overwrites with version cursors.

- Repository: [Builder.io/agent-native](https://github.com/BuilderIO/agent-native)
- Tags: how-to-guide
- Published: 2026-06-27

---

**Agent-Native enables optimistic UI updates by combining a shared SSE/polling transport in `useDbSync` with TanStack Query's cache management, allowing immediate local state mutations while preventing stale poll responses from overwriting fresh data through version cursors and query cancellation.**

Agent-Native is an open-source framework for building real-time applications with synchronized state. When implementing **optimistic UI updates with useDbSync polling**, developers can apply local changes immediately while relying on a robust transport layer to reconcile server state without visual flicker or regression.

## How useDbSync Manages Real-Time Synchronization

The `useDbSync` hook in [`packages/core/src/client/use-db-sync.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts) creates a unified transport layer that merges Server-Sent Events (SSE) with intelligent polling to keep client UI synchronized with the server-side database.

### Singleton Transport Architecture

At the module level, `useDbSync` maintains a `transportRegistry` that stores one `SyncTransport` instance per unique `<pollUrl>\0<sseUrl>` pair ([source](packages/core/src/client/use-db-sync.ts#L22-L33)). This singleton pattern ensures that all hook instances in the same browser tab reuse a single SSE connection and poll loop, regardless of how many components invoke the hook.

Each call to `useDbSync` registers a subscriber identified by a unique `Symbol`. The transport aggregates subscriber preferences—including poll interval, tab-hidden behavior, and fallback intervals—to compute an `effectiveInterval` based on the fastest caller's requirements ([source](packages/core/src/client/use-db-sync.ts#L35-L48)).

### Version Cursor and Event Delivery

The transport tracks a global `versionRef` that advances with every batch of events from the server. This cursor is sent as the `?since=` query parameter during poll requests, ensuring only new events are returned ([source](packages/core/src/client/use-db-sync.ts#L34-L44)).

When data arrives via SSE (the fast path to `/_agent-native/events`) or fallback polling (`/_agent-native/poll`), the transport normalizes the payload through `normalizeEventPayload` and fans out events to all subscribers via their `onEvents` callbacks ([source](packages/core/src/client/use-db-sync.ts#L84-L91)). If SSE is unavailable or the tab is hidden, the transport automatically falls back to polling, capped by the `effectiveInterval` and protected by a `POLL_ABORT_MIN_MS` timeout to prevent hanging requests ([source](packages/core/src/client/use-db-sync.ts#L55-L60)).

### Per-Subscriber Freshness Guarantees

Each subscriber maintains its own `subscriberVersion` cursor. When `onEvents` executes, it discards any events older than the subscriber's current version, preventing stale poll responses from overwriting newer UI state ([source](packages/core/src/client/use-db-sync.ts#L107-L127)).

## The Optimistic UI Pattern

Implementing optimistic updates in Agent-Native requires orchestrating local cache mutations with the transport's event stream to prevent race conditions.

### Step 1: Cancel In-Flight Polls

Before writing locally, cancel any ongoing queries to stop in-flight polls that could return stale data and revert the optimistic UI. In [`templates/slides/app/hooks/use-sidebar-collapsed.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/app/hooks/use-sidebar-collapsed.ts), the pattern calls `queryClient.cancelQueries({ queryKey })` immediately before mutation ([source](templates/slides/app/hooks/use-sidebar-collapsed.ts#L78-L82)).

### Step 2: Mutate Local Cache

Update the TanStack Query cache optimistically using `queryClient.setQueryData`, and persist the value to best-effort storage like `localStorage` for fast reloads ([source](templates/slides/app/hooks/use-sidebar-collapsed.ts#L86-L89)).

### Step 3: Send Remote Mutation

Fire the REST request with `keepalive: true` so it survives page unloads. Include the `X-Request-Source` header set to the tab's `TAB_ID` (generated in [`templates/slides/app/lib/tab-id.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/app/lib/tab-id.ts)) so the server can tag events with the originating tab ([source](templates/slides/app/hooks/use-sidebar-collapsed.ts#L90-L96)).

### Step 4: Handle Failures Gracefully

If the request fails, invalidate the query to trigger a refetch of the authoritative state. The `useDbSync` transport will then re-emit the latest events, correcting the UI ([source](templates/slides/app/hooks/use-sidebar-collapsed.ts#L97-L100)).

### Step 5: Filter Echo Events

Pass the `ignoreSource` option to `useDbSync` to filter out events tagged with the current tab's `TAB_ID`. This prevents the hook from processing its own writes twice ([source](packages/core/src/client/use-db-sync.ts#L82-L84)).

## Complete Implementation Example

The following implementation from [`templates/slides/app/hooks/use-sidebar-collapsed.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/app/hooks/use-sidebar-collapsed.ts) demonstrates the complete optimistic UI pattern:

```typescript
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";
import { TAB_ID } from "@/lib/tab-id";

const KEY = "sidebarCollapsed";
const URL = `/_agent-native/application-state/${KEY}`;
const QUERY_KEY = ["app-state", KEY] as const;

export function useSidebarCollapsed() {
  const qc = useQueryClient();

  const { data } = useQuery<boolean>({
    queryKey: QUERY_KEY,
    queryFn: async () => {
      const res = await fetch(URL);
      const { collapsed } = await res.json();
      return Boolean(collapsed);
    },
    staleTime: 0,
  });

  const setCollapsed = useCallback(
    async (next: boolean | ((prev: boolean) => boolean)) => {
      // 1. Stop any in-flight poll that could revert our optimistic UI.
      await qc.cancelQueries({ queryKey: QUERY_KEY });

      // 2. Compute the new value and update the cache optimistically.
      const prev = qc.getQueryData<boolean>(QUERY_KEY) ?? false;
      const nextVal = typeof next === "function" ? next(prev) : next;
      qc.setQueryData(QUERY_KEY, nextVal);

      // 3. Fire the remote mutation, tagging the request with the tab ID.
      fetch(URL, {
        method: "PUT",
        keepalive: true,
        headers: {
          "Content-Type": "application/json",
          "X-Request-Source": TAB_ID,
        },
        body: JSON.stringify({ collapsed: nextVal }),
      }).catch(() => {
        // 4. If it fails, let the poll/useDbSync pull the authoritative state.
        qc.invalidateQueries({ queryKey: QUERY_KEY });
      });
    },
    [qc],
  );

  return { collapsed: data ?? false, setCollapsed };
}

```

This hook uses `useQuery` for the initial fetch, then applies the optimistic pattern. Elsewhere in the application, `useDbSync` (configured with `ignoreSource: TAB_ID`) receives change events and updates the query cache, ensuring cross-tab consistency without visual flicker.

## Why This Architecture Works

**Single Source of Truth**: The server-side database remains the ultimate authority. `useDbSync` guarantees that every client receives every change event in version order, whether via SSE or poll.

**No Stale Overwrites**: By cancelling polls and tracking per-subscriber version cursors in [`packages/core/src/client/use-db-sync.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts), optimistic UI changes survive transient network latency without being overwritten by delayed poll responses.

**Low Network Traffic**: The transport only polls when SSE is unavailable, using the minimum interval among all subscribers to reduce unnecessary requests. The `POLL_ABORT_MIN_MS` timeout prevents resource exhaustion from hanging requests, as configured in [`packages/core/src/client/route-state.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/route-state.ts).

**Cross-Tab Consistency**: Because the transport is a singleton per browser tab and tags requests with `TAB_ID` from [`templates/slides/app/lib/tab-id.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/app/lib/tab-id.ts), multiple components share the same event stream while correctly filtering their own mutations. For advanced synchronization, [`packages/core/src/client/use-change-version.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-change-version.ts) provides helpers that bump per-source change counters when queries embed `useChangeVersion(source)`.

## Summary

- **`useDbSync`** creates a singleton SSE/polling transport per tab that fans out database events to all subscribers.
- **Optimistic updates** require cancelling in-flight queries before mutating local state to prevent race conditions.
- **Version cursors** (`versionRef` and `subscriberVersion`) ensure events are applied in order and stale responses are discarded.
- **Request tagging** with `X-Request-Source` and the `ignoreSource` option prevent duplicate processing of the tab's own mutations.
- **Graceful degradation** via `keepalive` requests and invalidation on error ensures the UI eventually converges to the server state.

## Frequently Asked Questions

### How does useDbSync prevent stale poll responses from overwriting optimistic updates?

Each subscriber maintains a `subscriberVersion` cursor that tracks the latest event it has processed. When a poll response arrives, `useDbSync` compares event versions against this cursor in [`packages/core/src/client/use-db-sync.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts) (lines 107-127), discarding any events older than the subscriber's current state. This ensures that optimistic updates applied locally are never reverted by delayed network responses.

### What is the purpose of the X-Request-Source header in optimistic mutations?

The `X-Request-Source` header contains the tab's unique `TAB_ID` (defined in [`templates/slides/app/lib/tab-id.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/app/lib/tab-id.ts)), allowing the server to tag change events with their originating tab. When `useDbSync` is configured with the `ignoreSource` option, it filters out events matching the current tab's ID, preventing the hook from processing its own writes twice and creating visual flicker.

### Why must I cancel queries before performing optimistic updates?

Cancelling in-flight queries with `queryClient.cancelQueries` stops active poll requests that could return stale data after you've applied the optimistic mutation. As shown in [`templates/slides/app/hooks/use-sidebar-collapsed.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/app/hooks/use-sidebar-collapsed.ts) (lines 78-82), this prevents race conditions where a delayed poll response would otherwise overwrite your fresh optimistic state with old server data.

### How does the transport handle multiple components using useDbSync simultaneously?

`useDbSync` uses a module-level `transportRegistry` to create only one `SyncTransport` per unique URL pair, shared across all hook instances in the tab ([source](packages/core/src/client/use-db-sync.ts#L22-33)). The transport computes an `effectiveInterval` based on the fastest subscriber's requirements, ensuring efficient polling while delivering events to all registered callbacks via the `onEvents` method.