diff --git a/packages/react-router/tests/routeContext.test.tsx b/packages/react-router/tests/routeContext.test.tsx index 7a38a272d9..c439b7f690 100644 --- a/packages/react-router/tests/routeContext.test.tsx +++ b/packages/react-router/tests/routeContext.test.tsx @@ -3125,4 +3125,95 @@ describe('useRouteContext in the component', () => { expect(content).toBeInTheDocument() }) + + test("reproducer #4998 - on navigate (with preload), route component isn't rendered with undefined context if beforeLoad is pending", async () => { + const beforeLoad = vi.fn() + const select = vi.fn() + let resolved = 0 + const rootRoute = createRootRoute() + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () => To foo, + }) + const fooRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/foo', + beforeLoad: async (...args) => { + beforeLoad(...args) + await sleep(WAIT_TIME) + resolved += 1 + return { foo: resolved } + }, + component: () => { + fooRoute.useRouteContext({ select }) + return

Foo index page

+ }, + pendingComponent: () => 'loading', + }) + const routeTree = rootRoute.addChildren([indexRoute, fooRoute]) + const router = createRouter({ + routeTree, + history, + defaultPreload: 'intent', + defaultPendingMs: 0, + }) + + render() + const linkToFoo = await screen.findByText('To foo') + + fireEvent.focus(linkToFoo) + expect(beforeLoad).toHaveBeenCalledTimes(1) + expect(resolved).toBe(0) + + await sleep(WAIT_TIME) + + expect(beforeLoad).toHaveBeenCalledTimes(1) + expect(resolved).toBe(1) + expect(beforeLoad).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + cause: 'preload', + preload: true, + }), + ) + + expect(select).not.toHaveBeenCalled() + + fireEvent.click(linkToFoo) + expect(beforeLoad).toHaveBeenCalledTimes(2) + expect(resolved).toBe(1) + + await screen.findByText('Foo index page') + + expect(beforeLoad).toHaveBeenCalledTimes(2) + expect(beforeLoad).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + cause: 'enter', + preload: false, + }), + ) + expect(select).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + foo: 1, + }), + ) + expect(select).not.toHaveBeenCalledWith({}) + + await sleep(WAIT_TIME) + expect(beforeLoad).toHaveBeenCalledTimes(2) + expect(resolved).toBe(2) + + // ensure the context has been updated once the beforeLoad has resolved + expect(select).toHaveBeenLastCalledWith( + expect.objectContaining({ + foo: 2, + }), + ) + + // the route component will be rendered multiple times, ensure it always has the context + expect(select).not.toHaveBeenCalledWith({}) + }) }) diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts index c1edd5648f..a38af5379a 100644 --- a/packages/router-core/src/load-matches.ts +++ b/packages/router-core/src/load-matches.ts @@ -369,7 +369,11 @@ const executeBeforeLoad = ( const parentMatchContext = parentMatch?.context ?? inner.router.options.context ?? undefined - const context = { ...parentMatchContext, ...match.__routeContext } + const context = { + ...match.__beforeLoadContext, + ...parentMatchContext, + ...match.__routeContext, + } let isPending = false const pending = () => {