Appearance
Routing, linking, loading, and errors
File tree is the router. Special files are the UX chrome. Once you accept that, Next feels less magical and more… Nuxt-y.
Links
tsx
import Link from 'next/link'
export function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/blog/hello">Post</Link>
</nav>
)
}Comments for the code above:
Link≈NuxtLink/ React RouterLink- Prefer
Linkover raw<a>for internal routes (client navigation)
Programmatic navigation
tsx
'use client'
import { useRouter } from 'next/navigation'
export function GoHomeButton() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/')}>
Home
</button>
)
}Note the import: next/navigation (App Router), not next/router (Pages Router). Easy footgun when reading old blog posts.
loading.tsx
tsx
// app/blog/loading.tsx
export default function Loading() {
return <p>Loading posts…</p>
}Shown while the segment's Server Component resolves. ≈ Nuxt's loading indicators, but file-based per segment.
error.tsx
tsx
'use client' // error boundaries are client components
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div>
<p>Something broke: {error.message}</p>
<button type="button" onClick={reset}>Try again</button>
</div>
)
}As you can see - must be a Client Component. reset re-attempts the render.
not-found
tsx
// app/not-found.tsx
export default function NotFound() {
return <h1>404 - gone fishing</h1>
}Trigger with notFound() from next/navigation in a server page when a slug doesn't exist.
Nested layouts
bash
app
|-- layout.tsx # chrome for everything
|-- blog
| |-- layout.tsx # blog sidebar
| |-- page.tsx
| |-- [slug]
| |-- page.tsxChild layouts wrap only their segment. Same idea as nested Nuxt layouts / nested React Router outlets - except folders do the nesting.
Dynamic segments
tsx
// app/blog/[slug]/page.tsx
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <h1>{slug}</h1>
}Catch-alls: [...slug]. Optional catch-alls: [[...slug]]. Same naming spirit as Vue Router / Nuxt file routes.
Next: when I'd still pick a Vite SPA instead of Next.
Best,
-- ł.