Skip to content

Replace RequestCache with TanStack Query#269

Merged
jennings merged 10 commits into
mainfrom
react-query
Jun 25, 2026
Merged

Replace RequestCache with TanStack Query#269
jennings merged 10 commits into
mainfrom
react-query

Conversation

@jennings

Copy link
Copy Markdown
Owner

I keep finding little inconsistencies where not all data on the screen updates at once. I think it's better if we just use TanStack Query to handle data fetching and caching.

Add TanStack Query (v5) to replace the hand-rolled RequestCache for mutable
data over time. If a query is marked with meta: { persist: true } then the key will be persisted between page loads. This is done for the data that RequestCache was previously storing in localStorage.

This does not replace immutable data that we were already storing in IndexedDB. That is untouched and remains outside of TanStack Query. It might be worthwhile to fetch even this data via TanStack Query for loading state, etc., but that can be done as a separate effort.

Resolves #259

jennings added 10 commits June 24, 2026 17:05
Add TanStack Query (v5) to replace the hand-rolled RequestCache for mutable
data over time. This commit is plumbing only — no other call sites are
migrated.

Changes:
- Install @tanstack/react-query, react-query-devtools,
  react-query-persist-client, query-async-storage-persister
- src/browser/lib/query-client.ts — single QueryClient (staleTime=30s,
  gcTime=5min, no refetch-on-focus) + raw IDB persister in a dedicated
  'pulldash-rq/data' store, separate from PersistentCache ('pulldash/responses'),
  using the same raw IDB pattern already in persistent-cache.ts (no idb-keyval)
- src/browser/lib/github-client.ts — module-level setOctokit/getOctokit so
  query functions can reach the authenticated Octokit without being inside the
  GitHub context; set by GitHubProvider on auth, cleared on sign-out
- src/browser/lib/queries.ts — queryOptions factory registry with conventions
  documented in the header comment; currentUser query as smoke test
  (meta: { persist: true }, staleTime: 5 min)
- src/browser/index.tsx — wrap app root with PersistQueryClientProvider;
  dehydrateOptions filters to meta.persist===true; ReactQueryDevtools
  mounted only in dev builds (__DEV__ define)
- scripts/build-browser.ts — add __DEV__ define (true when --watch)
- src/types/assets.d.ts — declare const __DEV__: boolean
- Migrate user:current from RequestCache to React Query — remove
  fetchCurrentUser() and currentUser from GitHubState; useCurrentUser() now
  uses useQuery(queries.currentUser()) enabled by ready; useCurrentUserLoader
  updated to use the hook

Resolves #260
Six functions in github.tsx that cache content-addressed (SHA-keyed) data
were using RequestCache (in-memory + localStorage) as their primary store,
falling back to PersistentCache only when prKey was provided. Since these
responses are immutable, they belong in IndexedDB permanently.

Changes:
- getCommitFiles, getSingleCommit, getRawGitCommit, getMergeCommitFiles,
  getRawCompareDiff, getPRFilesForRange, getCommitsForHeadSha:
  - Remove cache.get/set/getPending/setPending calls
  - Check PersistentCache.get first (always, not just when prKey given)
  - Use a per-store Map<string, Promise> (immutablePending) for in-flight
    request deduplication only; cleared on settle
  - Write to PersistentCache when prKey is available

- pr-review/index.tsx: four getPRFilesForRange callers that omitted prKey
  now pass `${owner}/${repo}/${pr.number}` so results are persisted

Existing cache keys are preserved so on-disk entries from previous sessions
remain valid without migration.

Resolves #261
getPRChecksForSha and getWorkflowRunsForSha were using RequestCache
(in-memory + localStorage) with a 15s TTL. They're mutable (status
transitions pending→success/failure), so they go through React Query.

Changes:
- queries.ts: add checksByCommit(owner, repo, sha) and
  workflowRunsByCommit(owner, repo, sha) factories, staleTime: 15_000,
  no meta.persist
- github.tsx: replace the RequestCache-based implementations of
  getPRChecksForSha / getWorkflowRunsForSha with one-line wrappers
  around queryClient.fetchQuery(); all 8 callers (fetchPRChecks plus
  7 direct calls in pr-review/index.tsx) transparently go through
  React Query now
- approveWorkflowRun: replace cache.invalidate('workflow-runs:...') with
  queryClient.invalidateQueries({ queryKey: ['workflow-runs', owner, repo] });
  partial key match invalidates all SHAs for this repo, preserving the
  previous prefix-match behavior

Resolves #262
Both are slow-changing, read-only from the app, and good first uses of the
meta.persist path (cross-session IndexedDB persistence).

Changes:
- queries.ts: add collaborators(owner, repo) and labels(owner, repo)
  factories, staleTime: 5 min, meta: { persist: true }
- github.tsx: getRepoCollaborators and getRepoLabels become one-line
  wrappers around queryClient.fetchQuery()
- pr-overview.tsx:
  - PROverview: replace useState + fetchCollaborators with
    useQuery(queries.collaborators), drop the manual fetch-on-picker-open
    pattern; collaborator data is now pre-fetched and cached eagerly
  - LabelsSection: replace useState + fetchLabels with
    useQuery(queries.labels), same pattern

Resolves #263
Replace the hand-rolled fetchPRList / searchPRs / searchRepos / searchUsers
functions (which all used RequestCache internally) with four new queryOptions
factories in queries.ts:

  - queries.prList(queryStrings, page, perPage) — compound query that runs
    all search strings in parallel, deduplicates, sorts, and enriches via a
    single GraphQL call. placeholderData: keepPreviousData prevents pagination
    flicker.

  - queries.searchPRs(query, page, perPage) — thin wrapper used by the store
    for the fetchInvolvedPRs helper.

  - queries.searchRepos(query) — used imperatively in the home repo-search
    dropdown.

  - queries.searchUsers(query) — used in the markdown @-mention autocomplete.

The prList enrichment logic (GraphQL PR metadata batch) is inlined into the
queryFn in queries.ts, calling getOctokit().graphql() directly rather than
routing through the store's GraphQLBatcher (the batcher was only a 5ms
micro-optimisation, unnecessary inside a queryFn).

home.tsx now derives its data from useQuery instead of useSyncExternalStore:

  const { data, isFetching, isPending, dataUpdatedAt } =
    useQuery({ ...queries.prList(searchQueries, page, perPage), enabled: ready });

RefreshCountdown receives dataUpdatedAt (a React Query timestamp) instead of
the old PRListState.lastFetchedAt. The manual 60-second setInterval auto-
refresh is removed; React Query's staleTime (30 s) + refetchOnWindowFocus
covers the same job.

invalidatePRCaches() in pr-review/index.tsx now also invalidates
["pr-list"] and ["search", "prs"] so the home page reflects PR state
changes (close / reopen / merge / enqueue) immediately.

GitHubState loses prList, prListQueries, and prListPage; the reset() function
calls queryClient.removeQueries(["pr-list"]) instead. fetchPRList,
refreshPRList, usePRList, and usePRListActions are removed.

Resolves #264
… Query

Replace RequestCache implementations for PR object, files, commits,
push versions, and user profile lookups with React Query factories.
Mutations that change PR header state now invalidate the
["pull-request", owner, repo, number] subtree (which covers files,
commits, push versions) and ["pr-list"]; the broad pattern-based
cache.invalidate still runs for the remaining RequestCache entries
(comments, reviews, conversation, timeline) that move in ticket #267.

PATCH /pulls responses (closePR, reopenPR, updatePR) prime the PR
query cache before invalidation so the UI shows the new state
immediately.

Progresses: #259
Resolves #265
Migrates the bulk of mutated PR data to React Query factories under
the ["pull-request", ...] and ["reactions", ...] key hierarchies:

- pullRequestComments, pullRequestReviews
- pullRequestConversation, pullRequestTimeline
- issueReactions, commentReactions, reviewCommentReactions

Reaction mutations now target their specific query key; comment and
conversation mutations invalidate just their subtree. PR-level
mutations continue to call invalidatePR, which prefix-matches the
entire ["pull-request", owner, repo, number] subtree — so a single
invalidate picks up files, commits, push-versions, comments, reviews,
conversation, and timeline at once.

Progresses: #259
Resolves #266
All call sites pass a SHA ref + prKey, so the RequestCache layer
was redundant — every fetched file is content-addressed and
immutable. Route the function through PersistentCache directly,
using the immutablePending map for in-flight dedup (matching
getSingleCommit / getCommitFiles).

Last mutable RequestCache reader; the class itself goes away in #268.

Progresses: #259
Resolves #267
All mutable reads now flow through React Query; the SHA-keyed
immutable ones live in PersistentCache. Delete the RequestCache
class, its gh_cache:* localStorage hydration, the auth-flow shim
that cleared those keys, and the dead caches.delete("pulldash-v1")
service-worker reference.

The public invalidateCache(pattern) API is replaced by a typed
invalidatePR(owner, repo, number) helper — every caller used the
same pr:owner/repo/number pattern. Logout's full-flush is now
queryClient.clear() in reset(), matching how React Query owns
the cache.

GraphQLBatcher is untouched — it batches at the transport layer.

Resolves #268
@jennings jennings merged commit 0a07f1f into main Jun 25, 2026
6 checks passed
@jennings jennings deleted the react-query branch June 25, 2026 06:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate mutable data fetches to React Query

1 participant