Skip to content

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.

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 Router Link
  • Prefer Link over 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.tsx

Child 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,

-- ł.