Files
dotfiles/dot_agents/skills/tanstack-router-best-practices/rules/err-not-found.md
T
Prad Nukala 6406208b35 shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables:
  ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone
  ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj
  (ghg-clone not named ghg: unrelated cargo binary owns that name)
- cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho
  cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files
- fish keeps only shell-state functions; converted ones now hit PATH
- cp-appid ignore gating moved to .local/bin path
- dot_agents/skills: 82 skills materialized from dotstate storage so
  dot_claude skill symlinks resolve on fresh bootstrap; dotstate
  profile/symlinks.json released .agents/skills .bashrc .zshrc
2026-08-10 15:15:06 -04:00

195 lines
4.6 KiB
Markdown

# err-not-found: Handle Not-Found Routes Properly
## Priority: HIGH
## Explanation
Configure `notFoundComponent` to handle 404 errors gracefully. TanStack Router provides not-found handling at multiple levels: root, route-specific, and programmatic via `notFound()`. Proper configuration prevents blank screens and improves UX.
## Bad Example
```tsx
// No not-found handling - shows blank screen or error
const router = createRouter({
routeTree,
// Missing defaultNotFoundComponent
})
// Or throwing generic error
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) {
throw new Error('Not found') // Generic error, not proper 404
}
return post
},
})
```
## Good Example: Root-Level Not Found
```tsx
// routes/__root.tsx
export const Route = createRootRoute({
component: RootComponent,
notFoundComponent: GlobalNotFound,
})
function GlobalNotFound() {
return (
<div className="not-found">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<Link to="/">Go Home</Link>
</div>
)
}
// router.tsx - Can also set default
const router = createRouter({
routeTree,
defaultNotFoundComponent: () => (
<div>
<h1>404</h1>
<Link to="/">Return Home</Link>
</div>
),
})
```
## Good Example: Route-Specific Not Found
```tsx
// routes/posts/$postId.tsx
import { createFileRoute, notFound } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) {
throw notFound() // Proper 404 handling
}
return post
},
notFoundComponent: PostNotFound, // Custom 404 for this route
component: PostPage,
})
function PostNotFound() {
const { postId } = Route.useParams()
return (
<div>
<h1>Post Not Found</h1>
<p>No post exists with ID: {postId}</p>
<Link to="/posts">Browse all posts</Link>
</div>
)
}
```
## Good Example: Not Found with Data
```tsx
export const Route = createFileRoute('/users/$username')({
loader: async ({ params }) => {
const user = await fetchUser(params.username)
if (!user) {
throw notFound({
// Pass data to notFoundComponent
data: {
username: params.username,
suggestions: await fetchSimilarUsernames(params.username),
},
})
}
return user
},
notFoundComponent: UserNotFound,
})
function UserNotFound() {
const { data } = Route.useMatch()
return (
<div>
<h1>User @{data?.username} not found</h1>
{data?.suggestions?.length > 0 && (
<div>
<p>Did you mean:</p>
<ul>
{data.suggestions.map((username) => (
<li key={username}>
<Link to="/users/$username" params={{ username }}>
@{username}
</Link>
</li>
))}
</ul>
</div>
)}
</div>
)
}
```
## Good Example: Catch-All Route
```tsx
// routes/$.tsx - Catch-all splat route
export const Route = createFileRoute('/$')({
component: CatchAllNotFound,
})
function CatchAllNotFound() {
const { _splat } = Route.useParams()
return (
<div>
<h1>Page Not Found</h1>
<p>No page exists at: /{_splat}</p>
<Link to="/">Go to homepage</Link>
</div>
)
}
```
## Good Example: Nested Not Found Bubbling
```tsx
// Not found bubbles up through route tree
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
notFoundComponent: PostsNotFound, // Catches child 404s too
})
// routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) throw notFound()
return post
},
// No notFoundComponent - bubbles to parent
})
// routes/posts/$postId/comments.tsx
export const Route = createFileRoute('/posts/$postId/comments')({
loader: async ({ params }) => {
const comments = await fetchComments(params.postId)
if (!comments) throw notFound() // Bubbles to /posts notFoundComponent
return comments
},
})
```
## Context
- `notFound()` throws a special error caught by nearest `notFoundComponent`
- Not found bubbles up the route tree if not handled locally
- Use `defaultNotFoundComponent` on router for global fallback
- Pass data to `notFound({ data })` for contextual 404 pages
- Catch-all routes (`/$`) can handle truly unknown paths
- Different from error boundaries - specifically for 404 cases