Files
gh-human-frontier-labs-inc-…/references/nextjs-15-patterns.md
2025-11-29 18:47:35 +08:00

8.5 KiB

Next.js 15 Patterns - CareBridge

Critical Changes from Next.js 14

Next.js 15 introduces async Request APIs. Several patterns have changed.

Async Params (CRITICAL)

In App Router Pages

ALL dynamic route params must be awaited

// ❌ WRONG - Next.js 14 pattern (will error in 15)
export default function Page({ params }: { params: { id: string } }) {
  const { id } = params // ERROR!
}

// ✅ CORRECT - Next.js 15 pattern
type Props = {
  params: Promise<{ id: string }>
}

export default async function Page({ params }: Props) {
  const { id } = await params
  // Use id
}

In API Routes

ALL dynamic route params must be awaited

// ❌ WRONG
export async function GET(
  request: Request,
  { params }: { params: { userId: string } }
) {
  const { userId } = params // ERROR!
}

// ✅ CORRECT
export async function GET(
  request: Request,
  { params }: { params: Promise<{ userId: string }> }
) {
  const { userId } = await params
  // Use userId
}

Async SearchParams

SearchParams are also async in Next.js 15

// ❌ WRONG
export default function Page({
  searchParams,
}: {
  searchParams: { caseId?: string }
}) {
  const caseId = searchParams.caseId // ERROR!
}

// ✅ CORRECT
type SearchParams = Promise<{ caseId?: string }>

export default async function Page({
  searchParams,
}: {
  searchParams: SearchParams
}) {
  const params = await searchParams
  const caseId = getCaseIdFromParams(params)
}

Server Components vs Client Components

When to Use Server Components (Default)

Use server components (no 'use client') for:

  • Pages that fetch data
  • Pages that need SEO
  • Static content
  • Database queries
// Server Component (default)
import { createServerClient } from '@/lib/supabase/server'

export default async function DashboardPage() {
  const supabase = await createServerClient()
  const { data } = await supabase.from('cases').select('*')

  return <div>{/* Render data */}</div>
}

When to Use Client Components

Use 'use client' for:

  • Interactive UI (onClick, onChange, etc.)
  • React hooks (useState, useEffect, useContext)
  • Browser APIs (localStorage, window, etc.)
  • Third-party libraries that need client-side
'use client'

import { useState } from 'react'

export function InteractiveComponent() {
  const [count, setCount] = useState(0)

  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

Mixing Server and Client

Pattern: Server component fetches data, passes to client component

// app/page.tsx (Server Component)
import { ClientComponent } from './client-component'

export default async function Page() {
  const data = await fetchData()

  return <ClientComponent data={data} />
}

// client-component.tsx (Client Component)
'use client'

export function ClientComponent({ data }) {
  const [selected, setSelected] = useState(null)

  return (
    <div onClick={() => setSelected(data)}>
      {/* Interactive UI */}
    </div>
  )
}

Metadata API

Use the Metadata API for SEO:

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Dashboard | CareBridge',
  description: 'Manage your care cases',
}

export default function Page() {
  return <div>Dashboard</div>
}

Loading and Error States

Loading States

Create loading.tsx next to page.tsx:

// app/dashboard/loading.tsx
export default function Loading() {
  return <div>Loading dashboard...</div>
}

Error Boundaries

Create error.tsx next to page.tsx:

// app/dashboard/error.tsx
'use client' // Error boundaries must be client components

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  )
}

Data Fetching Patterns

Server-Side Fetching (Preferred)

// In Server Component
export default async function Page() {
  const supabase = await createServerClient()
  const { data } = await supabase.from('cases').select('*')

  return <CasesList cases={data} />
}

Client-Side Fetching (When Needed)

'use client'

import { useEffect, useState } from 'react'

export function ClientDataFetcher() {
  const [data, setData] = useState(null)

  useEffect(() => {
    fetch('/api/data')
      .then((res) => res.json())
      .then(setData)
  }, [])

  if (!data) return <div>Loading...</div>

  return <div>{/* Render data */}</div>
}

Form Actions

Use Server Actions for forms:

// app/actions.ts
'use server'

export async function createCase(formData: FormData) {
  const name = formData.get('name')

  const supabase = await createServerClient()
  await supabase.from('cases').insert({ name })

  revalidatePath('/dashboard')
}

// app/page.tsx
import { createCase } from './actions'

export default function Page() {
  return (
    <form action={createCase}>
      <input name="name" />
      <button type="submit">Create</button>
    </form>
  )
}

Common Mistakes

Mistake #1: Forgetting 'use client'

// WRONG - Will error because useState needs client
import { useState } from 'react'

export function Component() {
  const [count, setCount] = useState(0) // ERROR!
}

// CORRECT
'use client'

import { useState } from 'react'

export function Component() {
  const [count, setCount] = useState(0)
}

Mistake #2: Not awaiting params

// WRONG
export default function Page({ params }) {
  const { id } = params // ERROR in Next.js 15!
}

// CORRECT
export default async function Page({ params }) {
  const { id } = await params
}

Mistake #3: Using client-side hooks in server components

// WRONG
export default function Page() {
  const router = useRouter() // ERROR!
}

// CORRECT - Add 'use client'
'use client'

export default function Page() {
  const router = useRouter()
}

Mistake #4: Importing server-only code in client components

// WRONG - Client component trying to use server-only code
'use client'

import { createServerClient } from '@/lib/supabase/server' // ERROR!

export function Component() {
  const supabase = await createServerClient() // Won't work!
}

// CORRECT - Use Server Action instead
'use client'

import { getData } from './actions'

export function Component() {
  const handleClick = async () => {
    const data = await getData() // Calls server action
  }
}

Environment Variables

Public vs Private

// ✅ Public (available in browser)
const key = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY

// ✅ Private (server-only)
const secret = process.env.STRIPE_SECRET_KEY // Only works in server components/actions

Accessing in Client Components

// ❌ WRONG - Private env vars don't work in client
'use client'

const secret = process.env.STRIPE_SECRET_KEY // undefined!

// ✅ CORRECT - Use public env vars
'use client'

const publicKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY

Caching

Next.js 15 has granular caching:

// Cache indefinitely (static data)
const data = await fetch('https://api.example.com/data', {
  cache: 'force-cache',
})

// Revalidate every hour
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 3600 },
})

// Never cache (dynamic data)
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store',
})

Route Handlers (API Routes)

// app/api/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const id = searchParams.get('id')

  return Response.json({ id })
}

export async function POST(request: Request) {
  const body = await request.json()

  return Response.json({ success: true })
}

Dynamic Routes

// app/cases/[caseId]/page.tsx
type Props = {
  params: Promise<{ caseId: string }>
}

export default async function CasePage({ params }: Props) {
  const { caseId } = await params

  return <div>Case: {caseId}</div>
}

Middleware

Use for auth checks, redirects, etc.:

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isProtectedRoute = createRouteMatcher([
  '/dashboard(.*)',
  '/cases(.*)',
])

export default clerkMiddleware((auth, req) => {
  if (isProtectedRoute(req)) auth().protect()
})

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}