Challenge: Multi-tab Sync Dashboard

Learn how to build a small dashboard where two tabs share the same server state, handle writes, and stay consistent without refetching everything.

Problem statement

We are building a multi-tab dashboard that renders two independent app instances, Tab A and Tab B, on the same page to simulate two browser tabs. Each tab uses its own QueryClient, which means each tab maintains an isolated cache and can drift out of sync after a write.

When a write happens in one tab, that tab should broadcast an event. The other tab listens for that event and converges by invalidating only the queries whose server truth may have changed. This should be selective invalidation, not “invalidate everything,” and refetching should happen only for active or visible queries to avoid unnecessary network and UI churn.

Success criteria

With the architecture in place, the following success criteria confirm that the dashboard behaves like a production app and that cache coordination works correctly across both tabs.

  • Tab A and Tab B each has its own cache but views the same server data.

  • Selecting an account triggers dependent queries (details + activity feed).

  • The activity feed uses infinite queries and can load more pages.

  • “Pin” toggling is an optimistic mutation with rollback on error.

  • After a mutation, we perform selective invalidation (not global refetch).

  • A mutation in one tab invalidates the other tab selectively via a BroadcastChannel .

  • Conditional refetching: when a panel is hidden/unmounted, invalidation should not cause visible churn; it should refetch when remounted.

Technical requirements: Implement the following features step by step:

  • Task 1: Two independent tabs (two caches)
    Create two separate QueryClient instances and render two dashboard panels, Tab A and Tab B. Each tab should run as an independent app instance with its own cache, while both read from the same server functions.

  • Task 2: Cache-shaped reads
    Add an ["accounts"] list query and renders the accounts in each tab. Selecting an account should update the local selection state per tab. Tab A and Tab B can select different accounts without affecting each other.

  • Task 3: Dependent queries
    When an account is selected, enable two account-scoped queries:

    • ["account", accountId] for account summary/details

    • ["activity", accountId] as an infinite query for the activity feed
      These queries should be disabled until accountId is available.

  • Task 4: Infinite queries and pagination
    Render activity items from data.pages and flatten the results for display. Add a Load more button that calls fetchNextPage() and disables itself while the next page is loading.

  • Task 5: Mutations, optimistic updates, selective invalidation, and cross-tab sync
    Add a Pin toggle for activity items with the following behavior:

    • Apply an optimistic update to the cached ["activity", accountId] pages so the UI updates immediately

    • Roll back the cache if the mutation fails

    • After success, invalidate only ["account", accountId] to refresh derived fields (such as pinned counts)

    • Broadcast a message so the other tab selectively invalidates only the affected query keys, not the entire cache

Project structure

Below is the hierarchy of the project files and folders: