refactor(web): use router context for preloadQuery and shared repo data

Quentin Gliech and Claude Opus 4.6 (1M context) created

provide preloadQuery via createRootRouteWithContext instead of
importing it directly in each route file:
- define RouterContext with preloadQuery in __root.tsx
- pass preloadQuery to createRouter({ context }) in App.tsx
- all route loaders access it via context.preloadQuery

use $repo beforeLoad to provide shared context to all child routes:
- resolve repo slug ("_" → null) once as context.ref
- preload validLabels and allIdentities queries (shared across
  issue list, bug detail, etc.)
- child routes read labelsRef/identitiesRef from Route.useRouteContext()
  instead of preloading their own copies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Change summary

webui2/src/App.tsx                       |  3 ++
webui2/src/routes/$repo.tsx              | 21 +++++++++++++++++++
webui2/src/routes/$repo/commit/$hash.tsx |  5 +--
webui2/src/routes/$repo/index.tsx        |  5 +--
webui2/src/routes/$repo/issues/$id.tsx   | 22 ++++---------------
webui2/src/routes/$repo/issues/index.tsx | 28 ++++---------------------
webui2/src/routes/__root.tsx             |  9 ++++++-
webui2/src/routes/index.tsx              |  3 -
8 files changed, 46 insertions(+), 50 deletions(-)

Detailed changes

webui2/src/App.tsx 🔗

@@ -1,11 +1,14 @@
 import { createRouter, RouterProvider } from "@tanstack/react-router";
 
