Make your app resilient and responsive — error boundaries that contain crashes, Suspense for streaming slow content, portals for modals, and Server Actions for mutations.
Why: a render error with no boundary blanks the entire app. A boundary catches errors from everything below it and shows a fallback instead, with a reset hook to retry. Note: in Next.js, an error.tsx file gives every route segment its own boundary automatically.
$ pnpm add react-error-boundary'use client'
import { ErrorBoundary } from 'react-error-boundary'
function Widget() {
// Imagine this throws while rendering
throw new Error('Widget crashed')
}
export default function Dashboard() {
return (
// The crash stops here — the rest of the app keeps working
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Something broke: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<Widget />
</ErrorBoundary>
)
}Why: Suspense shows a fallback while something inside is not ready — an async Server Component, a lazy-loaded chunk, a suspending query. In Next.js this is how you stream a slow section in without blocking the whole page. Note: loading.tsx wraps a route in Suspense for you.
import { Suspense } from 'react'
async function SlowStats() {
const res = await fetch('https://api.example.com/stats') // imagine: slow
const stats = await res.json()
return <p>{stats.total} visits this week</p>
}
export default function Page() {
return (
<main>
<h1>Dashboard</h1>
{/* The page shows instantly; the slow part streams in when ready */}
<Suspense fallback={<p>Loading stats…</p>}>
<SlowStats />
</Suspense>
</main>
)
}Why: modals, tooltips, and toasts break when a parent element clips its children (overflow: hidden) or makes them render underneath other content (z-index). createPortal escapes that by putting the element somewhere else on the page — usually at the end of <body> — while it still behaves like a normal child in your code: same state, same events.
'use client'
import { useState } from 'react'
import { createPortal } from 'react-dom'
export default function Page() {
const [open, setOpen] = useState(false)
return (
<div style={{ overflow: 'hidden' }}> {/* would clip a normal child */}
<button onClick={() => setOpen(true)}>Open modal</button>
{open &&
createPortal(
// Renders at the end of <body>, escaping overflow and z-index traps
<div className="modal">
<p>I render into document.body</p>
<button onClick={() => setOpen(false)}>Close</button>
</div>,
document.body,
)}
</div>
)
}Why: a Server Action is a function marked 'use server' that runs on the server but is called like a local function — wire it straight to a form's action and the form submits to server code with no API route and no fetch call. It even works before JavaScript finishes loading, since the browser falls back to a plain form submit.
// app/actions.ts
'use server' // every function exported here runs only on the server
export async function createPost(formData: FormData) {
// formData holds the form's fields, looked up by their name attribute
const title = formData.get('title') as string
// …insert into your database here
console.log('created:', title)
}
// app/new-post/page.tsx
import { createPost } from '../actions'
export default function NewPost() {
return (
// The form posts straight to server code
<form action={createPost}>
<input name="title" placeholder="Post title" />
<button>Publish</button>
</form>
)
}