diff --git a/.changeset/violet-poets-wait.md b/.changeset/violet-poets-wait.md new file mode 100644 index 0000000000..8d86936fec --- /dev/null +++ b/.changeset/violet-poets-wait.md @@ -0,0 +1,5 @@ +--- +'@tanstack/router-core': patch +--- + +Fix context value from a parent route's `beforeLoad` not being propagated to a sub-route while the sub-route's loader is reloading in the background diff --git a/e2e/react-router/basic-file-based/src/routeTree.gen.ts b/e2e/react-router/basic-file-based/src/routeTree.gen.ts index 7ee0051549..2a5d84fb94 100644 --- a/e2e/react-router/basic-file-based/src/routeTree.gen.ts +++ b/e2e/react-router/basic-file-based/src/routeTree.gen.ts @@ -25,12 +25,14 @@ import { Route as SearchParamsRouteRouteImport } from './routes/search-params/ro import { Route as PathlessLayoutRouteRouteImport } from './routes/pathless-layout/route' import { Route as NonNestedRouteRouteImport } from './routes/non-nested/route' import { Route as FullpathTestRouteRouteImport } from './routes/fullpath-test/route' +import { Route as ContextPropagationRouteRouteImport } from './routes/context-propagation/route' import { Route as IndexRouteImport } from './routes/index' import { Route as SearchParamsIndexRouteImport } from './routes/search-params/index' import { Route as RelativeIndexRouteImport } from './routes/relative/index' import { Route as RedirectIndexRouteImport } from './routes/redirect/index' import { Route as PostsIndexRouteImport } from './routes/posts.index' import { Route as ParamsPsIndexRouteImport } from './routes/params-ps/index' +import { Route as ContextPropagationIndexRouteImport } from './routes/context-propagation/index' import { Route as StructuralSharingEnabledRouteImport } from './routes/structural-sharing.$enabled' import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default' import { Route as RedirectTargetRouteImport } from './routes/redirect/$target' @@ -210,6 +212,11 @@ const FullpathTestRouteRoute = FullpathTestRouteRouteImport.update({ path: '/fullpath-test', getParentRoute: () => rootRouteImport, } as any) +const ContextPropagationRouteRoute = ContextPropagationRouteRouteImport.update({ + id: '/context-propagation', + path: '/context-propagation', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', @@ -240,6 +247,11 @@ const ParamsPsIndexRoute = ParamsPsIndexRouteImport.update({ path: '/params-ps/', getParentRoute: () => rootRouteImport, } as any) +const ContextPropagationIndexRoute = ContextPropagationIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ContextPropagationRouteRoute, +} as any) const StructuralSharingEnabledRoute = StructuralSharingEnabledRouteImport.update({ id: '/structural-sharing/$enabled', @@ -780,6 +792,7 @@ const NonNestedDeepBazBarFooQuxRoute = export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/context-propagation': typeof ContextPropagationRouteRouteWithChildren '/fullpath-test': typeof FullpathTestRouteRouteWithChildren '/non-nested': typeof NonNestedRouteRouteWithChildren '/pathless-layout': typeof PathlessLayoutRouteRouteWithChildren @@ -811,6 +824,7 @@ export interface FileRoutesByFullPath { '/redirect/$target': typeof RedirectTargetRouteWithChildren '/search-params/default': typeof SearchParamsDefaultRoute '/structural-sharing/$enabled': typeof StructuralSharingEnabledRoute + '/context-propagation/': typeof ContextPropagationIndexRoute '/params-ps/': typeof ParamsPsIndexRoute '/posts/': typeof PostsIndexRoute '/redirect/': typeof RedirectIndexRoute @@ -925,6 +939,7 @@ export interface FileRoutesByTo { '/posts/$postId': typeof PostsPostIdRoute '/search-params/default': typeof SearchParamsDefaultRoute '/structural-sharing/$enabled': typeof StructuralSharingEnabledRoute + '/context-propagation': typeof ContextPropagationIndexRoute '/params-ps': typeof ParamsPsIndexRoute '/posts': typeof PostsIndexRoute '/redirect': typeof RedirectIndexRoute @@ -1003,6 +1018,7 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/context-propagation': typeof ContextPropagationRouteRouteWithChildren '/fullpath-test': typeof FullpathTestRouteRouteWithChildren '/non-nested': typeof NonNestedRouteRouteWithChildren '/pathless-layout': typeof PathlessLayoutRouteRouteWithChildren @@ -1039,6 +1055,7 @@ export interface FileRoutesById { '/redirect/$target': typeof RedirectTargetRouteWithChildren '/search-params/default': typeof SearchParamsDefaultRoute '/structural-sharing/$enabled': typeof StructuralSharingEnabledRoute + '/context-propagation/': typeof ContextPropagationIndexRoute '/params-ps/': typeof ParamsPsIndexRoute '/posts/': typeof PostsIndexRoute '/redirect/': typeof RedirectIndexRoute @@ -1127,6 +1144,7 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' + | '/context-propagation' | '/fullpath-test' | '/non-nested' | '/pathless-layout' @@ -1158,6 +1176,7 @@ export interface FileRouteTypes { | '/redirect/$target' | '/search-params/default' | '/structural-sharing/$enabled' + | '/context-propagation/' | '/params-ps/' | '/posts/' | '/redirect/' @@ -1272,6 +1291,7 @@ export interface FileRouteTypes { | '/posts/$postId' | '/search-params/default' | '/structural-sharing/$enabled' + | '/context-propagation' | '/params-ps' | '/posts' | '/redirect' @@ -1349,6 +1369,7 @@ export interface FileRouteTypes { id: | '__root__' | '/' + | '/context-propagation' | '/fullpath-test' | '/non-nested' | '/pathless-layout' @@ -1385,6 +1406,7 @@ export interface FileRouteTypes { | '/redirect/$target' | '/search-params/default' | '/structural-sharing/$enabled' + | '/context-propagation/' | '/params-ps/' | '/posts/' | '/redirect/' @@ -1472,6 +1494,7 @@ export interface FileRouteTypes { } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + ContextPropagationRouteRoute: typeof ContextPropagationRouteRouteWithChildren FullpathTestRouteRoute: typeof FullpathTestRouteRouteWithChildren NonNestedRouteRoute: typeof NonNestedRouteRouteWithChildren PathlessLayoutRouteRoute: typeof PathlessLayoutRouteRouteWithChildren @@ -1633,6 +1656,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof FullpathTestRouteRouteImport parentRoute: typeof rootRouteImport } + '/context-propagation': { + id: '/context-propagation' + path: '/context-propagation' + fullPath: '/context-propagation' + preLoaderRoute: typeof ContextPropagationRouteRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -1675,6 +1705,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ParamsPsIndexRouteImport parentRoute: typeof rootRouteImport } + '/context-propagation/': { + id: '/context-propagation/' + path: '/' + fullPath: '/context-propagation/' + preLoaderRoute: typeof ContextPropagationIndexRouteImport + parentRoute: typeof ContextPropagationRouteRoute + } '/structural-sharing/$enabled': { id: '/structural-sharing/$enabled' path: '/structural-sharing/$enabled' @@ -2364,6 +2401,20 @@ declare module '@tanstack/react-router' { } } +interface ContextPropagationRouteRouteChildren { + ContextPropagationIndexRoute: typeof ContextPropagationIndexRoute +} + +const ContextPropagationRouteRouteChildren: ContextPropagationRouteRouteChildren = + { + ContextPropagationIndexRoute: ContextPropagationIndexRoute, + } + +const ContextPropagationRouteRouteWithChildren = + ContextPropagationRouteRoute._addFileChildren( + ContextPropagationRouteRouteChildren, + ) + interface FullpathTestLayoutRouteRouteChildren { FullpathTestLayoutIdRoute: typeof FullpathTestLayoutIdRoute FullpathTestLayoutIndexRoute: typeof FullpathTestLayoutIndexRoute @@ -2869,6 +2920,7 @@ const ParamsPsNamedFooRouteRouteWithChildren = const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + ContextPropagationRouteRoute: ContextPropagationRouteRouteWithChildren, FullpathTestRouteRoute: FullpathTestRouteRouteWithChildren, NonNestedRouteRoute: NonNestedRouteRouteWithChildren, PathlessLayoutRouteRoute: PathlessLayoutRouteRouteWithChildren, diff --git a/e2e/react-router/basic-file-based/src/routes/context-propagation/index.tsx b/e2e/react-router/basic-file-based/src/routes/context-propagation/index.tsx new file mode 100644 index 0000000000..a8b5e3a029 --- /dev/null +++ b/e2e/react-router/basic-file-based/src/routes/context-propagation/index.tsx @@ -0,0 +1,23 @@ +import { createFileRoute } from '@tanstack/react-router' + +// Records whether the component ever rendered without the context value from +// the parent route's beforeLoad (https://github.com/TanStack/router/issues/7602) +let sawUndefinedContext = false + +export const Route = createFileRoute('/context-propagation/')({ + // ensure the loader runs again on back navigation despite defaultStaleTime + staleTime: 0, + loader: () => new Promise((resolve) => setTimeout(resolve, 100)), + component: RouteComponent, +}) + +function RouteComponent() { + const { number } = Route.useRouteContext() + sawUndefinedContext ||= number === undefined + + return ( +

+ number = {String(number)}, saw undefined = {String(sawUndefinedContext)} +

+ ) +} diff --git a/e2e/react-router/basic-file-based/src/routes/context-propagation/route.tsx b/e2e/react-router/basic-file-based/src/routes/context-propagation/route.tsx new file mode 100644 index 0000000000..cc04221d5b --- /dev/null +++ b/e2e/react-router/basic-file-based/src/routes/context-propagation/route.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/context-propagation')({ + beforeLoad: () => ({ number: 42 }), +}) diff --git a/e2e/react-router/basic-file-based/tests/context-propagation.spec.ts b/e2e/react-router/basic-file-based/tests/context-propagation.spec.ts new file mode 100644 index 0000000000..4729b497c3 --- /dev/null +++ b/e2e/react-router/basic-file-based/tests/context-propagation.spec.ts @@ -0,0 +1,22 @@ +import { expect, test } from '@playwright/test' + +// https://github.com/TanStack/router/issues/7602 +test('context value from beforeLoad is propagated to a sub-route while the loader of the sub-route is executing', async ({ + page, +}) => { + await page.goto('/context-propagation') + await expect(page.getByTestId('context-propagation-result')).toHaveText( + 'number = 42, saw undefined = false', + ) + + await page.getByRole('link', { name: 'Home', exact: true }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') + + await page.goBack() + + // the component must never render with an undefined context value + // while the loader is executing + await expect(page.getByTestId('context-propagation-result')).toHaveText( + 'number = 42, saw undefined = false', + ) +}) diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts index f901a0c97d..20d31afd1d 100644 --- a/packages/router-core/src/load-matches.ts +++ b/packages/router-core/src/load-matches.ts @@ -832,6 +832,7 @@ const loadRouteMatch = async ( shouldReloadInBackground ) { loaderIsRunningAsync = true + syncMatchContext(inner, matchId, index) ;(async () => { try { await runLoader(inner, matchPromises, matchId, index, route)