From e945936fab8531c2cd4ef400c816cccb70861005 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Mon, 13 Apr 2026 17:01:04 -0400 Subject: [PATCH] Fix stuck navigations caused by route nesting bug and blocking auth checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit issues.tsx acted as an unintended layout parent for issues.$issueId.tsx due to TanStack Router dot-notation, but never rendered , so child routes had nowhere to mount — the URL changed but content froze. Renamed to issues.index.tsx to make it a sibling index route. Also added defaultPendingComponent for immediate loading feedback on all transitions and cached the _protected beforeLoad auth result to eliminate redundant server round-trips on every in-app navigation. --- apps/dashboard/src/routeTree.gen.ts | 46 +++++++++---------- apps/dashboard/src/router.tsx | 2 + apps/dashboard/src/routes/_protected.tsx | 19 ++++++-- .../$repo/{issues.tsx => issues.index.tsx} | 2 +- 4 files changed, 42 insertions(+), 27 deletions(-) rename apps/dashboard/src/routes/_protected/$owner/$repo/{issues.tsx => issues.index.tsx} (99%) diff --git a/apps/dashboard/src/routeTree.gen.ts b/apps/dashboard/src/routeTree.gen.ts index f62913f..5014470 100644 --- a/apps/dashboard/src/routeTree.gen.ts +++ b/apps/dashboard/src/routeTree.gen.ts @@ -29,7 +29,7 @@ import { Route as ProtectedOwnerRepoIndexRouteImport } from './routes/_protected import { Route as ApiGithubAppCallbackRouteImport } from './routes/api/github/app/callback' import { Route as ApiGithubAppAuthorizeRouteImport } from './routes/api/github/app/authorize' import { Route as ProtectedOwnerRepoPullsRouteImport } from './routes/_protected/$owner/$repo/pulls' -import { Route as ProtectedOwnerRepoIssuesRouteImport } from './routes/_protected/$owner/$repo/issues' +import { Route as ProtectedOwnerRepoIssuesIndexRouteImport } from './routes/_protected/$owner/$repo/issues.index' import { Route as ProtectedOwnerRepoReviewPullIdRouteImport } from './routes/_protected/$owner/$repo/review.$pullId' import { Route as ProtectedOwnerRepoPullPullIdRouteImport } from './routes/_protected/$owner/$repo/pull.$pullId' import { Route as ProtectedOwnerRepoIssuesNewRouteImport } from './routes/_protected/$owner/$repo/issues.new' @@ -135,10 +135,10 @@ const ProtectedOwnerRepoPullsRoute = ProtectedOwnerRepoPullsRouteImport.update({ path: '/$owner/$repo/pulls', getParentRoute: () => ProtectedRoute, } as any) -const ProtectedOwnerRepoIssuesRoute = - ProtectedOwnerRepoIssuesRouteImport.update({ - id: '/$owner/$repo/issues', - path: '/$owner/$repo/issues', +const ProtectedOwnerRepoIssuesIndexRoute = + ProtectedOwnerRepoIssuesIndexRouteImport.update({ + id: '/$owner/$repo/issues/', + path: '/$owner/$repo/issues/', getParentRoute: () => ProtectedRoute, } as any) const ProtectedOwnerRepoReviewPullIdRoute = @@ -182,15 +182,15 @@ export interface FileRoutesByFullPath { '/api/webhooks/github': typeof ApiWebhooksGithubRoute '/$owner/': typeof ProtectedOwnerIndexRoute '/settings/': typeof ProtectedSettingsIndexRoute + '/$owner/$repo/pulls': typeof ProtectedOwnerRepoPullsRoute '/api/github/app/authorize': typeof ApiGithubAppAuthorizeRoute '/api/github/app/callback': typeof ApiGithubAppCallbackRoute '/$owner/$repo/': typeof ProtectedOwnerRepoIndexRoute - '/$owner/$repo/issues': typeof ProtectedOwnerRepoIssuesRoute - '/$owner/$repo/pulls': typeof ProtectedOwnerRepoPullsRoute '/$owner/$repo/issues/$issueId': typeof ProtectedOwnerRepoIssuesIssueIdRoute '/$owner/$repo/issues/new': typeof ProtectedOwnerRepoIssuesNewRoute '/$owner/$repo/pull/$pullId': typeof ProtectedOwnerRepoPullPullIdRoute '/$owner/$repo/review/$pullId': typeof ProtectedOwnerRepoReviewPullIdRoute + '/$owner/$repo/issues/': typeof ProtectedOwnerRepoIssuesIndexRoute } export interface FileRoutesByTo { '/$': typeof SplatRoute @@ -207,15 +207,15 @@ export interface FileRoutesByTo { '/api/webhooks/github': typeof ApiWebhooksGithubRoute '/$owner': typeof ProtectedOwnerIndexRoute '/settings': typeof ProtectedSettingsIndexRoute + '/$owner/$repo/pulls': typeof ProtectedOwnerRepoPullsRoute '/api/github/app/authorize': typeof ApiGithubAppAuthorizeRoute '/api/github/app/callback': typeof ApiGithubAppCallbackRoute '/$owner/$repo': typeof ProtectedOwnerRepoIndexRoute - '/$owner/$repo/issues': typeof ProtectedOwnerRepoIssuesRoute - '/$owner/$repo/pulls': typeof ProtectedOwnerRepoPullsRoute '/$owner/$repo/issues/$issueId': typeof ProtectedOwnerRepoIssuesIssueIdRoute '/$owner/$repo/issues/new': typeof ProtectedOwnerRepoIssuesNewRoute '/$owner/$repo/pull/$pullId': typeof ProtectedOwnerRepoPullPullIdRoute '/$owner/$repo/review/$pullId': typeof ProtectedOwnerRepoReviewPullIdRoute + '/$owner/$repo/issues': typeof ProtectedOwnerRepoIssuesIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -235,15 +235,15 @@ export interface FileRoutesById { '/api/webhooks/github': typeof ApiWebhooksGithubRoute '/_protected/$owner/': typeof ProtectedOwnerIndexRoute '/_protected/settings/': typeof ProtectedSettingsIndexRoute + '/_protected/$owner/$repo/pulls': typeof ProtectedOwnerRepoPullsRoute '/api/github/app/authorize': typeof ApiGithubAppAuthorizeRoute '/api/github/app/callback': typeof ApiGithubAppCallbackRoute '/_protected/$owner/$repo/': typeof ProtectedOwnerRepoIndexRoute - '/_protected/$owner/$repo/issues': typeof ProtectedOwnerRepoIssuesRoute - '/_protected/$owner/$repo/pulls': typeof ProtectedOwnerRepoPullsRoute '/_protected/$owner/$repo/issues/$issueId': typeof ProtectedOwnerRepoIssuesIssueIdRoute '/_protected/$owner/$repo/issues/new': typeof ProtectedOwnerRepoIssuesNewRoute '/_protected/$owner/$repo/pull/$pullId': typeof ProtectedOwnerRepoPullPullIdRoute '/_protected/$owner/$repo/review/$pullId': typeof ProtectedOwnerRepoReviewPullIdRoute + '/_protected/$owner/$repo/issues/': typeof ProtectedOwnerRepoIssuesIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -263,15 +263,15 @@ export interface FileRouteTypes { | '/api/webhooks/github' | '/$owner/' | '/settings/' + | '/$owner/$repo/pulls' | '/api/github/app/authorize' | '/api/github/app/callback' | '/$owner/$repo/' - | '/$owner/$repo/issues' - | '/$owner/$repo/pulls' | '/$owner/$repo/issues/$issueId' | '/$owner/$repo/issues/new' | '/$owner/$repo/pull/$pullId' | '/$owner/$repo/review/$pullId' + | '/$owner/$repo/issues/' fileRoutesByTo: FileRoutesByTo to: | '/$' @@ -288,15 +288,15 @@ export interface FileRouteTypes { | '/api/webhooks/github' | '/$owner' | '/settings' + | '/$owner/$repo/pulls' | '/api/github/app/authorize' | '/api/github/app/callback' | '/$owner/$repo' - | '/$owner/$repo/issues' - | '/$owner/$repo/pulls' | '/$owner/$repo/issues/$issueId' | '/$owner/$repo/issues/new' | '/$owner/$repo/pull/$pullId' | '/$owner/$repo/review/$pullId' + | '/$owner/$repo/issues' id: | '__root__' | '/$' @@ -315,15 +315,15 @@ export interface FileRouteTypes { | '/api/webhooks/github' | '/_protected/$owner/' | '/_protected/settings/' + | '/_protected/$owner/$repo/pulls' | '/api/github/app/authorize' | '/api/github/app/callback' | '/_protected/$owner/$repo/' - | '/_protected/$owner/$repo/issues' - | '/_protected/$owner/$repo/pulls' | '/_protected/$owner/$repo/issues/$issueId' | '/_protected/$owner/$repo/issues/new' | '/_protected/$owner/$repo/pull/$pullId' | '/_protected/$owner/$repo/review/$pullId' + | '/_protected/$owner/$repo/issues/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -481,11 +481,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProtectedOwnerRepoPullsRouteImport parentRoute: typeof ProtectedRoute } - '/_protected/$owner/$repo/issues': { - id: '/_protected/$owner/$repo/issues' + '/_protected/$owner/$repo/issues/': { + id: '/_protected/$owner/$repo/issues/' path: '/$owner/$repo/issues' - fullPath: '/$owner/$repo/issues' - preLoaderRoute: typeof ProtectedOwnerRepoIssuesRouteImport + fullPath: '/$owner/$repo/issues/' + preLoaderRoute: typeof ProtectedOwnerRepoIssuesIndexRouteImport parentRoute: typeof ProtectedRoute } '/_protected/$owner/$repo/review/$pullId': { @@ -539,13 +539,13 @@ interface ProtectedRouteChildren { ProtectedSettingsRoute: typeof ProtectedSettingsRouteWithChildren ProtectedIndexRoute: typeof ProtectedIndexRoute ProtectedOwnerIndexRoute: typeof ProtectedOwnerIndexRoute - ProtectedOwnerRepoIssuesRoute: typeof ProtectedOwnerRepoIssuesRoute ProtectedOwnerRepoPullsRoute: typeof ProtectedOwnerRepoPullsRoute ProtectedOwnerRepoIndexRoute: typeof ProtectedOwnerRepoIndexRoute ProtectedOwnerRepoIssuesIssueIdRoute: typeof ProtectedOwnerRepoIssuesIssueIdRoute ProtectedOwnerRepoIssuesNewRoute: typeof ProtectedOwnerRepoIssuesNewRoute ProtectedOwnerRepoPullPullIdRoute: typeof ProtectedOwnerRepoPullPullIdRoute ProtectedOwnerRepoReviewPullIdRoute: typeof ProtectedOwnerRepoReviewPullIdRoute + ProtectedOwnerRepoIssuesIndexRoute: typeof ProtectedOwnerRepoIssuesIndexRoute } const ProtectedRouteChildren: ProtectedRouteChildren = { @@ -555,13 +555,13 @@ const ProtectedRouteChildren: ProtectedRouteChildren = { ProtectedSettingsRoute: ProtectedSettingsRouteWithChildren, ProtectedIndexRoute: ProtectedIndexRoute, ProtectedOwnerIndexRoute: ProtectedOwnerIndexRoute, - ProtectedOwnerRepoIssuesRoute: ProtectedOwnerRepoIssuesRoute, ProtectedOwnerRepoPullsRoute: ProtectedOwnerRepoPullsRoute, ProtectedOwnerRepoIndexRoute: ProtectedOwnerRepoIndexRoute, ProtectedOwnerRepoIssuesIssueIdRoute: ProtectedOwnerRepoIssuesIssueIdRoute, ProtectedOwnerRepoIssuesNewRoute: ProtectedOwnerRepoIssuesNewRoute, ProtectedOwnerRepoPullPullIdRoute: ProtectedOwnerRepoPullPullIdRoute, ProtectedOwnerRepoReviewPullIdRoute: ProtectedOwnerRepoReviewPullIdRoute, + ProtectedOwnerRepoIssuesIndexRoute: ProtectedOwnerRepoIssuesIndexRoute, } const ProtectedRouteWithChildren = ProtectedRoute._addFileChildren( diff --git a/apps/dashboard/src/router.tsx b/apps/dashboard/src/router.tsx index c084434..9ef5e19 100644 --- a/apps/dashboard/src/router.tsx +++ b/apps/dashboard/src/router.tsx @@ -1,5 +1,6 @@ import { createRouter as createTanStackRouter } from "@tanstack/react-router"; import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query"; +import { DashboardContentLoading } from "#/components/layouts/dashboard-content-loading"; import { DashboardErrorScreen } from "#/components/layouts/dashboard-error-screen"; import { NotFoundScreen } from "#/components/layouts/not-found-screen"; import { @@ -19,6 +20,7 @@ export function getRouter() { defaultPreload: "intent", defaultPreloadStaleTime: 0, defaultPendingMs: 0, + defaultPendingComponent: DashboardContentLoading, defaultErrorComponent: DashboardErrorScreen, defaultNotFoundComponent: NotFoundScreen, Wrap: ({ children }) => ( diff --git a/apps/dashboard/src/routes/_protected.tsx b/apps/dashboard/src/routes/_protected.tsx index 826c818..6295aa2 100644 --- a/apps/dashboard/src/routes/_protected.tsx +++ b/apps/dashboard/src/routes/_protected.tsx @@ -5,9 +5,22 @@ import { getSession } from "#/lib/auth.functions"; import { checkSetupComplete } from "#/lib/github.functions"; import { buildSeo, formatPageTitle, PRIVATE_ROUTE_HEADERS } from "#/lib/seo"; +/** + * Cache the auth check so navigations within the dashboard are instant. + * The cache is cleared on full page reloads. If the session expires mid-use, + * API calls in child routes will 401 and the error boundary handles it. + */ +let cachedAuth: Awaited> | null = null; + export const Route = createFileRoute("/_protected")({ beforeLoad: async ({ location }) => { - const session = await getSession(); + if (cachedAuth) return cachedAuth; + + const [session, setupComplete] = await Promise.all([ + getSession(), + checkSetupComplete(), + ]); + if (!session) { throw redirect({ to: "/login", @@ -15,12 +28,12 @@ export const Route = createFileRoute("/_protected")({ }); } - const setupComplete = await checkSetupComplete(); if (!setupComplete) { throw redirect({ to: "/setup" }); } - return { user: session.user, session: session.session }; + cachedAuth = { user: session.user, session: session.session }; + return cachedAuth; }, headers: () => PRIVATE_ROUTE_HEADERS, head: ({ match }) => { diff --git a/apps/dashboard/src/routes/_protected/$owner/$repo/issues.tsx b/apps/dashboard/src/routes/_protected/$owner/$repo/issues.index.tsx similarity index 99% rename from apps/dashboard/src/routes/_protected/$owner/$repo/issues.tsx rename to apps/dashboard/src/routes/_protected/$owner/$repo/issues.index.tsx index 5e72deb..8b87f47 100644 --- a/apps/dashboard/src/routes/_protected/$owner/$repo/issues.tsx +++ b/apps/dashboard/src/routes/_protected/$owner/$repo/issues.index.tsx @@ -24,7 +24,7 @@ import { useHasMounted } from "#/lib/use-has-mounted"; const PER_PAGE = 30; -export const Route = createFileRoute("/_protected/$owner/$repo/issues")({ +export const Route = createFileRoute("/_protected/$owner/$repo/issues/")({ ssr: false, head: ({ match, params }) => buildSeo({