Skip to content

The useOutages hydration writes query data before the query is active: setQueryData races the first fetch and can be overwritten #391

Description

@usmanimamu17-create

Problem

In useOutages (src/features/outages/hooks/useOutages.ts), the IndexedDB hydration effect and the useQuery run concurrently on mount. The hydration does:

persistedCache.get(cacheKeyStr).then((cached) => {
  if (cached && cached.items.length > 0) {
    const existing = queryClient.getQueryData(queryKey);
    if (!existing || existing.items.length === 0) {
      queryClient.setQueryData(queryKey, cached);
    }
  }
});

getQueryData returns undefined until the query mounts/observes, so the guard "only set if nothing exists" often evaluates undefined — the hydration writes cached data, and then the query's own first fetch resolves and overwrites it (React Query's setQueryData vs. query state: the fetch result replaces it). Consequences:

  • The offline-first fast path rarely shows: on a normal (online) mount, the cached data is written and immediately replaced by the network fetch — users see the loading state anyway, so the whole hydration layer provides no visible benefit online and only helps when the fetch is slow.
  • The race is order-dependent and untested: whether the user sees cached data depends on whether IndexedDB resolves before or after the fetch; there is no test pinning either outcome.
  • The guard's intent (don't clobber fresher data) is defeated because the freshness comparison is against a not-yet-mounted query.

Root cause

The hydration effect was written without coordinating with the query's lifecycle (no useQuery-level initialData/placeholderData hook-in).

Why this is architecturally hard

  1. The correct integration is React Query's initialData/placeholderData with a hydration promise (or queryClient.fetchQuery with the cache read inside the queryFn), which makes the cache read part of the query's data flow instead of a racing side effect — a structural change to the hook.
  2. The hydratedRef one-shot behavior (companion issue) interacts: the hydration must still happen once, but the write must land in the query state, not the global cache, at the right time.
  3. Tests must simulate both orderings (cache resolves before/after fetch) and assert the user-visible outcome — a test surface that does not exist today.

Proposed design

Move the cache read into the query's data flow (e.g. read in queryFn first, fall back to network, or use placeholderData), eliminating the race, and add tests for both resolution orderings asserting the user sees cached data without a fetch when offline.

Acceptance criteria

Service

  • Cached data displays without a flash of loading when the network is slow/unavailable.
  • A successful fetch still replaces the cached data.

Tests

  • A test asserts cached-first rendering in both resolution orderings.
  • Existing offline-cache tests pass.

Out of scope

Schema versioning and empty-result caching (tracked separately).

Getting started

npm test

Good first files to read: src/features/outages/hooks/useOutages.ts.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardStellar Waveissue-trackingThird CampaignCampaign: Third Campaignarea/offlineImported campaign issue labelpriority/mediumImported campaign issue label

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions