diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx index d842b7507ea..3f10605653c 100644 --- a/packages/react-router/src/Matches.tsx +++ b/packages/react-router/src/Matches.tsx @@ -48,6 +48,7 @@ export interface RouteMatch< searchError: unknown updatedAt: number loadPromise: ControlledPromise + beforeLoadPromise: ControlledPromise loaderPromise: Promise loaderData?: TLoaderData routeContext: TRouteContext diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts index 203c7d92632..89a6781067e 100644 --- a/packages/react-router/src/router.ts +++ b/packages/react-router/src/router.ts @@ -1082,6 +1082,7 @@ export class Router< isFetching: false, error: undefined, paramsError: parseErrors[index], + beforeLoadPromise: createControlledPromise(), loaderPromise: Promise.resolve(), loadPromise, routeContext: undefined!, @@ -1785,7 +1786,9 @@ export class Router< matches[index] = match = updateMatch(match.id, (prev) => ({ ...prev, isFetching: 'beforeLoad', + beforeLoadPromise: createControlledPromise(), loadPromise, + abortController, })) const handleSerialError = (err: any, routerCode: string) => { @@ -1869,7 +1872,7 @@ export class Router< ...beforeLoadContext, } - matches[index] = match = { + matches[index] = match = updateMatch(match.id, () => ({ ...match, routeContext: replaceEqualDeep( match.routeContext, @@ -1877,8 +1880,9 @@ export class Router< ), context: replaceEqualDeep(match.context, context), abortController, - } - updateMatch(match.id, () => match) + })) + + match.beforeLoadPromise.resolve() } catch (err) { handleSerialError(err, 'BEFORE_LOAD') break @@ -1895,6 +1899,18 @@ export class Router< const parentMatchPromise = matchPromises[index - 1] const route = this.looseRoutesById[match.routeId]! + // In case the beforeLoad isn't done (such as multiple load routines running concurently) + // we need to await it, to ensure context is correctly generated + if (match.beforeLoadPromise.status === 'pending') { + await match.beforeLoadPromise + const existing = getRouteMatch(this.state, match.id)! + matches[index] = match = { + ...match, + routeContext: existing.routeContext, + context: existing.context, + } + } + const loaderContext: LoaderFnContext = { params: match.params, deps: match.loaderDeps, @@ -1910,10 +1926,8 @@ export class Router< } const fetchAndResolveInLoaderLifetime = async () => { - const existing = getRouteMatch(this.state, match.id)! let lazyPromise = Promise.resolve() let componentsPromise = Promise.resolve() as Promise - let loaderPromise = existing.loaderPromise // If the Matches component rendered // the pending component and needs to show it for @@ -1982,7 +1996,8 @@ export class Router< checkLatest() // Kick off the loader! - loaderPromise = route.options.loader?.(loaderContext) + const loaderPromise = + route.options.loader?.(loaderContext) matches[index] = match = updateMatch( match.id, @@ -1993,7 +2008,7 @@ export class Router< ) } - let loaderData = await loaderPromise + let loaderData = await match.loaderPromise if (this.serializeLoaderData) { loaderData = this.serializeLoaderData(loaderData, { router: this, diff --git a/packages/react-router/tests/routeContext.test.tsx b/packages/react-router/tests/routeContext.test.tsx index 455020a4c07..eb6aadea6b9 100644 --- a/packages/react-router/tests/routeContext.test.tsx +++ b/packages/react-router/tests/routeContext.test.tsx @@ -381,6 +381,42 @@ describe('beforeLoad in the route definition', () => { expect(mock).toHaveBeenCalledTimes(1) }) + test("on navigate (with preload), loader isn't invoked with undefined context if beforeLoad is pending when navigation happens", async () => { + const mock = vi.fn() + + const rootRoute = createRootRoute() + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + }) + const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + beforeLoad: async () => { + await sleep(WAIT_TIME) // Use a longer delay here + return { mock } + }, + loader: async ({ context }) => { + await sleep(WAIT_TIME) + context.mock() + }, + }) + + const routeTree = rootRoute.addChildren([aboutRoute, indexRoute]) + const router = createRouter({ routeTree, context: { foo: 'bar' } }) + + await router.load() + + // Don't await, simulate user clicking before preload is done + router.preloadRoute(aboutRoute) + + await router.navigate(aboutRoute) + await router.invalidate() + + // Expect double call: once from preload, once from navigate + expect(mock).toHaveBeenCalledTimes(2) + }) + // Check if context returned by /nested/about, is the same as its parent route /nested on navigate test('nested destination on navigate, route context in the /nested/about route is correctly inherited from the /nested parent', async () => { const mock = vi.fn()