+import { preloadQuery } from "@/lib/apollo";
+
 import { routeTree } from "./routeTree.gen";
 
 const router = createRouter({
   routeTree,
   defaultPreload: "intent",
   scrollRestoration: true,
+  context: { preloadQuery },
 });
 
 declare module "@tanstack/react-router" {

webui2/src/routes/$repo.tsx 🔗

@@ -1,7 +1,28 @@
 import { createFileRoute } from "@tanstack/react-router";
 
+import {
+  type ValidLabelsQuery,
+  ValidLabelsDocument,
+  type AllIdentitiesQuery,
+  AllIdentitiesDocument,
+} from "@/__generated__/graphql";
 import { RepoShell } from "@/lib/repo";
 
 export const Route = createFileRoute("/$repo")({
   component: RepoShell,
+  beforeLoad: ({ params: { repo }, context: { preloadQuery } }) => {
+    // Normalize the repo slug: "_" means the default (null) repo
+    const ref = repo === "_" ? null : repo;
+
+    // Preload labels and identities shared by all child routes (issue list,
+    // bug detail, etc.). These are stable across filter/page changes.
+    const labelsRef = preloadQuery<ValidLabelsQuery>(ValidLabelsDocument, {
+      variables: { ref },
+    });
+    const identitiesRef = preloadQuery<AllIdentitiesQuery>(AllIdentitiesDocument, {
+      variables: { ref },
+    });
+
+    return { ref, labelsRef, identitiesRef };
+  },
 });

webui2/src/routes/$repo/commit/$hash.tsx 🔗

@@ -9,7 +9,6 @@ import { ArrowLeft, GitCommit } from "lucide-react";
 
 import { FileDiffView } from "@/components/code/FileDiffView";
 import { Skeleton } from "@/components/ui/skeleton";
-import { preloadQuery } from "@/lib/apollo";
 import { useRepo } from "@/lib/repo";
 
 const COMMIT_QUERY = gql`
@@ -57,9 +56,9 @@ interface CommitQueryData {
 export const Route = createFileRoute("/$repo/commit/$hash")({
   component: RouteComponent,
   pendingComponent: CommitPageSkeleton,
-  loader: async ({ params: { repo, hash } }) => {
+  loader: async ({ context: { preloadQuery, ref }, params: { hash } }) => {
     const commitRef = preloadQuery<CommitQueryData>(COMMIT_QUERY, {
-      variables: { repo: repo === "_" ? null : repo, hash },
+      variables: { repo: ref, hash },
     });
     return { commitRef: await preloadQuery.toPromise(commitRef) };
   },

webui2/src/routes/$repo/index.tsx 🔗

@@ -24,7 +24,6 @@ import { RefSelector } from "@/components/code/RefSelector";
 import { Markdown } from "@/components/content/Markdown";
 import { ButtonLink } from "@/components/ui/button-link";
 import { Skeleton } from "@/components/ui/skeleton";
-import { preloadQuery } from "@/lib/apollo";
 import { useRepo } from "@/lib/repo";
 
 const REFS_QUERY = gql`
@@ -126,9 +125,9 @@ export const Route = createFileRoute("/$repo/")({
   component: RouteComponent,
   pendingComponent: CodePageSkeleton,
   validateSearch: (search) => v.parse(codePageSearchSchema, search),
-  loader: async ({ params: { repo } }) => {
+  loader: async ({ context: { preloadQuery, ref } }) => {
     const refsRef = preloadQuery<RefsQueryData>(REFS_QUERY, {
-      variables: { repo: repo === "_" ? null : repo },
+      variables: { repo: ref },
     });
     return { refsRef: await preloadQuery.toPromise(refsRef) };
   },

webui2/src/routes/$repo/issues/$id.tsx 🔗

@@ -3,12 +3,7 @@ import { createFileRoute, Link } from "@tanstack/react-router";
 import { formatDistanceToNow } from "date-fns";
 import { ArrowLeft } from "lucide-react";
 
-import {
-  type BugDetailQuery,
-  BugDetailDocument,
-  type ValidLabelsQuery,
-  ValidLabelsDocument,
-} from "@/__generated__/graphql";
+import { type BugDetailQuery, BugDetailDocument } from "@/__generated__/graphql";
 import { CommentBox } from "@/components/bugs/CommentBox";
 import { LabelEditor } from "@/components/bugs/LabelEditor";
 import { StatusBadge } from "@/components/bugs/StatusBadge";
@@ -17,24 +12,16 @@ import { TitleEditor } from "@/components/bugs/TitleEditor";
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
 import { Separator } from "@/components/ui/separator";
 import { Skeleton } from "@/components/ui/skeleton";
-import { preloadQuery } from "@/lib/apollo";
 import { useRepo } from "@/lib/repo";
 
 export const Route = createFileRoute("/$repo/issues/$id")({
   component: RouteComponent,
   pendingComponent: BugDetailSkeleton,
-  loader: async ({ params: { repo, id } }) => {
-    const ref = repo === "_" ? null : repo;
+  loader: async ({ context: { preloadQuery, ref }, params: { id } }) => {
     const bugDetailRef = preloadQuery<BugDetailQuery>(BugDetailDocument, {
       variables: { ref, prefix: id },
     });
-    const labelsRef = preloadQuery<ValidLabelsQuery>(ValidLabelsDocument, {
-      variables: { ref },
-    });
-    return {
-      bugDetailRef: await preloadQuery.toPromise(bugDetailRef),
-      labelsRef: await preloadQuery.toPromise(labelsRef),
-    };
+    return { bugDetailRef: await preloadQuery.toPromise(bugDetailRef) };
   },
 });
 
@@ -42,7 +29,8 @@ export const Route = createFileRoute("/$repo/issues/$id")({
 // comments and events, and a sidebar with labels and participants.
 function RouteComponent() {
   const repo = useRepo();
-  const { bugDetailRef, labelsRef } = Route.useLoaderData();
+  const { bugDetailRef } = Route.useLoaderData();
+  const { labelsRef } = Route.useRouteContext();
   const { data } = useReadQuery(bugDetailRef);
   const { data: labelsData } = useReadQuery(labelsRef);
   const validLabels = labelsData?.repository?.validLabels.nodes ?? [];

webui2/src/routes/$repo/issues/index.tsx 🔗

@@ -4,14 +4,7 @@ import { CircleDot, CircleCheck, ChevronLeft, ChevronRight } from "lucide-react"
 import { useState } from "react";
 import * as v from "valibot";
 
-import {
-  type BugListQuery,
-  BugListDocument,
-  type ValidLabelsQuery,
-  ValidLabelsDocument,
-  type AllIdentitiesQuery,
-  AllIdentitiesDocument,
-} from "@/__generated__/graphql";
+import { type BugListQuery, BugListDocument } from "@/__generated__/graphql";
 import { BugRow } from "@/components/bugs/BugRow";
 import { IssueFilters } from "@/components/bugs/IssueFilters";
 import type { SortValue } from "@/components/bugs/IssueFilters";
@@ -19,7 +12,6 @@ import { QueryInput } from "@/components/bugs/QueryInput";
 import { Button } from "@/components/ui/button";
 import { ButtonLink } from "@/components/ui/button-link";
 import { Skeleton } from "@/components/ui/skeleton";
-import { preloadQuery } from "@/lib/apollo";
 import { useRepo } from "@/lib/repo";
 import { cn } from "@/lib/utils";
 
@@ -33,8 +25,7 @@ export const Route = createFileRoute("/$repo/issues/")({
   pendingComponent: BugListSkeleton,
   validateSearch: (search) => v.parse(issuesSearchSchema, search),
   loaderDeps: ({ search: { q, after } }) => ({ q, after }),
-  loader: async ({ params: { repo }, deps: { q, after } }) => {
-    const ref = repo === "_" ? null : repo;
+  loader: async ({ context: { preloadQuery, ref }, deps: { q, after } }) => {
     const parsed = parseQueryString(q);
     const baseQuery = buildBaseQuery(parsed.labels, parsed.author, parsed.freeText);
     const bugListRef = preloadQuery<BugListQuery>(BugListDocument, {
@@ -47,17 +38,7 @@ export const Route = createFileRoute("/$repo/issues/")({
         after: after || undefined,
       },
     });
-    const labelsRef = preloadQuery<ValidLabelsQuery>(ValidLabelsDocument, {
-      variables: { ref },
-    });
-    const identitiesRef = preloadQuery<AllIdentitiesQuery>(AllIdentitiesDocument, {
-      variables: { ref },
-    });
-    return {
-      bugListRef: await preloadQuery.toPromise(bugListRef),
-      labelsRef: await preloadQuery.toPromise(labelsRef),
-      identitiesRef: await preloadQuery.toPromise(identitiesRef),
-    };
+    return { bugListRef: await preloadQuery.toPromise(bugListRef) };
   },
 });
 
@@ -84,7 +65,8 @@ function RouteComponent() {
   // Draft is the text input value — starts from URL, only committed on submit
   const [draft, setDraft] = useState(q);
 
-  const { bugListRef, labelsRef, identitiesRef } = Route.useLoaderData();
+  const { bugListRef } = Route.useLoaderData();
+  const { labelsRef, identitiesRef } = Route.useRouteContext();
   const { data } = useReadQuery(bugListRef);
   const { data: labelsData } = useReadQuery(labelsRef);
   const { data: identitiesData } = useReadQuery(identitiesRef);

webui2/src/routes/__root.tsx 🔗

@@ -1,11 +1,16 @@
-import { createRootRoute, useRouter } from "@tanstack/react-router";
+import { createRootRouteWithContext, useRouter } from "@tanstack/react-router";
 import { AlertTriangle } from "lucide-react";
 
 import { Shell } from "@/components/layout/Shell";
 import { Button } from "@/components/ui/button";
 import { ButtonLink } from "@/components/ui/button-link";
+import type { preloadQuery } from "@/lib/apollo";
 
-export const Route = createRootRoute({
+export interface RouterContext {
+  preloadQuery: typeof preloadQuery;
+}
+
+export const Route = createRootRouteWithContext<RouterContext>()({
   component: Shell,
   errorComponent: ErrorPage,
 });

webui2/src/routes/index.tsx 🔗

@@ -7,11 +7,10 @@ import { GitFork, FolderOpen, AlertCircle } from "lucide-react";
 import { useEffect } from "react";
 
 import { type RepositoriesQuery, RepositoriesDocument } from "@/__generated__/graphql";
-import { preloadQuery } from "@/lib/apollo";
 
 export const Route = createFileRoute("/")({
   component: RouteComponent,
-  loader: async () => {
+  loader: async ({ context: { preloadQuery } }) => {
     const repositoriesRef = preloadQuery<RepositoriesQuery>(RepositoriesDocument);
     return { repositoriesRef: await preloadQuery.toPromise(repositoriesRef) };
   },