diff --git a/examples/react/quickstart-file-based/src/routes/about.tsx b/examples/react/quickstart-file-based/src/routes/about.tsx index 0eda76df7cb..492e6b85c25 100644 --- a/examples/react/quickstart-file-based/src/routes/about.tsx +++ b/examples/react/quickstart-file-based/src/routes/about.tsx @@ -3,14 +3,6 @@ import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/about')({ component: AboutComponent, - beforeLoad: async () => { - await new Promise((resolve) => setTimeout(resolve, 2000)) - return { someData: 'hello' } - }, - loader: async ({ context }) => { - await new Promise((resolve) => setTimeout(resolve, 1000)) - console.debug(context.someData) - }, }) function AboutComponent() { diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts index 73ae51dd2a5..8b07e1244de 100644 --- a/packages/react-router/src/router.ts +++ b/packages/react-router/src/router.ts @@ -1542,7 +1542,9 @@ export class Router< matches: pendingMatches, location: next, checkLatest: () => this.checkLatest(promise), + // eslint-disable-next-line ts/require-await onReady: async () => { + // eslint-disable-next-line ts/require-await this.startViewTransition(async () => { // this.viewTransitionPromise = createControlledPromise() @@ -1912,6 +1914,7 @@ export class Router< handleSerialError(searchError, 'VALIDATE_SEARCH') } + // Actually run the beforeLoad function and get the context try { const beforeLoadContext = await runBeforeLoad() checkLatest() @@ -2041,6 +2044,7 @@ export class Router< } } + // Actually run the loader and handle the result try { let loaderData = await runLoader() if (this.serializeLoaderData) { diff --git a/packages/react-router/tests/createLazyRoute.test.tsx b/packages/react-router/tests/createLazyRoute.test.tsx index bb3baeebf66..ce33c4c6bb8 100644 --- a/packages/react-router/tests/createLazyRoute.test.tsx +++ b/packages/react-router/tests/createLazyRoute.test.tsx @@ -1,12 +1,21 @@ +import React, { act } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { cleanup, configure, render, screen } from '@testing-library/react' import { - RouterHistory, + Link, + RouterProvider, + createBrowserHistory, createMemoryHistory, createRootRoute, createRoute, createRouter, } from '../src' -import { cleanup } from '@testing-library/react' +import type { RouterHistory } from '../src' + +// TODO: Move this setup logic including the '@testing-library/jest-dom/vitest' into its own setup file +// @ts-expect-error +global.IS_REACT_ACT_ENVIRONMENT = true afterEach(() => { vi.resetAllMocks() @@ -18,7 +27,16 @@ function createTestRouter(initialHistory?: RouterHistory) { initialHistory ?? createMemoryHistory({ initialEntries: ['/'] }) const rootRoute = createRootRoute({}) - const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: '/' }) + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () => ( +
+

Index Route

+ Link to heavy +
+ ), + }) const heavyRoute = createRoute({ getParentRoute: () => rootRoute, @@ -36,6 +54,8 @@ function createTestRouter(initialHistory?: RouterHistory) { } describe('preload: matched routes', { timeout: 20000 }, () => { + configure({ reactStrictMode: true }) + it('should wait for lazy options to be streamed in before ', async () => { const { router } = createTestRouter( createMemoryHistory({ initialEntries: ['/'] }), @@ -55,4 +75,28 @@ describe('preload: matched routes', { timeout: 20000 }, () => { expect(lazyRoute.options.component).toBeDefined() }) + + it('should render the heavy/lazy component', async () => { + const { router } = createTestRouter(createBrowserHistory()) + + await act(() => render()) + + const linkToHeavy = await screen.findByText('Link to heavy') + expect(linkToHeavy).toBeInTheDocument() + + expect(router.state.location.pathname).toBe('/') + expect(window.location.pathname).toBe('/') + + // click the link to navigate to the heavy route + act(() => linkToHeavy.click()) + + const heavyElement = await screen.findByText('I am sooo heavy') + expect(heavyElement).toBeInTheDocument() + + expect(router.state.location.pathname).toBe('/heavy') + expect(window.location.pathname).toBe('/heavy') + + const lazyRoute = router.routesByPath['/heavy'] + expect(lazyRoute.options.component).toBeDefined() + }) }) diff --git a/packages/react-router/tests/routeContext.test.tsx b/packages/react-router/tests/routeContext.test.tsx index eb6aadea6b9..48a1222ed2f 100644 --- a/packages/react-router/tests/routeContext.test.tsx +++ b/packages/react-router/tests/routeContext.test.tsx @@ -381,7 +381,8 @@ 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 () => { + // TODO: Move this test to the loader section + test("on navigate (with preload using router methods), loader isn't invoked with undefined context if beforeLoad is pending when navigation happens", async () => { const mock = vi.fn() const rootRoute = createRootRoute() @@ -403,7 +404,10 @@ describe('beforeLoad in the route definition', () => { }) const routeTree = rootRoute.addChildren([aboutRoute, indexRoute]) - const router = createRouter({ routeTree, context: { foo: 'bar' } }) + const router = createRouter({ + routeTree, + context: { foo: 'bar' }, + }) await router.load() @@ -417,6 +421,66 @@ describe('beforeLoad in the route definition', () => { expect(mock).toHaveBeenCalledTimes(2) }) + // TODO: Move this test to the loader section + 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: '/', + component: () => { + return ( +
+

Index page

+ + link to about + +
+ ) + }, + }) + 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() + }, + component: () =>
About page
, + }) + + const routeTree = rootRoute.addChildren([aboutRoute, indexRoute]) + const router = createRouter({ + routeTree, + defaultPreload: 'intent', + context: { foo: 'bar' }, + }) + + await act(() => render()) + + const linkToAbout = await screen.findByRole('link', { + name: 'link to about', + }) + expect(linkToAbout).toBeInTheDocument() + + // Don't await, simulate user clicking before preload is done + linkToAbout.focus() + linkToAbout.click() + + const aboutElement = await screen.findByText('About page') + expect(aboutElement).toBeInTheDocument() + + expect(window.location.pathname).toBe('/about') + + // 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()