diff --git a/.prettierignore b/.prettierignore
index 316b5b3cd90..fa9beff847f 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,5 +4,4 @@
**/build
**/coverage
**/dist
-**/docs
pnpm-lock.yaml
diff --git a/README.md b/README.md
index 53d8e96ba4e..e720b30082f 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,7 @@ Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Quer
- Easy Integration w/ external caches and storage (eg. React Query, Apollo, SWR, RTKQuery)
## Example Usage
+
To run example React projects with Tanstack Router, see [CONTRIBUTING.md](./CONTRIBUTING.md)
diff --git a/docs/framework/react/api/router/createRootRouteWithContextFunction.md b/docs/framework/react/api/router/createRootRouteWithContextFunction.md
index 8b6f1b2dcf2..b5a5b29e2be 100644
--- a/docs/framework/react/api/router/createRootRouteWithContextFunction.md
+++ b/docs/framework/react/api/router/createRootRouteWithContextFunction.md
@@ -22,7 +22,10 @@ The `createRootRouteWithContext` function is a helper function that can be used
### Examples
```tsx
-import { createRootRouteWithContext, createRouter } from '@tanstack/react-router'
+import {
+ createRootRouteWithContext,
+ createRouter,
+} from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>()({
diff --git a/docs/framework/react/api/router/useParamsHook.md b/docs/framework/react/api/router/useParamsHook.md
index d6da97b8669..076839ccf48 100644
--- a/docs/framework/react/api/router/useParamsHook.md
+++ b/docs/framework/react/api/router/useParamsHook.md
@@ -35,7 +35,7 @@ function Component() {
// OR
- const routeParams = routeApi.useParams();
+ const routeParams = routeApi.useParams()
// OR
diff --git a/docs/framework/react/decisions-on-dx.md b/docs/framework/react/decisions-on-dx.md
index 81c36d49cda..6c8a8f5e3fb 100644
--- a/docs/framework/react/decisions-on-dx.md
+++ b/docs/framework/react/decisions-on-dx.md
@@ -16,7 +16,7 @@ But Tanstack Router is different. It's not your average routing library. It's no
## Tanstack Router's origin story
-It's important remember that Tanstack Router's origins stem from [Nozzle.io](https://nozzle.io)'s need for a client-side routing solution that offered a first-in-class *URL Search Parameters* experience without compromising on the ***type-safety*** that was required to power its complex dashboards.
+It's important remember that Tanstack Router's origins stem from [Nozzle.io](https://nozzle.io)'s need for a client-side routing solution that offered a first-in-class _URL Search Parameters_ experience without compromising on the **_type-safety_** that was required to power its complex dashboards.
And so, from Tanstack Router's very inception, every facet of it's design was meticulously thought out to ensure that its type-safety and developer experience were second to none.
@@ -36,7 +36,7 @@ But to achieve this, we had to make some decisions that deviate from the norms i
## 1. Why is the Router's configuration done this way?
-When you want to leverage the Typescript's inference features to its fullest, you'll quickly realize that *Generics* are your best friend. And so, Tanstack Router uses Generics everywhere to ensure that the types of your routes are inferred as much as possible.
+When you want to leverage the Typescript's inference features to its fullest, you'll quickly realize that _Generics_ are your best friend. And so, Tanstack Router uses Generics everywhere to ensure that the types of your routes are inferred as much as possible.
This means that you have to define your routes in a way that allows Typescript to infer the types of your routes as much as possible.
@@ -54,7 +54,7 @@ function App() {
{/* ... */}
// ^? Typescript cannot infer the routes in this configuration
- );
+ )
}
```
@@ -67,22 +67,22 @@ And since this would mean that you'd have to manually type the `to` prop of the
const router = createRouter({
routes: {
posts: {
- component: PostsPage, // /posts
+ component: PostsPage, // /posts
children: {
- "$postId": {
- component: PostIdPage // /posts/$postId
- }
- }
+ $postId: {
+ component: PostIdPage, // /posts/$postId
+ },
+ },
},
// ...
- }
+ },
})
```
At first glance, this seems like a good idea. It's easy to visualize the entire route hierarchy in one go. But this approach has a couple big downsides that make it not ideal for large applications:
-* **It's not very scalable**: As your application grows, the tree will grow and become harder to manage. And since its all defined in one file, it can become very hard to maintain.
-* **It's not great for code-splitting**: You'd have to manually code-split each component and then pass it into the `component` property of the route, further complicating the route configuration with an ever-growing route configuration file.
+- **It's not very scalable**: As your application grows, the tree will grow and become harder to manage. And since its all defined in one file, it can become very hard to maintain.
+- **It's not great for code-splitting**: You'd have to manually code-split each component and then pass it into the `component` property of the route, further complicating the route configuration with an ever-growing route configuration file.
This only get worse as your begin to use more features of the router, such as nested context, loaders, search param validation, etc.
@@ -110,10 +110,7 @@ There were two approaches we considered for this:
import { router } from '@/src/app'
export const PostsIdLink = () => {
return (
-
- to='/posts/$postId'
- params={{ postId: '123' }}
- >
+ to="/posts/$postId" params={{ postId: '123' }}>
Go to post 123
)
@@ -141,7 +138,7 @@ And then you can benefit from its auto-complete anywhere in your app without hav
export const PostsIdLink = () => {
return (
@@ -165,11 +162,11 @@ Something you'll notice (quite soon) in the Tanstack Router documentation is tha
As mentioned in the beginning, Tanstack Router was designed for complex applications that require a high degree of type-safety and maintainability. And to achieve this, the configuration of the router has be done in an precise way that allows Typescript to infer the types of your routes as much as possible.
-A key difference in the set-up of a *basic* application with Tanstack Router, is that your route configurations require a function to be provided to `getParentRoute`, that returns the parent route of the current route.
+A key difference in the set-up of a _basic_ application with Tanstack Router, is that your route configurations require a function to be provided to `getParentRoute`, that returns the parent route of the current route.
```tsx
-import { createRoute } from '@tanstack/react-router';
-import { postsRoute } from './postsRoute';
+import { createRoute } from '@tanstack/react-router'
+import { postsRoute } from './postsRoute'
export const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
@@ -181,36 +178,28 @@ At this stage, this is done so the definition of `postsIndexRoute` can be aware
As such, this is a critical part of the route configuration and a point of failure if not done correctly.
-But this is only one part of setting up a basic application. Tanstack Router requires the all the routes (including the root route) to be stitched into a ***route-tree*** so that it may be passed into the `createRouter` function before declaring the `Router` instance on the module for type inference. This is another critical part of the route configuration and a point of failure if not done correctly.
+But this is only one part of setting up a basic application. Tanstack Router requires the all the routes (including the root route) to be stitched into a **_route-tree_** so that it may be passed into the `createRouter` function before declaring the `Router` instance on the module for type inference. This is another critical part of the route configuration and a point of failure if not done correctly.
> 🤯 If this route-tree were in its own file for an application with ~40-50 routes, it can easily grow up to 700+ lines.
```tsx
const routeTree = rootRoute.addChildren([
- postsRoute.addChildren([
- postsIndexRoute,
- postsIdRoute
- ])
+ postsRoute.addChildren([postsIndexRoute, postsIdRoute]),
])
```
-This complexity only increases as you begin to use more features of the router, such as nested context, loaders, search param validation, etc. As such, it no longer becomes feasible to define your routes in a single file. And so, users end up building their own *semi consistent* way of defining their routes across multiple files. This can lead to inconsistencies and errors in the route configuration.
+This complexity only increases as you begin to use more features of the router, such as nested context, loaders, search param validation, etc. As such, it no longer becomes feasible to define your routes in a single file. And so, users end up building their own _semi consistent_ way of defining their routes across multiple files. This can lead to inconsistencies and errors in the route configuration.
Finally, comes the issue of code-splitting. As your application grows, you'll want to code-split your components to reduce the initial bundle size of your application. This can be a bit of a headache to manage when you're defining your routes in a single file or even across multiple files.
```tsx
-import {
- createRoute,
- lazyRouteComponent
-} from '@tanstack/react-router';
-import { postsRoute } from './postsRoute';
+import { createRoute, lazyRouteComponent } from '@tanstack/react-router'
+import { postsRoute } from './postsRoute'
export const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
- component: lazyRouteComponent(
- () => import('../page-components/posts/index')
- )
+ component: lazyRouteComponent(() => import('../page-components/posts/index')),
})
```
@@ -225,17 +214,17 @@ Tanstack Router's file-based routing is designed to solve all of these issues. I
The file-based routing approach is powered by the Tanstack Router CLI. It performs 3 essential tasks that solve the pain points in route configuration when using code-based routing:
1. **Route configuration boilerplate**: It generates the boilerplate for your route configurations.
-2. **Route tree stitching**: It stitches together your route configurations into a single cohesive route-tree. Also in the background, it correctly updates the route configurations to define the `getParentRoute` function match the routes with their parent routes.
+2. **Route tree stitching**: It stitches together your route configurations into a single cohesive route-tree. Also in the background, it correctly updates the route configurations to define the `getParentRoute` function match the routes with their parent routes.
3. **Code-splitting**: It automatically code-splits your components and handles updating your route configurations with the correct lazy imports.
Let's take a look at how the route configuration for the previous example would look like with file-based routing.
```tsx
// src/routes/posts/index.lazy.ts
-import { createLazyFileRoute } from '@tanstack/react-router';
+import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/posts/')({
- component: () => "Posts index component goes here!!!"
+ component: () => 'Posts index component goes here!!!',
})
```
@@ -243,4 +232,4 @@ That's it! No need to worry about defining the `getParentRoute` function, stitch
At no point does the Tanstack Router CLI take away your control over your route configurations. It's designed to be as flexible as possible, allowing you to define your routes in a way that suits your application whilst reducing the boilerplate and complexity of the route configuration.
-> 🧠 Check out the guides for [file-based routing](./guide/file-based-routing) and [code-splitting](./guide/code-splitting) for a more in-depth explanation of how they work in Tanstack Router.
\ No newline at end of file
+> 🧠 Check out the guides for [file-based routing](./guide/file-based-routing) and [code-splitting](./guide/code-splitting) for a more in-depth explanation of how they work in Tanstack Router.
diff --git a/docs/framework/react/devtools.md b/docs/framework/react/devtools.md
index b7f39728111..e23650236e1 100644
--- a/docs/framework/react/devtools.md
+++ b/docs/framework/react/devtools.md
@@ -40,11 +40,11 @@ const TanStackRouterDevtools =
)
```
-Then wrap the `TanStackRouterDevtools` component in suspense.
+Then wrap the `TanStackRouterDevtools` component in suspense.
```tsx
-
+
```
diff --git a/docs/framework/react/guide/code-based-routing.md b/docs/framework/react/guide/code-based-routing.md
index 8cd6bb32094..b1e3f011166 100644
--- a/docs/framework/react/guide/code-based-routing.md
+++ b/docs/framework/react/guide/code-based-routing.md
@@ -176,6 +176,7 @@ function PostComponent() {
return
Post ID: {postId}
}
```
+
> 🧠 Quick tip: If your component is code-split, you can use the [getRouteApi function](./guide/code-splitting#manually-accessing-route-apis-in-other-files-with-the-routeapi-class) to avoid having to import the `postIdRoute` configuration to get access to the typed `useParams()` hook.
## Splat / Catch-All Routes
diff --git a/docs/framework/react/guide/data-mutations.md b/docs/framework/react/guide/data-mutations.md
index ab1c1ddcddd..3630e46506b 100644
--- a/docs/framework/react/guide/data-mutations.md
+++ b/docs/framework/react/guide/data-mutations.md
@@ -70,7 +70,7 @@ Without notifying your mutation management library about the route change, it's
Hopefully and hypothetically, the easiest way is for your mutation library to support a keying mechanism that will allow your mutations's state to be reset when the key changes:
```tsx
-const routeApi = getRouteApi("/posts/$postId/edit")
+const routeApi = getRouteApi('/posts/$postId/edit')
function EditPost() {
const { roomId } = routeApi.useParams()
diff --git a/docs/framework/react/guide/deferred-data-loading.md b/docs/framework/react/guide/deferred-data-loading.md
index d0a0a7be915..87c5378123f 100644
--- a/docs/framework/react/guide/deferred-data-loading.md
+++ b/docs/framework/react/guide/deferred-data-loading.md
@@ -63,6 +63,7 @@ function PostIdComponent() {
)
}
```
+
> 🧠 Quick tip: If your component is code-split, you can use the [getRouteApi function](./guide/code-splitting#manually-accessing-route-apis-in-other-files-with-the-routeapi-class) to avoid having to import the `Route` configuration to get access to the typed `useLoaderData()` hook.
The `Await` component resolves the promise by triggering the nearest suspense boundary until it is resolved, after which it renders the component's `children` as a function with the resolved data.
diff --git a/docs/framework/react/guide/file-based-routing.md b/docs/framework/react/guide/file-based-routing.md
index c76741b8181..990cd9c80c3 100644
--- a/docs/framework/react/guide/file-based-routing.md
+++ b/docs/framework/react/guide/file-based-routing.md
@@ -125,7 +125,7 @@ File-based routing requires that you follow a few simple file naming conventions
- **`.route.tsx` File Type**
- When using directories to organize your routes, the `route` suffix can be used to create a route file at the directory's path. For example, `blog/post/route.tsx` will be used at the route file for the `/blog/post` route.
- **`.lazy.tsx` File Type**
- - The `lazy` suffix can be used to code-split components for a route. For example, `blog.post.lazy.tsx` will be used as the component for the `blog.post` route.
+ - The `lazy` suffix can be used to code-split components for a route. For example, `blog.post.lazy.tsx` will be used as the component for the `blog.post` route.
- **`.component.tsx` File Type (⚠️ deprecated)**
- **`.errorComponent.tsx` File Type (⚠️ deprecated)**
- **`.pendingComponent.tsx` File Type (⚠️ deprecated)**
diff --git a/docs/framework/react/guide/history-types.md b/docs/framework/react/guide/history-types.md
index 617af901027..c65afccd277 100644
--- a/docs/framework/react/guide/history-types.md
+++ b/docs/framework/react/guide/history-types.md
@@ -13,10 +13,7 @@ If you don't create a history instance, a browser-oriented instance of this API
Once you have a history instance, you can pass it to the `Router` constructor:
```ts
-import {
- createMemoryHistory,
- createRouter,
-} from '@tanstack/react-router'
+import { createMemoryHistory, createRouter } from '@tanstack/react-router'
const memoryHistory = createMemoryHistory({
initialEntries: ['/'], // Pass your initial url
@@ -34,10 +31,7 @@ The `createBrowserHistory` is the default history type. It uses the browser's hi
Hash routing can be helpful if your server doesn't support rewrites to index.html for HTTP requests (among other environments that don't have a server).
```ts
-import {
- createHashHistory,
- createRouter,
-} from '@tanstack/react-router'
+import { createHashHistory, createRouter } from '@tanstack/react-router'
const hashHistory = createHashHistory()
@@ -49,10 +43,7 @@ const router = createRouter({ routeTree, history: hashHistory })
Memory routing is useful in environments that are not a browser or when you do not want components to interact with the URL.
```ts
-import {
- createMemoryHistory,
- createRouter,
-} from '@tanstack/react-router'
+import { createMemoryHistory, createRouter } from '@tanstack/react-router'
const memoryHistory = createMemoryHistory({
initialEntries: ['/'], // Pass your initial url
diff --git a/docs/framework/react/guide/not-found-errors.md b/docs/framework/react/guide/not-found-errors.md
index 2307f7ac04b..4aa6f71f628 100644
--- a/docs/framework/react/guide/not-found-errors.md
+++ b/docs/framework/react/guide/not-found-errors.md
@@ -4,11 +4,145 @@ title: Not Found Errors
> ⚠️ This page covers the newer `notFound` function and `notFoundComponent` API for handling not found errors. The `NotFoundRoute` route is deprecated and will be removed in a future release. See [Migrating from `NotFoundRoute`](#migrating-from-notfoundroute) for more information.
-Not-found errors are a special class of errors that may be thrown in loader methods and components to signal that a resource cannot be found. TanStack Router has a special API for handling these errors, similar to Next.js' own not-found API.
+## Overview
-Beyond being able to display a not-found, TanStack Router also lets you specify where a not-found error gets handled. This allows you to handle not-found errors in a way that preserves layouts.
+There are 2 uses for not-found errors in TanStack Router:
-## `notFound` and `notFoundComponent`
+- **Non-matching route paths**: When a path does not match any known route matching pattern **OR** when it partially matches a route, but with extra path segments
+ - The **router** will automatically throw a not-found error when a path does not match any known route matching pattern
+ - If the router's `notFoundMode` is set to `fuzzy`, the nearest parent route with a `notFoundComponent` will handle the error. If the router's `notFoundMode` is set to `root`, the root route will handle the error.
+ - Examples:
+ - Attempting to access `/users` when there is no `/users` route
+ - Attempting to access `/posts/1/edit` when the route tree only handles `/posts/$postId`
+- **Missing resources**: When a resource cannot be found, such as a post with a given ID or any asynchronous data that is not available or does not exist
+ - **You, the developer** must throw a not-found error when a resource cannot be found. This can be done in `beforeLoad`, `loader`, or components using the `notFound` utility.
+ - Will be handled by the nearest parent route with a `notFoundComponent` or the root route
+ - Examples:
+ - Attempting to access `/posts/1` when the post with ID 1 does not exist
+ - Attempting to access `/docs/path/to/document` when the document does not exist
+
+Under the hood, both of these cases are implemented using the same `notFound` function and `notFoundComponent` API.
+
+## The `notFoundMode` option
+
+When TanStack Router encounters a **pathname** that doesn't match any known route pattern **OR** partially matches a route pattern but with extra trailing pathname segments, it will automatically throw a not-found error.
+
+Depending on the `notFoundMode` option, the router will handle these automatic errors differently::
+
+- ["fuzzy" mode](#notfoundmode-fuzzy) (default): The router will intelligently find the closest matching suitable route and display the `notFoundComponent`.
+- ["root" mode](#notfoundmode-root): All not-found errors will be handled by the root route's `notFoundComponent`, regardless of the nearest matching route.
+
+### `notFoundMode: 'fuzzy'`
+
+By default, the router's `notFoundMode` is set to `fuzzy`, which indicates that if a pathname doesn't match any known route, the router will attempt to use the closest matching route with children/(an outlet) and a configured not found component.
+
+> **❓ Why is this the default?** Fuzzy matching to preserve as much parent layout as possible for the user gives them more context to navigate to a useful location based on where they thought they would arrive.
+
+The nearest suitable route is found using the following criteria:
+
+- The route must have children and therefore an `Outlet` to render the `notFoundComponent`
+- The route must have a `notFoundComponent` configured or the router must have a `defaultNotFoundComponent` configured
+
+For example, consider the following route tree:
+
+- `__root__` (has a `notFoundComponent` configured)
+ - `posts` (has a `notFoundComponent` configured)
+ - `$postId` (has a `notFoundComponent` configured)
+
+If provided the path of `/posts/1/edit`, the following component structure will be rendered:
+
+- ``
+ - ``
+ - ``
+
+The `notFoundComponent` of the `posts` route will be rendered because it is the **nearest suitable parent route with children (and therefore an outlet) and a `notFoundComponent` configured**.
+
+### `notFoundMode: 'root'`
+
+When `notFoundMode` is set to `root`, all not-found errors will be handled by the root route's `notFoundComponent` instead of bubbling up from the nearest fuzzy-matched route.
+
+For example, consider the following route tree:
+
+- `__root__` (has a `notFoundComponent` configured)
+ - `posts` (has a `notFoundComponent` configured)
+ - `$postId` (has a `notFoundComponent` configured)
+
+If provided the path of `/posts/1/edit`, the following component structure will be rendered:
+
+- ``
+ - ``
+
+The `notFoundComponent` of the `__root__` route will be rendered because the `notFoundMode` is set to `root`.
+
+## Configuring a route's `notFoundComponent`
+
+To handle both types of not-found errors, you can attach a `notFoundComponent` to a route. This component will be rendered when a not-found error is thrown.
+
+For example, configuring a `notFoundComponent` for a `/settings` route to handle non-existing settings pages:
+
+```tsx
+export const Route = createFileRoute('/settings')({
+ component: () => {
+ return (
+
+
Settings page
+
+
+ )
+ },
+ notFoundComponent: () => {
+ return
This setting page doesn't exist!
+ },
+})
+```
+
+Or configuring a `notFoundComponent` for a `/posts/$postId` route to handle posts that don't exist:
+
+```tsx
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => {
+ const post = await getPost(postId)
+ if (!post) throw notFound()
+ return { post }
+ },
+ component: ({ post }) => {
+ return (
+
+
{post.title}
+
{post.body}
+
+ )
+ },
+ notFoundComponent: () => {
+ return
Post not found!
+ },
+})
+```
+
+## Default Router-Wide Not Found Handling
+
+You may want to provide a default not-found component for every route in your app with child routes.
+
+> Why only routes with children? **Leaf-node routes (routes without children) will never render an `Outlet` and therefore are not able to handle not-found errors.**
+
+To do this, pass a `defaultNotFoundComponent` to the `createRouter` function:
+
+```tsx
+const router = createRouter({
+ defaultNotFoundComponent: () => {
+ return (
+
+
Not found!
+ Go home
+
+ )
+ },
+})
+```
+
+## Throwing your own `notFound` errors
+
+You can manually throw not-found errors in loader methods and components using the `notFound` function. This is useful when you need to signal that a resource cannot be found.
The `notFound` function works in a similar fashion to the `redirect` function. To cause a not-found error, you can **throw a `notFound()`**.
@@ -28,24 +162,13 @@ export const Route = createFileRoute('/posts/$postId')({
})
```
-To handle a not-found error, attach a `notFoundComponent` to the route or **any parent route**.
-
-```tsx
-export const Route = createFileRoute('/posts/$postId')({
- loader: async ({ params: { postId } }) => {
- // -- see above --
- },
- notFoundComponent: () => {
- return
Post not found!
- },
-})
-```
+The not-found error above will be handled by the same route or nearest parent route that has either a `notFoundComponent` route option or the `defaultNotFoundComponent` router option configured.
-**If the route you are throwing a not-found error doesn't have a `notFoundComponent` to handle the error,** TanStack Router will check the parent routes of the route where the error was thrown and find a route that defines a `notFoundComponent`. If no routes are able to handle the error, the root route will handle it with a default component.
+If neither the route nor any suitable parent route is found to handle the error, the root route will handle it using TanStack Router's **extremely basic (and purposefully undesirable)** default not-found component that simply renders `
Not Found
`. It's highly recommended to either attach at least one `notFoundComponent` to the root route or configure a router-wide `defaultNotFoundComponent` to handle not-found errors.
-### Specifying Which Routes Handle Not Found Errors
+## Specifying Which Routes Handle Not Found Errors
-With just calling the `notFound` function, TanStack Router will try resolving a `notFoundComponent` starting from the route which threw it. If you need to trigger a not-found on a specific parent route, you can pass in a route id to the `route` option in the `notFound` function.
+Sometimes you may want to trigger a not-found on a specific parent route and bypass the normal not-found component propagation. To do this, pass in a route id to the `route` option in the `notFound` function.
```tsx
// _layout.tsx
@@ -78,9 +201,21 @@ export const Route = createFileRoute('/_layout/a')({
})
```
-### "Global" Not Found Errors
+### Manually targeting the root route
+
+You can also target the root route by passing the exported `rootRouteId` variable to the `notFound` function's `route` property:
+
+```tsx
+import { rootRouteId } from '@tanstack/react-router'
-"Global" not-found errors are not-founds on the root route. These errors occur when TanStack Router can't match a route for a given path or when a route throws a not-found error that is marked as `global: true` in its options. To handle these errors, attach a `notFoundComponent` to the root route.
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => {
+ const post = await getPost(postId)
+ if (!post) throw notFound({ route: rootRouteId })
+ return { post }
+ },
+})
+```
### Throwing Not Found Errors in Components
diff --git a/docs/framework/react/guide/path-params.md b/docs/framework/react/guide/path-params.md
index 5b03c5355c7..8c3c02f842b 100644
--- a/docs/framework/react/guide/path-params.md
+++ b/docs/framework/react/guide/path-params.md
@@ -67,6 +67,7 @@ function PostComponent() {
return
Post {postId}
}
```
+
> 🧠 Quick tip: If your component is code-split, you can use the [getRouteApi function](./guide/code-splitting#manually-accessing-route-apis-in-other-files-with-the-routeapi-class) to avoid having to import the `Route` configuration to get access to the typed `useParams()` hook.
## Path Params outside of Routes
diff --git a/docs/framework/react/guide/route-trees.md b/docs/framework/react/guide/route-trees.md
index 634e3f5caa7..7e90280e2ca 100644
--- a/docs/framework/react/guide/route-trees.md
+++ b/docs/framework/react/guide/route-trees.md
@@ -64,23 +64,23 @@ Route trees be represented using a number of different ways:
Flat routing uses same level of nesting. They make it easy to see and find routes in your project:
-| Filename | Route Path | Component Output |
-| ----------------------------- | ------------------------- | --------------------------------- |
-| `__root.tsx` | | `` |
-| `index.tsx` | `/` (exact) | `` |
-| `about.tsx` | `/about` | `` |
-| `posts.tsx` | `/posts` | `` |
-| `posts.index.tsx` | `/posts` (exact) | `` |
-| `posts.$postId.tsx` | `/posts/$postId` | `` |
-| `posts_.$postId.edit.tsx` | `/posts/$postId/edit` | `` |
-| `settings.tsx` | `/settings` | `` |
-| `settings.profile.tsx` | `/settings/profile` | `` |
-| `settings.notifications.tsx` | `/settings/notifications` | `` |
-| `_layout.tsx` | | `` |
-| `_layout.layout-a.tsx` | `/layout-a` | `` |
-| `_layout.layout-b.tsx` | `/layout-b` | `` |
-| `files.$.tsx` | `/files/$` | `` |
-| `__404.tsx` | (Not Found) | `` |
+| Filename | Route Path | Component Output |
+| ---------------------------- | ------------------------- | --------------------------------- |
+| `__root.tsx` | | `` |
+| `index.tsx` | `/` (exact) | `` |
+| `about.tsx` | `/about` | `` |
+| `posts.tsx` | `/posts` | `` |
+| `posts.index.tsx` | `/posts` (exact) | `` |
+| `posts.$postId.tsx` | `/posts/$postId` | `` |
+| `posts_.$postId.edit.tsx` | `/posts/$postId/edit` | `` |
+| `settings.tsx` | `/settings` | `` |
+| `settings.profile.tsx` | `/settings/profile` | `` |
+| `settings.notifications.tsx` | `/settings/notifications` | `` |
+| `_layout.tsx` | | `` |
+| `_layout.layout-a.tsx` | `/layout-a` | `` |
+| `_layout.layout-b.tsx` | `/layout-b` | `` |
+| `files.$.tsx` | `/files/$` | `` |
+| `__404.tsx` | (Not Found) | `` |
## Directory Routes
diff --git a/docs/framework/react/guide/router-context.md b/docs/framework/react/guide/router-context.md
index 4eaaee399b6..e1c7657c9c0 100644
--- a/docs/framework/react/guide/router-context.md
+++ b/docs/framework/react/guide/router-context.md
@@ -18,7 +18,10 @@ These are just suggested uses of the router context. You can use it for whatever
Like everything else, the root router context is strictly typed. This type can be augmented via any route's `beforeLoad` option as it is merged down the route match tree. To constrain the type of the root router context, you must use the `createRootRouteWithContext()(routeOptions)` function to create a new router context instead of the `createRootRoute()` function to create your root route. Here's an example:
```tsx
-import { createRootRouteWithContext, createRouter } from '@tanstack/react-router'
+import {
+ createRootRouteWithContext,
+ createRouter,
+} from '@tanstack/react-router'
interface MyRouterContext {
user: User
@@ -105,7 +108,10 @@ export const Route = createFileRoute('/todos')({
### How about an external data fetching library?
```tsx
-import { createRootRouteWithContext, createRouter } from '@tanstack/react-router'
+import {
+ createRootRouteWithContext,
+ createRouter,
+} from '@tanstack/react-router'
interface MyRouterContext {
queryClient: QueryClient
diff --git a/docs/framework/react/guide/search-params.md b/docs/framework/react/guide/search-params.md
index 0787c67e5dd..c58aefd9063 100644
--- a/docs/framework/react/guide/search-params.md
+++ b/docs/framework/react/guide/search-params.md
@@ -247,8 +247,8 @@ const allProductsRoute = createRoute({
const routeApi = getRouteApi('/shop/products')
const ProductList = () => {
- const routeSearch = routeApi.useSearch();
-
+ const routeSearch = routeApi.useSearch()
+
// OR
const { page, filter, sort } = useSearch({
diff --git a/docs/framework/react/quick-start.md b/docs/framework/react/quick-start.md
index cf5995b4dca..894bc9d88c6 100644
--- a/docs/framework/react/quick-start.md
+++ b/docs/framework/react/quick-start.md
@@ -69,7 +69,7 @@ export const Route = createRootRoute({
### `src/routes/index.lazy.tsx`
```tsx
-import { createLazyFileRoute } from '@tanstack/react-router';
+import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/')({
component: Index,
@@ -87,7 +87,7 @@ function Index() {
### `src/routes/about.lazy.tsx`
```tsx
-import { createLazyFileRoute } from '@tanstack/react-router';
+import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/about')({
component: About,
@@ -211,12 +211,9 @@ if (!rootElement.innerHTML) {
)
}
```
-## Using File-Based Route Configuration
-
-If you are working with this pattern you should change the `id` of the root `
` on your `index.html` file to ``````
-
-
+## Using File-Based Route Configuration
+If you are working with this pattern you should change the `id` of the root `
` on your `index.html` file to ``
If you glossed over these examples or didn't understand something, we don't blame you, because there's so much more to learn to really take advantage of TanStack Router! Let's move on.
diff --git a/packages/react-router-server/src/client.tsx b/packages/react-router-server/src/client.tsx
index e118f5b5ec2..d01ec971de8 100644
--- a/packages/react-router-server/src/client.tsx
+++ b/packages/react-router-server/src/client.tsx
@@ -198,6 +198,7 @@ export const Scripts = () => {
tag: 'script',
attrs: {
...script,
+ suppressHydrationWarning: true,
key: `script-${script.src}`,
},
children,
diff --git a/packages/react-router-server/src/server-fns/fetcher.tsx b/packages/react-router-server/src/server-fns/fetcher.tsx
index 9afefb09bfd..dfb27abb86c 100644
--- a/packages/react-router-server/src/server-fns/fetcher.tsx
+++ b/packages/react-router-server/src/server-fns/fetcher.tsx
@@ -55,7 +55,8 @@ export async function fetcher(
let response = await handleResponseErrors(handlerResponse)
if (['json'].includes(response.headers.get(serverFnReturnTypeHeader)!)) {
- const json = await response.json()
+ const text = await response.text()
+ const json = text ? JSON.parse(text) : undefined
// If the response is a redirect or not found, throw it
// for the router to handle
@@ -85,13 +86,14 @@ export async function fetcher(
// If the response is JSON, return it parsed
const contentType = response.headers.get('content-type')
+ const text = await response.text()
if (contentType && contentType.includes('application/json')) {
- return response.json()
+ return text ? JSON.parse(text) : undefined
} else {
// Otherwise, return the text as a fallback
// If the user wants more than this, they can pass a
// request instead
- return response.text()
+ return text
}
}
async function handleResponseErrors(response: Response) {
diff --git a/packages/react-router-server/src/server-fns/handler.tsx b/packages/react-router-server/src/server-fns/handler.tsx
index 65e5f63e7a4..8ab5529d433 100644
--- a/packages/react-router-server/src/server-fns/handler.tsx
+++ b/packages/react-router-server/src/server-fns/handler.tsx
@@ -23,87 +23,98 @@ export async function handleRequest(request: Request) {
invariant(typeof serverFnId === 'string', 'Invalid server action')
console.info(`ServerFn Request: ${serverFnId} - ${serverFnName}`)
+ console.info()
const action = (
await getManifest('server').chunks[serverFnId!]?.import?.()
)?.[serverFnName!] as Function
- try {
- const args = await (async () => {
- if (request.headers.get(serverFnPayloadTypeHeader) === 'payload') {
- return [
- method.toLowerCase() === 'get'
- ? (() => {
- const { _serverFnId, _serverFnName, ...rest } = search
- return rest
- })()
- : await request.json(),
- { method, request },
- ] as const
+ const response = await (async () => {
+ try {
+ const args = await (async () => {
+ if (request.headers.get(serverFnPayloadTypeHeader) === 'payload') {
+ return [
+ method.toLowerCase() === 'get'
+ ? (() => {
+ const { _serverFnId, _serverFnName, ...rest } = search
+ return rest
+ })()
+ : await request.json(),
+ { method, request },
+ ] as const
+ }
+
+ if (request.headers.get(serverFnPayloadTypeHeader) === 'request') {
+ return [request, { method, request }] as const
+ }
+
+ // payload type === 'args'
+ return (await request.json()) as any[]
+ })()
+
+ let result = await action.apply(null, args)
+
+ if (result instanceof Response) {
+ return result
}
- if (request.headers.get(serverFnPayloadTypeHeader) === 'request') {
- return [request, { method, request }] as const
+ const response = new Response(
+ result !== undefined ? JSON.stringify(result) : undefined,
+ {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json',
+ [serverFnReturnTypeHeader]: 'json',
+ },
+ },
+ )
+
+ return response
+ } catch (error: any) {
+ // Currently this server-side context has no idea how to
+ // build final URLs, so we need to defer that to the client.
+ // The client will check for __redirect and __notFound keys,
+ // and if they exist, it will handle them appropriately.
+
+ if (isRedirect(error) || isNotFound(error)) {
+ return new Response(JSON.stringify(error), {
+ headers: {
+ 'Content-Type': 'application/json',
+ [serverFnReturnTypeHeader]: 'json',
+ },
+ })
}
- // payload type === 'args'
- return (await request.json()) as any[]
- })()
-
- console.info(` Payload: ${JSON.stringify(args, null, 2)}`)
-
- let result = await action.apply(null, args)
-
- if (result instanceof Response) {
- return result
- }
+ console.error('Server Fn Error!')
+ console.error(error)
+ console.info()
- const response = new Response(JSON.stringify(result ?? null), {
- status: 200,
- headers: {
- 'Content-Type': 'application/json',
- [serverFnReturnTypeHeader]: 'json',
- },
- })
-
- return response
- } catch (error: any) {
- // Currently this server-side context has no idea how to
- // build final URLs, so we need to defer that to the client.
- // The client will check for __redirect and __notFound keys,
- // and if they exist, it will handle them appropriately.
-
- if (isRedirect(error)) {
- // TODO: Use a common variable for the __redirect key
return new Response(JSON.stringify(error), {
+ status: 500,
headers: {
'Content-Type': 'application/json',
- [serverFnReturnTypeHeader]: 'json',
+ [serverFnReturnTypeHeader]: 'error',
},
})
}
-
- if (isNotFound(error)) {
- // TODO: Use a common variable for the __notFound key
- return new Response(JSON.stringify(error), {
- headers: {
- 'Content-Type': 'application/json',
- [serverFnReturnTypeHeader]: 'json',
- },
- })
- }
-
- console.error('Server Fn Error!')
- console.error(error)
-
- return new Response(JSON.stringify(error), {
- status: 500,
- headers: {
- 'Content-Type': 'application/json',
- [serverFnReturnTypeHeader]: 'error',
- },
- })
+ })()
+
+ console.info(`ServerFn Response: ${response.status}`)
+ if (
+ response.status === 200 &&
+ response.headers.get('Content-Type') === 'application/json'
+ ) {
+ const cloned = response.clone()
+ const text = await cloned.text()
+ const payload = text ? JSON.stringify(JSON.parse(text)) : 'undefined'
+
+ console.info(
+ ` - Payload: ${payload.length > 100 ? payload.substring(0, 100) + '...' : payload}`,
+ )
}
+ console.info()
+
+ return response
} else {
throw new Error('Invalid request')
}
diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx
index d4855959a43..06ef8c0bbc5 100644
--- a/packages/react-router/src/Matches.tsx
+++ b/packages/react-router/src/Matches.tsx
@@ -10,8 +10,6 @@ import {
ReactNode,
RootSearchSchema,
StaticDataRouteOption,
- UpdatableStaticRouteOption,
- rootRouteId,
} from './route'
import {
AllParams,
@@ -22,14 +20,17 @@ import {
RouteIds,
RoutePaths,
} from './routeInfo'
-import { RegisteredRouter, RouterState } from './router'
-import { DeepPartial, Expand, NoInfer, StrictOrFrom, pick } from './utils'
+import { AnyRouter, RegisteredRouter, RouterState } from './router'
import {
- CatchNotFound,
- DefaultGlobalNotFound,
- NotFoundError,
- isNotFound,
-} from './not-found'
+ DeepPartial,
+ Expand,
+ NoInfer,
+ StrictOrFrom,
+ isServer,
+ pick,
+} from './utils'
+import { CatchNotFound, DefaultGlobalNotFound, isNotFound } from './not-found'
+import { isRedirect } from './redirects'
export const matchContext = React.createContext(undefined)
@@ -44,7 +45,7 @@ export interface RouteMatch<
params: TReturnIntersection extends false
? RouteById['types']['allParams']
: Expand>>
- status: 'pending' | 'success' | 'error'
+ status: 'pending' | 'success' | 'error' | 'redirected' | 'notFound'
isFetching: boolean
showPending: boolean
error: unknown
@@ -74,7 +75,7 @@ export interface RouteMatch<
links?: JSX.IntrinsicElements['link'][]
scripts?: JSX.IntrinsicElements['script'][]
headers?: Record
- notFoundError?: NotFoundError
+ globalNotFound?: boolean
staticData: StaticDataRouteOption
}
@@ -173,13 +174,16 @@ export function Match({ matchId }: { matchId: string }) {
>
{
- // If the current not found handler doesn't exist or doesn't handle global not founds, forward it up the tree
- if (!routeNotFoundComponent || (error.global && !route.isRoot))
+ // If the current not found handler doesn't exist or it has a
+ // route ID which doesn't match the current route, rethrow the error
+ if (
+ !routeNotFoundComponent ||
+ (error.routeId && error.routeId !== routeId) ||
+ (!error.routeId && !route.isRoot)
+ )
throw error
- return React.createElement(routeNotFoundComponent, {
- data: error.data,
- })
+ return React.createElement(routeNotFoundComponent, error as any)
}}
>
@@ -212,24 +216,49 @@ function MatchInner({
'error',
'showPending',
'loadPromise',
- 'notFoundError',
]),
})
- // If a global not-found is found, and it's the root route, render the global not-found component.
- if (match.notFoundError) {
- if (routeId === rootRouteId && !route.options.notFoundComponent)
- return
+ const RouteErrorComponent =
+ (route.options.errorComponent ?? router.options.defaultErrorComponent) ||
+ ErrorComponent
- invariant(
- route.options.notFoundComponent,
- 'Route matched with notFoundError should have a notFoundComponent',
+ if (match.status === 'notFound') {
+ invariant(isNotFound(match.error), 'Expected a notFound error')
+
+ return renderRouteNotFound(router, route, match.error.data)
+ }
+
+ if (match.status === 'redirected') {
+ // Redirects should be handled by the router transition. If we happen to
+ // encounter a redirect here, it's a bug. Let's warn, but render nothing.
+ invariant(isRedirect(match.error), 'Expected a redirect error')
+
+ warning(
+ false,
+ 'Tried to render a redirected route match! This is a weird circumstance, please file an issue!',
)
- return
+ return null
}
if (match.status === 'error') {
+ // If we're on the server, we need to use React's new and super
+ // wonky api for throwing errors from a server side render inside
+ // of a suspense boundary. This is the only way to get
+ // renderToPipeableStream to not hang indefinitely.
+ // We'll serialize the error and rethrow it on the client.
+ if (isServer) {
+ return (
+
+ )
+ }
+
if (isServerSideError(match.error)) {
const deserializeError =
router.options.errorSerializer?.deserialize ?? defaultDeserializeError
@@ -263,7 +292,28 @@ function MatchInner({
}
export const Outlet = React.memo(function Outlet() {
+ const router = useRouter()
const matchId = React.useContext(matchContext)
+ const routeId = useRouterState({
+ select: (s) =>
+ getRenderedMatches(s).find((d) => d.id === matchId)?.routeId as string,
+ })
+
+ const route = router.routesById[routeId]!
+
+ const { parentGlobalNotFound } = useRouterState({
+ select: (s) => {
+ const matches = getRenderedMatches(s)
+ const parentMatch = matches.find((d) => d.id === matchId)
+ invariant(
+ parentMatch,
+ `Could not find parent match for matchId "${matchId}"`,
+ )
+ return {
+ parentGlobalNotFound: parentMatch.globalNotFound,
+ }
+ },
+ })
const childMatchId = useRouterState({
select: (s) => {
@@ -273,6 +323,10 @@ export const Outlet = React.memo(function Outlet() {
},
})
+ if (parentGlobalNotFound) {
+ return renderRouteNotFound(router, route, undefined)
+ }
+
if (!childMatchId) {
return null
}
@@ -280,6 +334,25 @@ export const Outlet = React.memo(function Outlet() {
return
})
+function renderRouteNotFound(router: AnyRouter, route: AnyRoute, data: any) {
+ if (!route.options.notFoundComponent) {
+ if (router.options.defaultNotFoundComponent) {
+ return
+ }
+
+ if (process.env.NODE_ENV === 'development') {
+ warning(
+ route.options.notFoundComponent,
+ `A notFoundError was encountered on the route with ID "${route.id}", but a notFoundComponent option was not configured, nor was a router level defaultNotFoundComponent configured. Consider configuring at least one of these to avoid TanStack Router's overly generic defaultNotFoundComponent (
Not Found
)`,
+ )
+ }
+
+ return
+ }
+
+ return
+}
+
export interface MatchRouteOptions {
pending?: boolean
caseSensitive?: boolean
@@ -419,8 +492,8 @@ export function useMatch<
const matchSelection = useRouterState({
select: (state) => {
- const match = getRenderedMatches(state).find(
- (d) => d.id === nearestMatchId,
+ const match = getRenderedMatches(state).find((d) =>
+ opts?.from ? opts?.from === d.routeId : d.id === nearestMatchId,
)
invariant(
diff --git a/packages/react-router/src/RouterProvider.tsx b/packages/react-router/src/RouterProvider.tsx
index c8d107ca2cf..7ea3c4f017e 100644
--- a/packages/react-router/src/RouterProvider.tsx
+++ b/packages/react-router/src/RouterProvider.tsx
@@ -137,22 +137,11 @@ function Transitioner() {
useLayoutEffect(() => {
const unsub = router.history.subscribe(() => {
router.latestLocation = router.parseLocation(router.latestLocation)
- if (routerState.location !== router.latestLocation) {
+ if (router.state.location !== router.latestLocation) {
tryLoad()
}
})
- const nextLocation = router.buildLocation({
- search: true,
- params: true,
- hash: true,
- state: true,
- })
-
- if (routerState.location.href !== nextLocation.href) {
- router.commitLocation({ ...nextLocation, replace: true })
- }
-
return () => {
unsub()
}
diff --git a/packages/react-router/src/awaited.tsx b/packages/react-router/src/awaited.tsx
index 6b969f68ab0..f15e7e52558 100644
--- a/packages/react-router/src/awaited.tsx
+++ b/packages/react-router/src/awaited.tsx
@@ -1,12 +1,10 @@
import * as React from 'react'
import { useRouter } from './useRouter'
+import { defaultSerializeError } from './router'
import { DeferredPromise, isDehydratedDeferred } from './defer'
+import { defaultDeserializeError, isServerSideError } from './Matches'
+
import warning from 'tiny-warning'
-import {
- isServerSideError,
- defaultDeserializeError,
- defaultSerializeError,
-} from '.'
export type AwaitOptions = {
promise: DeferredPromise
diff --git a/packages/react-router/src/fileRoute.ts b/packages/react-router/src/fileRoute.ts
index 3d56158f203..f0e43a9724d 100644
--- a/packages/react-router/src/fileRoute.ts
+++ b/packages/react-router/src/fileRoute.ts
@@ -16,7 +16,6 @@ import {
RouteConstraints,
ResolveFullSearchSchemaInput,
SearchSchemaInput,
- LoaderFnContext,
RouteLoaderFn,
AnyPathParams,
AnySearchSchema,
@@ -26,7 +25,8 @@ import { useMatch, useLoaderDeps, useLoaderData, RouteMatch } from './Matches'
import { useSearch } from './useSearch'
import { useParams } from './useParams'
import warning from 'tiny-warning'
-import { RegisteredRouter, RouteById, RouteIds } from '.'
+import { RegisteredRouter } from './router'
+import { RouteById, RouteIds } from './routeInfo'
export interface FileRoutesByPath {
// '/': {
@@ -176,7 +176,10 @@ export class FileRoute<
>,
TRouterContext extends RouteConstraints['TRouterContext'] = AnyContext,
TLoaderDeps extends Record = {},
- TLoaderData extends any = unknown,
+ TLoaderDataReturn extends any = unknown,
+ TLoaderData extends any = [TLoaderDataReturn] extends [never]
+ ? undefined
+ : TLoaderDataReturn,
TChildren extends RouteConstraints['TChildren'] = unknown,
TRouteTree extends RouteConstraints['TRouteTree'] = AnyRoute,
>(
@@ -197,6 +200,7 @@ export class FileRoute<
TRouterContext,
TAllContext,
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData
>,
'getParentRoute' | 'path' | 'id'
@@ -220,6 +224,7 @@ export class FileRoute<
TAllContext,
TRouterContext,
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData,
TChildren,
TRouteTree
diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx
index 1c0d83e2ee4..861547e72df 100644
--- a/packages/react-router/src/link.tsx
+++ b/packages/react-router/src/link.tsx
@@ -179,9 +179,7 @@ export type ToSubOptions<
TTo extends string = '',
> = {
to?: ToPathOption
- // The new has string or a function to update it
hash?: true | Updater
- // State to pass to the history stack
state?: true | NonNullableUpdater
// The source route path. This is automatically set when using route-level APIs, but for type-safe relative routing on the router itself, this is required
from?: RoutePathsAutoComplete
diff --git a/packages/react-router/src/not-found.tsx b/packages/react-router/src/not-found.tsx
index 7cc5020bd93..2b4d73b2961 100644
--- a/packages/react-router/src/not-found.tsx
+++ b/packages/react-router/src/not-found.tsx
@@ -1,10 +1,20 @@
import * as React from 'react'
import { CatchBoundary } from './CatchBoundary'
import { useRouterState } from './useRouterState'
-import { RegisteredRouter, RouteIds } from '.'
+import { RegisteredRouter } from './router'
+import { RouteIds } from './routeInfo'
export type NotFoundError = {
+ /**
+ @deprecated
+ Use `routeId: rootRouteId` instead
+ */
global?: boolean
+ /**
+ @private
+ Do not use this. It's used internally to indicate a path matching error
+ */
+ _global?: boolean
data?: any
throw?: boolean
routeId?: RouteIds
diff --git a/packages/react-router/src/redirects.ts b/packages/react-router/src/redirects.ts
index e070fcf5516..54eaa14119b 100644
--- a/packages/react-router/src/redirects.ts
+++ b/packages/react-router/src/redirects.ts
@@ -29,9 +29,10 @@ export function redirect<
opts: Redirect,
): Redirect {
;(opts as any).isRedirect = true
- if (opts.throw) {
+ if (opts.throw ?? true) {
throw opts
}
+
return opts
}
diff --git a/packages/react-router/src/route.ts b/packages/react-router/src/route.ts
index 5e96425b006..9bba8614032 100644
--- a/packages/react-router/src/route.ts
+++ b/packages/react-router/src/route.ts
@@ -18,9 +18,8 @@ import {
UnionToIntersection,
} from './utils'
import { BuildLocationFn, NavigateFn } from './RouterProvider'
-import { LazyRoute } from '.'
-import warning from 'tiny-warning'
-import { NotFoundError, notFound } from '.'
+import { NotFoundError, notFound } from './not-found'
+import { LazyRoute } from './fileRoute'
export const rootRouteId = '__root__' as const
export type RootRouteId = typeof rootRouteId
@@ -67,7 +66,10 @@ export type RouteOptions<
TRouterContext extends RouteConstraints['TRouterContext'] = AnyContext,
TAllContext extends Record = AnyContext,
TLoaderDeps extends Record = {},
- TLoaderData extends any = unknown,
+ TLoaderDataReturn extends any = unknown,
+ TLoaderData extends any = [TLoaderDataReturn] extends [never]
+ ? undefined
+ : TLoaderDataReturn,
> = BaseRouteOptions<
TParentRoute,
TCustomId,
@@ -84,6 +86,7 @@ export type RouteOptions<
TRouterContext,
TAllContext,
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData
> &
UpdatableRouteOptions<
@@ -113,7 +116,10 @@ export type BaseRouteOptions<
TRouterContext extends RouteConstraints['TRouterContext'] = AnyContext,
TAllContext extends Record = AnyContext,
TLoaderDeps extends Record = {},
- TLoaderData extends any = unknown,
+ TLoaderDataReturn extends any = unknown,
+ TLoaderData extends any = [TLoaderDataReturn] extends [never]
+ ? undefined
+ : TLoaderDataReturn,
> = RoutePathOptions & {
getParentRoute: () => TParentRoute
validateSearch?: SearchSchemaValidator
@@ -146,7 +152,7 @@ export type BaseRouteOptions<
NoInfer,
NoInfer,
NoInfer,
- TLoaderData
+ TLoaderDataReturn
>
} & (
| {
@@ -295,7 +301,7 @@ export type RouteLoaderFn<
TLoaderData extends any = unknown,
> = (
match: LoaderFnContext,
-) => Promise | TLoaderData
+) => Promise | TLoaderData | void
export interface LoaderFnContext<
TAllParams = {},
@@ -312,6 +318,7 @@ export interface LoaderFnContext<
navigate: (opts: NavigateOptions) => Promise
parentMatchPromise?: Promise
cause: 'preload' | 'enter' | 'stay'
+ route: Route
}
export type SearchFilter = (prev: T) => U
@@ -376,6 +383,7 @@ export interface AnyRoute
any,
any,
any,
+ any,
any
> {}
@@ -409,104 +417,6 @@ export type RouteConstraints = {
TRouteTree: AnyRoute
}
-// TODO: This is part of a future APi to move away from classes and
-// towards a more functional API. It's not ready yet.
-
-// type RouteApiInstance<
-// TId extends RouteIds,
-// TRoute extends AnyRoute = RouteById,
-// TFullSearchSchema extends Record<
-// string,
-// any
-// > = TRoute['types']['fullSearchSchema'],
-// TAllParams extends AnyPathParams = TRoute['types']['allParams'],
-// TAllContext extends Record = TRoute['types']['allContext'],
-// TLoaderDeps extends Record = TRoute['types']['loaderDeps'],
-// TLoaderData extends any = TRoute['types']['loaderData'],
-// > = {
-// id: TId
-// useMatch: (opts?: {
-// select?: (s: TAllContext) => TSelected
-// }) => TSelected
-
-// useRouteContext: (opts?: {
-// select?: (s: TAllContext) => TSelected
-// }) => TSelected
-
-// useSearch: (opts?: {
-// select?: (s: TFullSearchSchema) => TSelected
-// }) => TSelected
-
-// useParams: (opts?: {
-// select?: (s: TAllParams) => TSelected
-// }) => TSelected
-
-// useLoaderDeps: (opts?: {
-// select?: (s: TLoaderDeps) => TSelected
-// }) => TSelected
-
-// useLoaderData: (opts?: {
-// select?: (s: TLoaderData) => TSelected
-// }) => TSelected
-// }
-
-// export function RouteApi_v2<
-// TId extends RouteIds,
-// TRoute extends AnyRoute = RouteById,
-// TFullSearchSchema extends Record<
-// string,
-// any
-// > = TRoute['types']['fullSearchSchema'],
-// TAllParams extends AnyPathParams = TRoute['types']['allParams'],
-// TAllContext extends Record = TRoute['types']['allContext'],
-// TLoaderDeps extends Record = TRoute['types']['loaderDeps'],
-// TLoaderData extends any = TRoute['types']['loaderData'],
-// >({
-// id,
-// }: {
-// id: TId
-// }): RouteApiInstance<
-// TId,
-// TRoute,
-// TFullSearchSchema,
-// TAllParams,
-// TAllContext,
-// TLoaderDeps,
-// TLoaderData
-// > {
-// return {
-// id,
-
-// useMatch: (opts) => {
-// return useMatch({ ...opts, from: id })
-// },
-
-// useRouteContext: (opts) => {
-// return useMatch({
-// ...opts,
-// from: id,
-// select: (d: any) => (opts?.select ? opts.select(d.context) : d.context),
-// } as any)
-// },
-
-// useSearch: (opts) => {
-// return useSearch({ ...opts, from: id } as any)
-// },
-
-// useParams: (opts) => {
-// return useParams({ ...opts, from: id } as any)
-// },
-
-// useLoaderDeps: (opts) => {
-// return useLoaderDeps({ ...opts, from: id } as any) as any
-// },
-
-// useLoaderData: (opts) => {
-// return useLoaderData({ ...opts, from: id } as any) as any
-// },
-// }
-// }
-
export function getRouteApi<
TId extends RouteIds,
TRoute extends AnyRoute = RouteById,
@@ -599,9 +509,6 @@ export class RouteApi<
}
}
-/**
- * @deprecated Use the `createRoute` function instead.
- */
export class Route<
TParentRoute extends RouteConstraints['TParentRoute'] = AnyRoute,
in out TPath extends RouteConstraints['TPath'] = '/',
@@ -652,7 +559,10 @@ export class Route<
>,
TRouterContext extends RouteConstraints['TRouterContext'] = AnyContext,
TLoaderDeps extends Record = {},
- TLoaderData extends any = unknown,
+ TLoaderDataReturn extends any = unknown,
+ TLoaderData extends any = [TLoaderDataReturn] extends [never]
+ ? undefined
+ : TLoaderDataReturn,
TChildren extends RouteConstraints['TChildren'] = unknown,
TRouteTree extends RouteConstraints['TRouteTree'] = AnyRoute,
> {
@@ -673,6 +583,7 @@ export class Route<
TRouterContext,
TAllContext,
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData
>
@@ -691,6 +602,9 @@ export class Route<
rank!: number
lazyFn?: () => Promise>
+ /**
+ * @deprecated Use the `createRoute` function instead.
+ */
constructor(
options: RouteOptions<
TParentRoute,
@@ -708,6 +622,7 @@ export class Route<
TRouterContext,
TAllContext,
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData
>,
) {
@@ -762,6 +677,7 @@ export class Route<
TRouterContext,
TAllContext,
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData
> &
RoutePathOptionsIntersection
@@ -836,6 +752,7 @@ export class Route<
TAllContext,
TRouterContext,
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData,
TNewChildren,
TRouteTree
@@ -985,7 +902,10 @@ export function createRoute<
>,
TRouterContext extends RouteConstraints['TRouterContext'] = AnyContext,
TLoaderDeps extends Record = {},
- TLoaderData extends any = unknown,
+ TLoaderDataReturn extends any = unknown,
+ TLoaderData extends any = [TLoaderDataReturn] extends [never]
+ ? undefined
+ : TLoaderDataReturn,
TChildren extends RouteConstraints['TChildren'] = unknown,
TRouteTree extends RouteConstraints['TRouteTree'] = AnyRoute,
>(
@@ -1005,6 +925,7 @@ export function createRoute<
TRouterContext,
TAllContext,
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData
>,
) {
@@ -1026,6 +947,7 @@ export function createRoute<
TAllContext,
TRouterContext,
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData,
TChildren,
TRouteTree
@@ -1044,7 +966,10 @@ export function createRootRouteWithContext() {
? RouteContext
: TRouteContextReturn,
TLoaderDeps extends Record = {},
- TLoaderData extends any = unknown,
+ TLoaderDataReturn extends any = unknown,
+ TLoaderData extends any = [TLoaderDataReturn] extends [never]
+ ? undefined
+ : TLoaderDataReturn,
>(
options?: Omit<
RouteOptions<
@@ -1063,6 +988,7 @@ export function createRootRouteWithContext() {
TRouterContext,
Assign, // TAllContext
TLoaderDeps,
+ TLoaderDataReturn, // TLoaderDataReturn,
TLoaderData // TLoaderData,
>,
| 'path'
@@ -1105,7 +1031,10 @@ export class RootRoute<
: TRouteContextReturn,
TRouterContext extends {} = {},
TLoaderDeps extends Record = {},
- TLoaderData extends any = unknown,
+ TLoaderDataReturn extends any = unknown,
+ TLoaderData extends any = [TLoaderDataReturn] extends [never]
+ ? undefined
+ : TLoaderDataReturn,
> extends Route<
any, // TParentRoute
'/', // TPath
@@ -1124,6 +1053,7 @@ export class RootRoute<
Expand>, // TAllContext
TRouterContext, // TRouterContext
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData,
any, // TChildren
any // TRouteTree
@@ -1149,6 +1079,7 @@ export class RootRoute<
TRouterContext,
Assign, // TAllContext
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData
>,
| 'path'
@@ -1173,7 +1104,10 @@ export function createRootRoute<
: TRouteContextReturn,
TRouterContext extends {} = {},
TLoaderDeps extends Record = {},
- TLoaderData extends any = unknown,
+ TLoaderDataReturn extends any = unknown,
+ TLoaderData extends any = [TLoaderDataReturn] extends [never]
+ ? undefined
+ : TLoaderDataReturn,
>(
options?: Omit<
RouteOptions<
@@ -1192,6 +1126,7 @@ export function createRootRoute<
TRouterContext,
Assign, // TAllContext
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData
>,
| 'path'
@@ -1210,6 +1145,7 @@ export function createRootRoute<
TRouteContext,
TRouterContext,
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData
>(options)
}
@@ -1333,7 +1269,10 @@ export class NotFoundRoute<
>,
TRouterContext extends RouteConstraints['TRouterContext'] = AnyContext,
TLoaderDeps extends Record = {},
- TLoaderData extends any = unknown,
+ TLoaderDataReturn extends any = unknown,
+ TLoaderData extends any = [TLoaderDataReturn] extends [never]
+ ? undefined
+ : TLoaderDataReturn,
TChildren extends RouteConstraints['TChildren'] = unknown,
TRouteTree extends RouteConstraints['TRouteTree'] = AnyRoute,
> extends Route<
@@ -1354,6 +1293,7 @@ export class NotFoundRoute<
TAllContext,
TRouterContext,
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData,
TChildren,
TRouteTree
@@ -1376,6 +1316,7 @@ export class NotFoundRoute<
TRouterContext,
TAllContext,
TLoaderDeps,
+ TLoaderDataReturn,
TLoaderData
>,
'caseSensitive' | 'parseParams' | 'stringifyParams' | 'path' | 'id'
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 51120292461..a549bc3277f 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -14,11 +14,11 @@ import {
AnySearchSchema,
AnyRoute,
AnyContext,
- AnyPathParams,
RouteMask,
Route,
LoaderFnContext,
rootRouteId,
+ NotFoundRouteComponent,
} from './route'
import {
FullSearchSchema,
@@ -67,10 +67,10 @@ import {
import invariant from 'tiny-invariant'
import { AnyRedirect, isRedirect } from './redirects'
import { NotFoundError, isNotFound } from './not-found'
-import { ResolveRelativePath, ToOptions } from './link'
+import { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
import { NoInfer } from '@tanstack/react-store'
import warning from 'tiny-warning'
-import { DeferredPromiseState } from '.'
+import { DeferredPromiseState } from './defer'
//
@@ -85,7 +85,7 @@ export interface Register {
// router: Router
}
-export type AnyRouter = Router
+export type AnyRouter = Router
export type RegisteredRouter = Register extends {
router: infer TRouter extends AnyRouter
@@ -125,6 +125,7 @@ export interface RouterOptions<
defaultStaleTime?: number
defaultPreloadStaleTime?: number
defaultPreloadGcTime?: number
+ notFoundMode?: 'root' | 'fuzzy'
defaultGcTime?: number
caseSensitive?: boolean
routeTree?: TRouteTree
@@ -142,9 +143,9 @@ export interface RouterOptions<
* See https://tanstack.com/router/v1/docs/guide/not-found-errors#migrating-from-notfoundroute for more info.
*/
notFoundRoute?: AnyRoute
+ defaultNotFoundComponent?: NotFoundRouteComponent
transformer?: RouterTransformer
errorSerializer?: RouterErrorSerializer
- globalNotFound?: RouteComponent
}
export interface RouterTransformer {
@@ -166,6 +167,7 @@ export interface RouterState {
location: ParsedLocation>
resolvedLocation: ParsedLocation>
lastUpdated: number
+ statusCode: number
}
export type ListenerFn = (event: TEvent) => void
@@ -193,7 +195,7 @@ export interface DehydratedRouterState {
export type DehydratedRouteMatch = Pick<
RouteMatch,
- 'id' | 'status' | 'updatedAt' | 'notFoundError' | 'loaderData'
+ 'id' | 'status' | 'updatedAt' | 'loaderData'
>
export interface DehydratedRouter {
@@ -380,6 +382,9 @@ export class Router<
this.state.isTransitioning || this.state.isLoading
? 'pending'
: 'idle',
+ cachedMatches: this.state.cachedMatches.filter(
+ (d) => !['redirected'].includes(d.status),
+ ),
}
},
})
@@ -586,7 +591,7 @@ export class Router<
matchRoutes = (
pathname: string,
locationSearch: AnySearchSchema,
- opts?: { throwOnError?: boolean; debug?: boolean },
+ opts?: { preload?: boolean; throwOnError?: boolean; debug?: boolean },
): RouteMatch[] => {
let routeParams: Record = {}
@@ -611,7 +616,7 @@ export class Router<
})
let routeCursor: AnyRoute =
- foundRoute || (this.routesById as any)['__root__']
+ foundRoute || (this.routesById as any)[rootRouteId]
let matchedRoutes: AnyRoute[] = [routeCursor]
@@ -639,6 +644,23 @@ export class Router<
if (routeCursor) matchedRoutes.unshift(routeCursor)
}
+ const globalNotFoundRouteId = (() => {
+ if (!isGlobalNotFound) {
+ return undefined
+ }
+
+ if (this.options.notFoundMode !== 'root') {
+ for (let i = matchedRoutes.length - 1; i >= 0; i--) {
+ const route = matchedRoutes[i]!
+ if (route.children) {
+ return route.id
+ }
+ }
+ }
+
+ return rootRouteId
+ })()
+
// Existing matches are matches that are already loaded along with
// pending matches that are still loading
@@ -677,6 +699,7 @@ export class Router<
// which is used to uniquely identify the route match in state
const parentMatch = matches[index - 1]
+ const isLast = index === matchedRoutes.length - 1
const [preMatchSearch, searchError]: [Record, any] = (() => {
// Validate the search params and stabilize them
@@ -737,7 +760,7 @@ export class Router<
// Waste not, want not. If we already have a match for this route,
// reuse it. This is important for layout routes, which might stick
// around between navigation actions that only change leaf routes.
- const existingMatch = getRouteMatch(this.state, matchId)
+ let existingMatch = getRouteMatch(this.state, matchId)
const cause = this.state.matches.find((d) => d.id === matchId)
? 'stay'
@@ -747,10 +770,6 @@ export class Router<
? {
...existingMatch,
cause,
- notFoundError:
- isGlobalNotFound && route.id === rootRouteId
- ? { global: true }
- : undefined,
params: routeParams,
}
: {
@@ -775,15 +794,16 @@ export class Router<
loaderDeps,
invalid: false,
preload: false,
- notFoundError:
- isGlobalNotFound && route.id === rootRouteId
- ? { global: true }
- : undefined,
links: route.options.links?.(),
scripts: route.options.scripts?.(),
staticData: route.options.staticData || {},
}
+ if (!opts?.preload) {
+ // If we have a global not found, mark the right match as global not found
+ match.globalNotFound = globalNotFoundRouteId === route.id
+ }
+
// Regardless of whether we're reusing an existing match or creating
// a new one, we need to update the match's search params
match.search = replaceEqualDeep(match.search, preMatchSearch)
@@ -796,9 +816,7 @@ export class Router<
return matches as any
}
- cancelMatch = (id: string) => {
- getRouteMatch(this.state, id)?.abortController?.abort()
- }
+ cancelMatch = (id: string) => {}
cancelMatches = () => {
this.state.pendingMatches?.forEach((match) => {
@@ -813,16 +831,23 @@ export class Router<
} = {},
matches?: AnyRouteMatch[],
): ParsedLocation => {
+ // if (dest.href) {
+ // return {
+ // pathname: dest.href,
+ // search: {},
+ // searchStr: '',
+ // state: {},
+ // hash: '',
+ // href: dest.href,
+ // unmaskOnReload: dest.unmaskOnReload,
+ // }
+ // }
+
const relevantMatches = this.state.pendingMatches || this.state.matches
const fromSearch =
relevantMatches[relevantMatches.length - 1]?.search ||
this.latestLocation.search
- let pathname = this.resolvePathWithBase(
- dest.from ?? this.latestLocation.pathname,
- `${dest.to ?? ''}`,
- )
-
const fromMatches = this.matchRoutes(
this.latestLocation.pathname,
fromSearch,
@@ -831,6 +856,15 @@ export class Router<
fromMatches?.find((e) => e.routeId === d.routeId),
)
+ const fromRoute = this.looseRoutesById[last(fromMatches)?.routeId]
+
+ let pathname = dest.to
+ ? this.resolvePathWithBase(
+ dest.from ?? this.latestLocation.pathname,
+ `${dest.to}`,
+ )
+ : fromRoute?.fullPath
+
const prevParams = { ...last(fromMatches)?.params }
let nextParams =
@@ -1002,7 +1036,7 @@ export class Router<
// If the next urls are the same and we're not replacing,
// do nothing
- if (!isSameUrl || !next.replace) {
+ if (!isSameUrl) {
let { maskedLocation, ...nextHistory } = next
if (maskedLocation) {
@@ -1097,18 +1131,19 @@ export class Router<
loadMatches = async ({
checkLatest,
+ location,
matches,
preload,
}: {
checkLatest: () => Promise | undefined
+ location: ParsedLocation
matches: AnyRouteMatch[]
preload?: boolean
}): Promise => {
let latestPromise
let firstBadMatchIndex: number | undefined
- const updateMatch = (match: AnyRouteMatch) => {
- // const isPreload = this.state.cachedMatches.find((d) => d.id === match.id)
+ const updateMatch = (match: AnyRouteMatch, opts?: { remove?: boolean }) => {
const isPending = this.state.pendingMatches?.find(
(d) => d.id === match.id,
)
@@ -1123,29 +1158,45 @@ export class Router<
this.__store.setState((s) => ({
...s,
- [matchesKey]: s[matchesKey]?.map((d) =>
- d.id === match.id ? match : d,
- ),
+ [matchesKey]: opts?.remove
+ ? s[matchesKey]?.filter((d) => d.id !== match.id)
+ : s[matchesKey]?.map((d) => (d.id === match.id ? match : d)),
}))
}
+ const handleMatchSpecialError = (match: AnyRouteMatch, err: any) => {
+ match = {
+ ...match,
+ status: isRedirect(err)
+ ? 'redirected'
+ : isNotFound(err)
+ ? 'notFound'
+ : 'error',
+ isFetching: false,
+ error: err,
+ }
+
+ updateMatch(match)
+
+ if (!err.routeId) {
+ err.routeId = match.routeId
+ }
+
+ throw err
+ }
+
// Check each match middleware to see if the route can be accessed
for (let [index, match] of matches.entries()) {
const parentMatch = matches[index - 1]
const route = this.looseRoutesById[match.routeId]!
const abortController = new AbortController()
- const handleError = (err: any, code: string) => {
+ const handleSerialError = (err: any, code: string) => {
err.routerCode = code
firstBadMatchIndex = firstBadMatchIndex ?? index
- if (isRedirect(err)) {
- throw err
- }
-
- if (isNotFound(err)) {
- err.routeId = match.routeId
- throw err
+ if (isRedirect(err) || isNotFound(err)) {
+ handleMatchSpecialError(match, err)
}
try {
@@ -1153,8 +1204,8 @@ export class Router<
} catch (errorHandlerErr) {
err = errorHandlerErr
- if (isRedirect(errorHandlerErr)) {
- throw errorHandlerErr
+ if (isRedirect(err) || isNotFound(err)) {
+ handleMatchSpecialError(match, errorHandlerErr)
}
}
@@ -1167,15 +1218,19 @@ export class Router<
}
}
- try {
- if (match.paramsError) {
- handleError(match.paramsError, 'PARSE_PARAMS')
- }
+ if (match.paramsError) {
+ handleSerialError(match.paramsError, 'PARSE_PARAMS')
+ }
- if (match.searchError) {
- handleError(match.searchError, 'VALIDATE_SEARCH')
- }
+ if (match.searchError) {
+ handleSerialError(match.searchError, 'VALIDATE_SEARCH')
+ }
+ // if (match.globalNotFound && !preload) {
+ // handleSerialError(notFound({ _global: true }), 'NOT_FOUND')
+ // }
+
+ try {
const parentContext = parentMatch?.context ?? this.options.context ?? {}
const pendingMs =
@@ -1192,16 +1247,15 @@ export class Router<
params: match.params,
preload: !!preload,
context: parentContext,
- location: this.state.location,
- // TOOD: just expose state and router, etc
+ location,
navigate: (opts) =>
this.navigate({ ...opts, from: match.pathname } as any),
buildLocation: this.buildLocation,
cause: preload ? 'preload' : match.cause,
})) ?? ({} as any)
- if (isRedirect(beforeLoadContext)) {
- throw beforeLoadContext
+ if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
+ handleSerialError(beforeLoadContext, 'BEFORE_LOAD')
}
const context = {
@@ -1217,7 +1271,7 @@ export class Router<
pendingPromise,
}
} catch (err) {
- handleError(err, 'BEFORE_LOAD')
+ handleSerialError(err, 'BEFORE_LOAD')
break
}
}
@@ -1232,13 +1286,8 @@ export class Router<
const route = this.looseRoutesById[match.routeId]!
const handleError = (err: any) => {
- if (isRedirect(err)) {
- throw err
- }
-
- if (isNotFound(err)) {
- err.routeId = match.routeId
- throw err
+ if (isRedirect(err) || isNotFound(err)) {
+ handleMatchSpecialError(match, err)
}
}
@@ -1254,11 +1303,6 @@ export class Router<
route.options.pendingMs ?? this.options.defaultPendingMs
const pendingMinMs =
route.options.pendingMinMs ?? this.options.defaultPendingMinMs
- const shouldPending =
- !preload &&
- typeof pendingMs === 'number' &&
- (route.options.pendingComponent ??
- this.options.defaultPendingComponent)
const loaderContext: LoaderFnContext = {
params: match.params,
@@ -1267,72 +1311,69 @@ export class Router<
parentMatchPromise,
abortController: match.abortController,
context: match.context,
- location: this.state.location,
+ location,
navigate: (opts) =>
this.navigate({ ...opts, from: match.pathname } as any),
cause: preload ? 'preload' : match.cause,
+ route,
}
const fetch = async () => {
- if (match.isFetching) {
- loadPromise = getRouteMatch(this.state, match.id)?.loadPromise
- } else {
- // If the user doesn't want the route to reload, just
- // resolve with the existing loader data
-
- if (match.fetchCount && match.status === 'success') {
- resolve()
+ try {
+ if (match.isFetching) {
+ loadPromise = getRouteMatch(this.state, match.id)?.loadPromise
+ } else {
+ // If the user doesn't want the route to reload, just
+ // resolve with the existing loader data
+
+ // if (match.fetchCount && match.status === 'success') {
+ // resolve()
+ // }
+
+ // Otherwise, load the route
+ matches[index] = match = {
+ ...match,
+ isFetching: true,
+ fetchCount: match.fetchCount + 1,
+ }
+
+ const lazyPromise =
+ route.lazyFn?.().then((lazyRoute) => {
+ Object.assign(route.options, lazyRoute.options)
+ }) || Promise.resolve()
+
+ // If for some reason lazy resolves more lazy components...
+ // We'll wait for that before pre attempt to preload any
+ // components themselves.
+ const componentsPromise = lazyPromise.then(() =>
+ Promise.all(
+ componentTypes.map(async (type) => {
+ const component = route.options[type]
+
+ if ((component as any)?.preload) {
+ await (component as any).preload()
+ }
+ }),
+ ),
+ )
+
+ // Kick off the loader!
+ const loaderPromise = route.options.loader?.(loaderContext)
+
+ loadPromise = Promise.all([
+ componentsPromise,
+ loaderPromise,
+ lazyPromise,
+ ]).then((d) => d[1])
}
- // Otherwise, load the route
matches[index] = match = {
...match,
- isFetching: true,
- fetchCount: match.fetchCount + 1,
- }
-
- const lazyPromise =
- route.lazyFn?.().then((lazyRoute) => {
- Object.assign(route.options, lazyRoute.options)
- }) || Promise.resolve()
-
- // If for some reason lazy resolves more lazy components...
- // We'll wait for that before pre attempt to preload any
- // components themselves.
- const componentsPromise = lazyPromise.then(() =>
- Promise.all(
- componentTypes.map(async (type) => {
- const component = route.options[type]
-
- if ((component as any)?.preload) {
- await (component as any).preload()
- }
- }),
- ),
- )
-
- // wrap loader into an async function to be able to catch synchronous exceptions
- async function loader() {
- return await route.options.loader?.(loaderContext)
+ loadPromise,
}
- // Kick off the loader!
- const loaderPromise = loader()
-
- loadPromise = Promise.all([
- componentsPromise,
- loaderPromise,
- lazyPromise,
- ]).then((d) => d[1])
- }
-
- matches[index] = match = {
- ...match,
- loadPromise,
- }
- updateMatch(match)
+ updateMatch(match)
- try {
const loaderData = await loadPromise
if ((latestPromise = checkLatest())) return await latestPromise
@@ -1367,6 +1408,7 @@ export class Router<
}
} catch (error) {
if ((latestPromise = checkLatest())) return await latestPromise
+
handleError(error)
try {
@@ -1414,10 +1456,44 @@ export class Router<
!!preload && !this.state.matches.find((d) => d.id === match.id),
}
- try {
- if (match.status !== 'success') {
- // If we need to potentially show the pending component,
- // start a timer to show it after the pendingMs
+ // If the route is successful and still fresh, just resolve
+ if (
+ match.status === 'success' &&
+ (match.invalid || (shouldReload ?? age > staleAge))
+ ) {
+ ;(async () => {
+ try {
+ await fetch()
+ } catch (err) {
+ console.info('Background Fetching Error', err)
+
+ if (isRedirect(err)) {
+ const isActive = (
+ this.state.pendingMatches || this.state.matches
+ ).find((d) => d.id === match.id)
+
+ // Redirects should not be persisted
+ handleError(err)
+
+ // If the route is still active, redirect
+ if (isActive) {
+ this.handleRedirect(err)
+ }
+ }
+ }
+ })()
+
+ return resolve()
+ }
+
+ const shouldPending =
+ !preload &&
+ typeof pendingMs === 'number' &&
+ (route.options.pendingComponent ??
+ this.options.defaultPendingComponent)
+
+ if (match.status !== 'success') {
+ try {
if (shouldPending) {
match.pendingPromise?.then(async () => {
if ((latestPromise = checkLatest())) return latestPromise
@@ -1433,14 +1509,10 @@ export class Router<
})
}
- // Critical Fetching, we need to await
await fetch()
- } else if (match.invalid || (shouldReload ?? age > staleAge)) {
- // Background Fetching, no need to wait
- fetch()
+ } catch (err) {
+ reject(err)
}
- } catch (err) {
- reject(err)
}
resolve()
@@ -1511,18 +1583,28 @@ export class Router<
})
try {
+ let redirected: AnyRedirect
+ let notFound: NotFoundError
+
try {
// Load the matches
await this.loadMatches({
matches: pendingMatches,
+ location: next,
checkLatest: () => this.checkLatest(promise),
})
} catch (err) {
if (isRedirect(err)) {
+ redirected = err
this.handleRedirect(err)
} else if (isNotFound(err)) {
+ notFound = err
this.handleNotFound(pendingMatches, err)
}
+
+ // Swallow all other errors that happen inside
+ // of loadMatches. These errors will be handled
+ // as state on each match.
}
// Only apply the latest transition
@@ -1552,6 +1634,12 @@ export class Router<
...s.cachedMatches,
...exitingMatches.filter((d) => d.status !== 'error'),
],
+ statusCode:
+ redirected?.code || notFound
+ ? 404
+ : s.matches.some((d) => d.status === 'error')
+ ? 500
+ : 200,
}))
this.cleanCache()
})
@@ -1583,6 +1671,8 @@ export class Router<
return latestPromise
}
+ console.log('Load Error', err)
+
reject(err)
}
})
@@ -1596,12 +1686,9 @@ export class Router<
if (!err.href) {
err.href = this.buildLocation(err as any).href
}
-
- if (isServer) {
- throw err
+ if (!isServer) {
+ this.navigate({ ...(err as any), replace: true })
}
-
- this.navigate(err as any)
}
cleanCache = () => {
@@ -1630,13 +1717,19 @@ export class Router<
})
}
- preloadRoute = async (
- navigateOpts: ToOptions = this.state.location as any,
- ) => {
- let next = this.buildLocation(navigateOpts as any)
+ preloadRoute = async <
+ TFrom extends RoutePaths | string = string,
+ TTo extends string = '',
+ TMaskFrom extends RoutePaths | string = TFrom,
+ TMaskTo extends string = '',
+ >(
+ opts: NavigateOptions,
+ ): Promise => {
+ let next = this.buildLocation(opts as any)
let matches = this.matchRoutes(next.pathname, next.search, {
throwOnError: true,
+ preload: true,
})
const loadedMatchIds = Object.fromEntries(
@@ -1661,17 +1754,18 @@ export class Router<
try {
matches = await this.loadMatches({
matches,
+ location: next,
preload: true,
checkLatest: () => undefined,
})
return matches
} catch (err) {
- // Preload errors are not fatal, but we should still log them
- if (!isRedirect(err) && !isNotFound(err)) {
- console.error(err)
+ if (isRedirect(err)) {
+ return await this.preloadRoute(err as any)
}
-
+ // Preload errors are not fatal, but we should still log them
+ console.error(err)
return undefined
}
}
@@ -1801,15 +1895,7 @@ export class Router<
return {
state: {
dehydratedMatches: this.state.matches.map((d) => ({
- ...pick(d, [
- 'id',
- 'status',
- 'updatedAt',
- 'loaderData',
- // Not-founds that occur during SSR don't require the client to load data before
- // triggering in order to prevent the flicker of the loading component
- 'notFoundError',
- ]),
+ ...pick(d, ['id', 'status', 'updatedAt', 'loaderData']),
// If an error occurs server-side during SSRing,
// send a small subset of the error to the client
error: d.error
@@ -1879,43 +1965,48 @@ export class Router<
})
}
- // Finds a match that has a notFoundComponent
handleNotFound = (matches: AnyRouteMatch[], err: NotFoundError) => {
const matchesByRouteId = Object.fromEntries(
matches.map((match) => [match.routeId, match]),
) as Record
- if (!err.global && err.routeId) {
- // If the err contains a routeId, start searching up from that route
- let currentRoute = this.looseRoutesById[err.routeId]
-
- if (currentRoute) {
- // Go up the tree until we find a route with a notFoundComponent
- while (!currentRoute.options.notFoundComponent) {
- currentRoute = currentRoute?.parentRoute
-
- invariant(
- currentRoute,
- 'Found invalid route tree while trying to find not-found handler.',
- )
+ // Start at the route that errored or default to the root route
+ let routeCursor =
+ (err.global
+ ? this.looseRoutesById[rootRouteId]
+ : this.looseRoutesById[err.routeId]) ||
+ this.looseRoutesById[rootRouteId]!
+
+ // Go up the tree until we find a route with a notFoundComponent or we hit the root
+ while (
+ !routeCursor.options.notFoundComponent &&
+ !this.options.defaultNotFoundComponent &&
+ routeCursor.id !== rootRouteId
+ ) {
+ routeCursor = routeCursor?.parentRoute
- if (currentRoute.id === rootRouteId) break
- }
+ invariant(
+ routeCursor,
+ 'Found invalid route tree while trying to find not-found handler.',
+ )
+ }
- const match = matchesByRouteId[currentRoute.id]
- invariant(match, 'Could not find match for route: ' + currentRoute.id)
- match.notFoundError = err
+ let match = matchesByRouteId[routeCursor.id]
- return
- }
- }
+ invariant(match, 'Could not find match for route: ' + routeCursor.id)
- // Otherwise, just set the notFoundError on the root route
- matchesByRouteId[rootRouteId]!.notFoundError = err
+ // Assign the error to the match
+ Object.assign(match, {
+ status: 'notFound',
+ error: err,
+ isFetching: false,
+ } as AnyRouteMatch)
}
hasNotFoundMatch = () => {
- return this.__store.state.matches.some((d) => d.notFoundError)
+ return this.__store.state.matches.some(
+ (d) => d.status === 'notFound' || d.globalNotFound,
+ )
}
// resolveMatchPromise = (matchId: string, key: string, value: any) => {
@@ -1957,6 +2048,7 @@ export function getInitialRouterState(
pendingMatches: [],
cachedMatches: [],
lastUpdated: 0,
+ statusCode: 200,
}
}
diff --git a/packages/router-devtools/src/devtools.tsx b/packages/router-devtools/src/devtools.tsx
index 29bb72a8817..e5820b2eb5a 100644
--- a/packages/router-devtools/src/devtools.tsx
+++ b/packages/router-devtools/src/devtools.tsx
@@ -9,6 +9,7 @@ import {
useRouter,
useRouterState,
AnyRouteMatch,
+ rootRouteId,
} from '@tanstack/react-router'
import useLocalStorage from './useLocalStorage'
@@ -298,7 +299,7 @@ export function TanStackRouterDevtools({