Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/router/api/router/RouteOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ The `RouteOptions` type accepts an object with the following properties:

### `pendingComponent` property

- Type: `RouteComponent` or `LazyRouteComponent`
- Type: `false`, `RouteComponent`, or `LazyRouteComponent`
- Optional - Defaults to `routerOptions.defaultPendingComponent`
- The content to be rendered if and when the route is pending and has reached its pendingMs threshold.
- Set to `false` to opt out of `routerOptions.defaultPendingComponent` for this route and render nothing instead.

### `notFoundComponent` property

Expand Down
2 changes: 1 addition & 1 deletion packages/react-router/src/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ declare module '@tanstack/router-core' {
component?: RouteComponent
errorComponent?: false | null | undefined | ErrorRouteComponent
notFoundComponent?: NotFoundRouteComponent
pendingComponent?: RouteComponent
pendingComponent?: false | null | undefined | RouteComponent
}

export interface RootRouteOptionsExtensions {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { act, cleanup, render, screen } from '@testing-library/react'
import { afterEach, expect, test, vi } from 'vitest'
import {
Outlet,
RouterProvider,
createControlledPromise,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
} from '../src'

afterEach(() => {
cleanup()
vi.useRealTimers()
})

// https://github.com/TanStack/router/issues/7773
test('pendingComponent: false suppresses the router-wide default pending fallback', async () => {
vi.useFakeTimers()

const loaderGate = createControlledPromise<string>()
const rootRoute = createRootRoute({ component: Outlet })
const optedOutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/opted-out',
pendingComponent: false,
loader: () => loaderGate,
component: () => (
<div data-testid="content">{optedOutRoute.useLoaderData()}</div>
),
})
const router = createRouter({
routeTree: rootRoute.addChildren([optedOutRoute]),
history: createMemoryHistory({ initialEntries: ['/opted-out'] }),
defaultPendingComponent: () => <div data-testid="pending">Pending</div>,
defaultPendingMs: 0,
defaultPendingMinMs: 100,
})

render(<RouterProvider router={router} />)

await act(async () => {
await vi.advanceTimersByTimeAsync(0)
Comment thread
nishant-batra marked this conversation as resolved.
Resolved
})
expect(screen.queryByTestId('pending')).not.toBeInTheDocument()
expect(screen.queryByTestId('content')).not.toBeInTheDocument()

loaderGate.resolve('loaded')

await act(async () => {
await vi.advanceTimersByTimeAsync(0)
})
expect(screen.queryByTestId('pending')).not.toBeInTheDocument()
expect(screen.getByTestId('content')).toHaveTextContent('loaded')
})

// Contrast case: confirms the type widening didn't disturb default inheritance
// for routes that don't opt out.
test('a sibling route without pendingComponent: false still uses the router-wide default', async () => {
vi.useFakeTimers()

const loaderGate = createControlledPromise<string>()
const rootRoute = createRootRoute({ component: Outlet })
const defaultRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/default',
loader: () => loaderGate,
component: () => (
<div data-testid="content">{defaultRoute.useLoaderData()}</div>
),
})
const router = createRouter({
routeTree: rootRoute.addChildren([defaultRoute]),
history: createMemoryHistory({ initialEntries: ['/default'] }),
defaultPendingComponent: () => <div data-testid="pending">Pending</div>,
defaultPendingMs: 0,
defaultPendingMinMs: 100,
})

render(<RouterProvider router={router} />)

await act(async () => {
await vi.advanceTimersByTimeAsync(0)
})
expect(screen.getByTestId('pending')).toBeInTheDocument()
expect(screen.queryByTestId('content')).not.toBeInTheDocument()

loaderGate.resolve('loaded')

await act(async () => {
await vi.advanceTimersByTimeAsync(100)
})
expect(screen.queryByTestId('pending')).not.toBeInTheDocument()
expect(screen.getByTestId('content')).toHaveTextContent('loaded')
})