diff --git a/docs/router/framework/react/api/router.md b/docs/router/framework/react/api/router.md index ad3a58f851..e760a07378 100644 --- a/docs/router/framework/react/api/router.md +++ b/docs/router/framework/react/api/router.md @@ -32,6 +32,7 @@ title: Router API - [``](../router/linkComponent.md) - [``](../router/matchRouteComponent.md) - [``](../router/navigateComponent.md) + - [``](../router/notFoundComponentComponent.md) - [``](../router/outletComponent.md) - Hooks - [`useAwaited`](../router/useAwaitedHook.md) diff --git a/docs/router/framework/react/api/router/notFoundComponentComponent.md b/docs/router/framework/react/api/router/notFoundComponentComponent.md new file mode 100644 index 0000000000..63b9f93ae0 --- /dev/null +++ b/docs/router/framework/react/api/router/notFoundComponentComponent.md @@ -0,0 +1,36 @@ +--- +id: notFoundComponentComponent +title: NotFoundComponent component +--- + +The `NotFoundComponent` component is a component that renders when a not-found error occurs in a route. + +## NotFoundComponent props + +The `NotFoundComponent` component accepts the following props: + +### `props.data` prop + +- Type: `unknown` +- Optional +- Custom data that is passed to the `notFoundComponent` when the not-found error is handled +- This data comes from the `data` property of the `NotFoundError` object + +### `props.isNotFound` prop + +- Type: `boolean` +- Required +- A boolean value indicating whether the current state is a not-found error state +- This value is always `true` + +### `props.routeId` prop + +- Type: `RouteIds` +- Required +- The ID of the route that is attempting to handle the not-found error +- Must be one of the valid route IDs from the router's route tree + +## NotFoundComponent returns + +- Returns appropriate UI for not-found error situations +- Typically includes a "page not found" message along with links to go home or navigate to previous pages diff --git a/packages/react-router/src/renderRouteNotFound.tsx b/packages/react-router/src/renderRouteNotFound.tsx index 26cf5b0928..86f31dfc22 100644 --- a/packages/react-router/src/renderRouteNotFound.tsx +++ b/packages/react-router/src/renderRouteNotFound.tsx @@ -10,7 +10,7 @@ export function renderRouteNotFound( ) { if (!route.options.notFoundComponent) { if (router.options.defaultNotFoundComponent) { - return + return } if (process.env.NODE_ENV === 'development') { @@ -23,5 +23,5 @@ export function renderRouteNotFound( return } - return + return } diff --git a/packages/react-router/tests/not-found.test.tsx b/packages/react-router/tests/not-found.test.tsx index 9bcbdbd79c..ce8bdf1207 100644 --- a/packages/react-router/tests/not-found.test.tsx +++ b/packages/react-router/tests/not-found.test.tsx @@ -9,8 +9,9 @@ import { createRootRoute, createRoute, createRouter, + notFound, } from '../src' -import type { RouterHistory } from '../src' +import type { NotFoundRouteProps, RouterHistory } from '../src' let history: RouterHistory @@ -123,3 +124,124 @@ test.each([ expect(notFoundComponent).toBeInTheDocument() }, ) + +test('defaultNotFoundComponent and notFoundComponent receives data props via spread operator', async () => { + const isCustomData = (data: unknown): data is typeof customData => { + return 'message' in (data as typeof customData) + } + + const customData = { + message: 'Custom not found message', + } + + const DefaultNotFoundComponentWithProps = (props: NotFoundRouteProps) => ( +
+ + {isCustomData(props.data) && {props.data.message}} + +
+ ) + + const rootRoute = createRootRoute({ + component: () => ( +
+

Root Component

+
+ + link to default not found route + + + link to not found route + +
+ +
+ ), + }) + + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () => ( +
+

Index Page

+
+ ), + }) + + const defaultNotFoundRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/default-not-found-route', + loader: () => { + throw notFound({ data: customData }) + }, + component: () => ( +
+ Should not render +
+ ), + }) + + const notFoundRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/not-found-route', + loader: () => { + throw notFound({ data: customData }) + }, + component: () => ( +
Should not render
+ ), + notFoundComponent: (props) => ( +
+ + {isCustomData(props.data) && {props.data.message}} + +
+ ), + }) + + const router = createRouter({ + routeTree: rootRoute.addChildren([ + indexRoute, + defaultNotFoundRoute, + notFoundRoute, + ]), + history, + defaultNotFoundComponent: DefaultNotFoundComponentWithProps, + }) + + render() + await router.load() + await screen.findByTestId('root-component') + + const defaultNotFoundRouteLink = screen.getByTestId( + 'default-not-found-route-link', + ) + defaultNotFoundRouteLink.click() + + const defaultNotFoundComponent = await screen.findByTestId( + 'default-not-found-with-props', + {}, + { timeout: 1000 }, + ) + expect(defaultNotFoundComponent).toBeInTheDocument() + + const defaultNotFoundComponentMessage = await screen.findByTestId('message') + expect(defaultNotFoundComponentMessage).toHaveTextContent(customData.message) + + const notFoundRouteLink = screen.getByTestId('not-found-route-link') + notFoundRouteLink.click() + + const notFoundComponent = await screen.findByTestId( + 'not-found-with-props', + {}, + { timeout: 1000 }, + ) + expect(notFoundComponent).toBeInTheDocument() + + const errorMessageComponent = await screen.findByTestId('message') + expect(errorMessageComponent).toHaveTextContent(customData.message) +}) diff --git a/packages/router-core/src/route.ts b/packages/router-core/src/route.ts index b89549cdc6..2a7f74fdbb 100644 --- a/packages/router-core/src/route.ts +++ b/packages/router-core/src/route.ts @@ -14,7 +14,7 @@ import type { RouteMatch, } from './Matches' import type { RootRouteId } from './root' -import type { ParseRoute, RouteById, RoutePaths } from './routeInfo' +import type { ParseRoute, RouteById, RouteIds, RoutePaths } from './routeInfo' import type { AnyRouter, RegisteredRouter } from './router' import type { BuildLocationFn, NavigateFn } from './RouterProvider' import type { @@ -1322,9 +1322,11 @@ export type ErrorComponentProps = { info?: { componentStack: string } reset: () => void } + export type NotFoundRouteProps = { - // TODO: Make sure this is `| null | undefined` (this is for global not-founds) - data: unknown + data?: unknown + isNotFound: boolean + routeId: RouteIds } export class BaseRoute< diff --git a/packages/solid-router/src/renderRouteNotFound.tsx b/packages/solid-router/src/renderRouteNotFound.tsx index b7421d863c..4603e6c0a3 100644 --- a/packages/solid-router/src/renderRouteNotFound.tsx +++ b/packages/solid-router/src/renderRouteNotFound.tsx @@ -9,7 +9,7 @@ export function renderRouteNotFound( ) { if (!route.options.notFoundComponent) { if (router.options.defaultNotFoundComponent) { - return + return } if (process.env.NODE_ENV === 'development') { @@ -22,5 +22,5 @@ export function renderRouteNotFound( return } - return + return }