Table of Contents
- Course Overview
- Introduction to Server Components
- Advanced Server Component Scenarios
- Async Data in Server Components
- Suspense Boundaries with Server Components
- Next.js: Toolchain, Progressive Rendering, and Error Handling
- URL Routing with Server Components
- Migrating a SPA to Server Components
- Reference Tables
- Architectural Diagrams
1. Course Overview
This course covers the fundamentals of React Server Components and their integration with Client Components in a modern React application using Next.js. The main topics are:
- Building Server Components directly
- Communication between Server and Client Components
- File-system-based routing with Server Components
- Advanced UI techniques combining both component types
Prerequisites: Modern JavaScript, basic knowledge of React.
2. Introduction to Server Components
What is a React Server Component?
A Server Component is a React component that runs exclusively on a Node server. It generates HTML that is streamed to the browser. It cannot:
- Handle events (onClick, onChange, etc.)
- Use hooks like
useState,useEffect,useContext, etc.
On the other hand, it can:
- Make direct database calls
- Call REST APIs without CORS issues (server-to-server communication)
- Import
server-onlymodules - Render Client Components as children
Why Use React Server Components?
| Advantage | Description |
|---|---|
| Improved performance | Reduces client-side JavaScript, speeds up initial load |
| Reduced network traffic | Only the necessary HTML is sent to the client |
| Better SEO | Server-rendered content is indexed by search engines |
| Simplified data fetching | Data fetched directly in the component, no useEffect needed |
| Clear separation | Server = data/display; Client = interactivity |
| Better UX | Progressive loading via React’s concurrent rendering |
Difference Between Server Components and SSR
| Aspect | Classic SSR | React Server Components |
|---|---|---|
| Rendering | All HTML at once | Progressive streaming |
| Suspense | Not native | Built-in with <Suspense> |
| State | Not possible | Not possible (but Client Components can have state) |
| Async data | Blocks the entire render | Only the waiting component is suspended |
React 19 Note: In React 19, all components in the
/appfolder of Next.js are Server Components by default. You must add'use client'to opt into a Client Component.
Incremental Rendering of Multiple Server Components
Browser refresh
├── Header (immediate render — no async data)
├── Footer (immediate render)
└── SessionList (waiting...)
├── [animated placeholder shown]
└── After 2s → SessionListItems rendered
└── SessionVideo (waiting for each item...)
├── [animated placeholder]
└── After Xs → thumbnail + view count rendered
Server Components with Client Component Children
The fundamental rule: a Server Component can render a Client Component as a child, but the props passed must be serializable (no functions, no non-serializable class instances).
// page.tsx — Server Component (default in /app)
import AppHeaderClock from './AppHeaderClock'; // Client Component
export default function AppHeader() {
const isoDateString = new Date().toISOString();
return (
<header>
<h1>Tech Conference</h1>
<AppHeaderClock isoDateString={isoDateString} />
</header>
);
}
// AppHeaderClock.tsx — Client Component
'use client';
import { useState, useEffect } from 'react';
export default function AppHeaderClock({ isoDateString }: { isoDateString: string }) {
const [time, setTime] = useState(new Date(isoDateString));
useEffect(() => {
const timer = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(timer);
}, []);
return <span>{time.toLocaleTimeString()}</span>;
}
3. Advanced Server Component Scenarios
Client Components Calling Other Client Components
When a Client Component calls another, the second is also a Client Component (even without an explicit 'use client', because it is imported from a Client Component). Best practice: always declare 'use client' explicitly.
// app-show-sun.tsx — Client Component
'use client';
export default function AppShowSun({ isoDateString }: { isoDateString: string }) {
const brightness = getSunBrightness(isoDateString);
return <div>☀️ Brightness: {brightness}%</div>;
}
Third-Party APIs and use client
npm libraries that use browser APIs (drag & drop, state, events) must be called from a Client Component. Example using useCounter from the rooks library:
// ❌ ERROR — called from a Server Component
import { useCounter } from 'rooks'; // useState inside → error
// ✅ Correct — called from a Client Component
'use client';
import { useCounter } from 'rooks';
export default function AppHeaderClock({ isoDateString }) {
const [count, { incrementBy }] = useCounter(0);
// ...
}
Server Components as Children of Client Components
This is the most advanced pattern. A Server Component cannot be directly imported inside a Client Component — it must be passed as children from a parent (Server Component):
// page.tsx — Parent Server Component
import AppHeaderClock from './AppHeaderClock'; // Client
import AppServerComponent from './AppServerComponent'; // Server
export default function AppHeader() {
return (
<AppHeaderClock>
{/* AppServerComponent passed as children — correct pattern */}
<AppServerComponent />
</AppHeaderClock>
);
}
// AppHeaderClock.tsx — Client Component
'use client';
export default function AppHeaderClock({ children }) {
return (
<div>
<Clock />
{children} {/* Server Component rendered here */}
</div>
);
}
// AppServerComponent.tsx — Server Component
import 'server-only'; // best practice
export default function AppServerComponent() {
return <p>I am a Server Component</p>;
}
Theme Provider as a Client Component
For a dark/light theme that affects the entire component tree:
// app-theme-provider.tsx — Client Component
'use client';
import { createContext, useContext, useState } from 'react';
export const ThemeContext = createContext({ darkTheme: false, toggleTheme: () => {} });
export default function AppThemeProvider({ children }: { children: React.ReactNode }) {
const [darkTheme, setDarkTheme] = useState(false);
return (
<ThemeContext.Provider value={{ darkTheme, toggleTheme: () => setDarkTheme(t => !t) }}>
{children}
</ThemeContext.Provider>
);
}
// app-show-theme.tsx — Deeply nested Client Component
'use client';
import { useContext } from 'react';
import { ThemeContext } from './app-theme-provider';
export default function AppShowTheme() {
const { darkTheme } = useContext(ThemeContext);
return <span>Current theme: {darkTheme ? 'Dark' : 'Light'}</span>;
}
HTML Forms with Server Components
Server Functions (formerly called Server Actions) allow a form to be processed entirely on the server:
// actions/save-profile.ts — Server Function
'use server';
export async function saveProfile(formData: FormData) {
const username = formData.get('username') as string;
// Simulate an async operation (e.g., DB save)
await new Promise(resolve => setTimeout(resolve, 2000));
console.log('Profile saved:', username);
}
// page.tsx — Server Component with form
import { saveProfile } from './actions/save-profile';
import SubmitButton from './SubmitButton';
export default function ProfilePage() {
return (
<form action={saveProfile}>
<input type="text" name="username" placeholder="Your username" />
<SubmitButton />
</form>
);
}
// SubmitButton.tsx — Client Component for UI feedback
'use client';
import { useFormStatus } from 'react-dom';
export default function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Saving...' : 'Save'}
</button>
);
}
4. Async Data in Server Components
Basic Pattern: Async Server Component
// sessions/page.tsx — Async Server Component
import 'server-only';
type Session = {
id: number;
title: string;
speakerId: number;
youtubeId?: string;
};
async function getSessionsList(): Promise<Session[]> {
// Simulate a REST call (2s delay)
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch('http://localhost:3000/api/sessiondata');
const data = await res.json();
return data.sessions as Session[];
}
export default async function SessionsList() {
const sessions = await getSessionsList();
return (
<ul>
{sessions.map(session => (
<li key={session.id}>
<h3>{session.title}</h3>
<SessionVideo youtubeId={session.youtubeId} />
</li>
))}
</ul>
);
}
Multiple Independent Async Server Components
Each Server Component manages its own asynchronous data independently. Streaming enables progressive display:
// SessionVideo.tsx — Independent async Server Component
import 'server-only';
type YouTubeData = {
thumbnailUrl: string;
viewCount: number;
};
async function getVideoData(videoId: string): Promise<YouTubeData> {
await new Promise(resolve => setTimeout(resolve, 1500));
const res = await fetch(`http://localhost:3000/api/youtubedata/${videoId}`);
return res.json();
}
export default async function SessionVideo({ youtubeId }: { youtubeId?: string }) {
if (!youtubeId) return null;
const videoData = await getVideoData(youtubeId);
return (
<div>
<img src={videoData.thumbnailUrl} alt="Video thumbnail" />
<span>{videoData.viewCount.toLocaleString()} views</span>
</div>
);
}
Cascading dependencies: Sessions must be loaded before videos (the youtubeId values come from session data). React handles this naturally through the component tree structure.
5. Suspense Boundaries with Server Components
Introduction to React Suspense
<Suspense> allows displaying a fallback (placeholder) while a component waits for async data. Advantages:
- Avoids blank screens or generic loading messages
- Fine-grained control over rendering order
- Natural integration with Error Boundaries
// app/page.tsx — Root with Suspense
import { Suspense } from 'react';
import SessionsList from './sessions/SessionsList';
import SessionsLoading from './sessions/SessionsLoading';
export default function Page() {
return (
<main>
<AppHeader />
<Suspense fallback={<SessionsLoading />}>
<SessionsList />
</Suspense>
<AppFooter />
</main>
);
}
// SessionsLoading.tsx — Animated placeholder component
export default function SessionsLoading() {
return (
<div className="sessions-loading">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="placeholder-card animate-pulse" />
))}
</div>
);
}
Nested Suspense
You can nest <Suspense> boundaries for even more granular rendering:
// SessionListItem.tsx — Suspense per individual item
import { Suspense } from 'react';
import SessionVideo from './SessionVideo';
import SessionVideoLoading from './SessionVideoLoading';
export default function SessionListItem({ session }) {
return (
<div>
<h3>{session.title}</h3>
<Suspense fallback={<SessionVideoLoading />}>
<SessionVideo youtubeId={session.youtubeId} />
</Suspense>
</div>
);
}
Dynamic Search with Client Components and React Context
Pattern for filtering a list of rendered Server Components without a network round-trip:
// SessionsQueryProvider.tsx — Client Component (Context Provider)
'use client';
import { createContext, useContext, useState } from 'react';
const SessionsQueryContext = createContext({ query: '', setQuery: (_: string) => {} });
export function SessionsQueryProvider({ children }: { children: React.ReactNode }) {
const [query, setQuery] = useState('');
return (
<SessionsQueryContext.Provider value={{ query, setQuery }}>
{children}
</SessionsQueryContext.Provider>
);
}
export function useSessionsQuery() {
return useContext(SessionsQueryContext);
}
// SearchInput.tsx — Client Component
'use client';
import { useSessionsQuery } from './SessionsQueryProvider';
export default function SearchInput() {
const { query, setQuery } = useSessionsQuery();
return (
<input
type="search"
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search sessions..."
/>
);
}
// SessionListItemClient.tsx — Client Component wrapper that filters
'use client';
import { useSessionsQuery } from './SessionsQueryProvider';
export default function SessionListItemClient({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
const { query } = useSessionsQuery();
const visible = title.toLowerCase().includes(query.toLowerCase());
return <div style={{ display: visible ? 'block' : 'none' }}>{children}</div>;
}
// SessionsList.tsx — Server Component using the Client wrapper
import 'server-only';
import SessionListItem from './SessionListItem'; // Server Component
import SessionListItemClient from './SessionListItemClient'; // Client Component
export default async function SessionsList() {
const sessions = await getSessionsList();
return (
<ul>
{sessions.map(session => (
<SessionListItemClient key={session.id} title={session.title}>
<SessionListItem session={session} />
</SessionListItemClient>
))}
</ul>
);
}
6. Next.js: Toolchain, Progressive Rendering, and Error Handling
Files Prescribed by the Next.js App Router
| File | Role | Type |
|---|---|---|
page.tsx | Main component for the route (required) | Server or Client Component |
layout.tsx | Shared layout across child routes | Server or Client Component |
loading.tsx | Automatic Suspense fallback | Server Component |
error.tsx | Automatic Error Boundary | Client Component (required) |
not-found.tsx | 404 page for the route | Server Component |
template.tsx | Like layout but recreates components on each navigation | Server or Client Component |
app/
├── layout.tsx ← Global layout
├── page.tsx ← Route: localhost:3000/
├── (sessions)/ ← Route Group (does not affect URL)
│ ├── layout.tsx ← Shared layout for the group
│ ├── sessions/
│ │ ├── page.tsx ← Route: localhost:3000/sessions
│ │ ├── loading.tsx ← Placeholder during loading
│ │ └── error.tsx ← Error handling
│ └── session-lines/
│ └── page.tsx ← Route: localhost:3000/session-lines
└── speakers/
├── page.tsx ← Route: localhost:3000/speakers
└── [speakerId]/
└── page.tsx ← Dynamic route: localhost:3000/speakers/1124
Example loading.tsx:
// app/sessions/loading.tsx
import SpeakerDetailLoading from '@/common/SpeakerDetailLoading';
export default function SessionsLoading() {
return (
<div>
{Array.from({ length: 4 }).map((_, i) => (
<SpeakerDetailLoading key={i} />
))}
</div>
);
}
Example error.tsx:
// app/sessions/error.tsx — MUST be a Client Component
'use client';
export default function SessionsError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Error loading sessions</h2>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
);
}
Error Boundaries
For custom error handling, use the react-error-boundary package:
npm install react-error-boundary
// ErrorBoundaryFunctionalWrapper.tsx — Client Component
'use client';
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error }: { error: Error }) {
return (
<div className="error-container">
<h3>Something went wrong</h3>
<p>{error.message}</p>
</div>
);
}
export default function ErrorBoundaryFunctionalWrapper({
children,
}: {
children: React.ReactNode;
}) {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
{children}
</ErrorBoundary>
);
}
Nested Error Boundaries: You can wrap each component individually for granular error handling:
// page.tsx — Error Boundaries per component
{sessions.map(session => (
<ErrorBoundaryFunctionalWrapper key={session.id}>
<Suspense fallback={<SpeakerDetailLoading />}>
<SpeakerDetail speakerId={session.speakerId} />
</Suspense>
</ErrorBoundaryFunctionalWrapper>
))}
7. URL Routing with Server Components
File-System-Based Routing
Next.js directly maps the folder structure to URL routes:
app/page.tsx → localhost:3000/
app/sessions/page.tsx → localhost:3000/sessions
app/speakers/page.tsx → localhost:3000/speakers
Creating a new project:
npx create-next-app@latest
# TypeScript: Yes
# ESLint: Yes
# Tailwind: No (or per preference)
# src/ directory: Yes
# App Router: Yes
Dynamic Routes
// app/speakers/[speakerId]/page.tsx
import 'server-only';
type Props = {
params: { speakerId: string };
};
async function getSpeaker(speakerId: string) {
await new Promise(resolve => setTimeout(resolve, 1000));
const res = await fetch(`http://localhost:3000/api/speakers/${speakerId}`);
return res.json();
}
export default async function SpeakerDetailPage({ params }: Props) {
const speaker = await getSpeaker(params.speakerId);
return (
<div>
<img src={speaker.imageUrl} alt={speaker.firstName} />
<h1>{speaker.firstName} {speaker.lastName}</h1>
<p>{speaker.bio}</p>
</div>
);
}
Linking to a dynamic route from a Server Component:
// SpeakerCard.tsx — Server Component
import Link from 'next/link';
export default function SpeakerCard({ speaker }) {
return (
<div>
<Link href={`/speakers/${speaker.id}`}>
{speaker.firstName} {speaker.lastName}
</Link>
</div>
);
}
Route Groups
Route Groups (folders in parentheses) allow sharing a layout without affecting the URL:
app/
└── (sessions)/ ← Route Group (not part of URL)
├── layout.tsx ← Shared layout for sessions and session-lines
├── sessions/
│ └── page.tsx ← localhost:3000/sessions
└── session-lines/
└── page.tsx ← localhost:3000/session-lines
// app/(sessions)/layout.tsx — Group layout
'use client'; // if navigation requires usePathname
import Link from 'next/link';
import { usePathname } from 'next/navigation';
export default function SessionsGroupLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
return (
<div>
<nav>
<Link href="/sessions" className={pathname === '/sessions' ? 'active' : ''}>
Grid View
</Link>
<Link href="/session-lines" className={pathname === '/session-lines' ? 'active' : ''}>
List View
</Link>
</nav>
{children}
</div>
);
}
Path aliases (tsconfig.json):
{
"compilerOptions": {
"paths": {
"@/common/*": ["./src/app/common/*"],
"@/sessions/*": ["./src/app/(sessions)/sessions/*"],
"@/speakers/*": ["./src/app/speakers/*"]
}
}
}
8. Migrating a SPA to Server Components
SPA vs RSC Comparison
Data Fetching — SpeakersList
SPA (Client Component only):
// speakers/SpeakersList.tsx — Client Component (SPA)
'use client';
import { useState, useEffect } from 'react';
import type { Speaker } from '@/common/types';
export default function SpeakersList() {
const [speakers, setSpeakers] = useState<Speaker[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('/api/speakers')
.then(res => {
if (!res.ok) throw new Error('Network error');
return res.json();
})
.then(data => {
setSpeakers(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <div>Loading speakers...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{speakers.map(s => <SpeakerCard key={s.id} speaker={s} />)}
</ul>
);
}
RSC (Server Component):
// speakers/SpeakersList.tsx — Server Component (RSC)
import 'server-only';
import type { Speaker } from '@/common/types';
async function getSpeakers(): Promise<Speaker[]> {
await new Promise(resolve => setTimeout(resolve, 2000)); // simulation
const res = await fetch('http://localhost:3000/api/speakers');
return res.json();
}
export default async function SpeakersList() {
const speakers = await getSpeakers();
// No loading/error handling here → managed by loading.tsx and error.tsx
return (
<ul>
{speakers.map(s => <SpeakerCard key={s.id} speaker={s} />)}
</ul>
);
}
Key Migration Points
| Aspect | SPA (before) | RSC (after) |
|---|---|---|
| Loading handling | useState(true) + conditionals | loading.tsx or <Suspense> |
| Error handling | try/catch + state | error.tsx or <ErrorBoundary> |
| JS bundle size | Large (everything included) | Reduced (Server Components excluded) |
| Data fetching | useEffect on the browser | Direct async/await on the server |
| Deployment | Possible on static CDN | Requires a Node server |
| Code complexity | High (lifecycle management) | Low (simple async/await) |
9. Reference Tables
Server Components vs Client Components
| Feature | Server Component | Client Component |
|---|---|---|
| Directive | None (default in /app) | 'use client' at top of file |
| Environment | Node server | Browser |
| React hooks | ❌ Not supported | ✅ useState, useEffect, etc. |
| Events | ❌ onClick, onChange, etc. | ✅ All event handlers |
| Direct DB access | ✅ Yes | ❌ No (security) |
| REST calls | ✅ No CORS | ✅ With CORS |
import 'server-only' | ✅ Recommended | ❌ Error |
| React Context | ❌ No (as consumer) | ✅ Yes |
| Included in JS bundle | ❌ No | ✅ Yes |
| Streaming | ✅ Via Suspense | ❌ Not applicable |
What to Use Where?
| Need | Component type |
|---|---|
| Display data from an API/DB | Server Component |
| Make a REST call | Server Component (preferred) |
| Form with visual feedback | Server Component + Client Component for the button |
Local state (useState) | Client Component |
| Timers, animations | Client Component |
| npm libraries with hooks | Client Component |
| Dark/light theme toggle | Client Component (Context Provider) |
| SEO-critical content | Server Component |
| Static layout/navigation | Server Component |
| Local search/filtering | Client Component |
useContext as consumer | Client Component |
Props Passing Rules
| Prop type | Server → Client | Client → Server |
|---|---|---|
string, number, boolean | ✅ | ❌ (not direct) |
null, undefined | ✅ | ❌ |
| Plain objects (serializable) | ✅ | ❌ |
| Arrays of serializable types | ✅ | ❌ |
| Functions | ❌ | ❌ |
| Class instances | ❌ | ❌ |
children (JSX) | ✅ via wrapper pattern | ✅ children pattern |
| Dates | ✅ (convert to string) | ❌ |
10. Architectural Diagrams
Network Boundary: Server vs Client Components
graph TB
subgraph NODE["Node Server"]
direction TB
SC1["AppHeader<br/>(Server Component)"]
SC2["SessionsList<br/>(Server Component)"]
SC3["SessionVideo<br/>(Server Component)"]
SC4["SpeakerDetail<br/>(Server Component)"]
API1[("REST API<br/>/api/sessions")]
API2[("REST API<br/>/api/youtube")]
DB[("Database")]
SC2 -->|"await fetch()"| API1
SC3 -->|"await fetch()"| API2
SC4 -->|"direct query"| DB
end
BOUNDARY["═══════════ Network boundary (HTML + JSON streaming) ═══════════"]
subgraph BROWSER["Browser"]
direction TB
CC1["AppHeaderClock<br/>'use client'"]
CC2["SessionsQueryProvider<br/>'use client'"]
CC3["SearchInput<br/>'use client'"]
CC4["SessionListItemClient<br/>'use client'"]
CC5["SubmitButton<br/>'use client'"]
end
SC1 -->|"serializable props"| BOUNDARY
SC2 -->|"children (HTML)"| BOUNDARY
BOUNDARY --> CC1
BOUNDARY --> CC2
BOUNDARY --> CC3
BOUNDARY --> CC4
RSC (React Server Components) Flow
sequenceDiagram
participant B as Browser
participant N as Node Server
participant API as External APIs
B->>N: GET localhost:3000/sessions
Note over N: React concurrent rendering starts
N-->>B: Stream: Header + Footer + Suspense placeholders
Note over N: SessionsList waiting...
N->>API: fetch('/api/sessiondata')
API-->>N: JSON sessions (after 2s)
N-->>B: Stream: Sessions HTML (replaces placeholders)
Note over N: SessionVideo × N waiting...
par For each video
N->>API: fetch('/api/youtubedata/videoId')
API-->>N: JSON video data
N-->>B: Stream: Individual video HTML
end
Note over B: App fully interactive
B->>B: Hydration of Client Components
Streaming with Suspense
graph TD
A["page.tsx<br/>(Server Component)"] --> B["<Suspense fallback=<LoadingHeader/>>"]
A --> C["AppHeader → immediate render"]
A --> D["AppFooter → immediate render"]
B --> E["SessionsList<br/>(async Server Component)"]
E -->|"waiting"| F["<Suspense fallback=<SessionsLoading/>>"]
E -->|"resolved"| G["SessionListItem × N"]
G --> H["<Suspense fallback=<VideoLoading/>>"]
H -->|"waiting"| I["SessionVideoLoading<br/>(animated placeholder)"]
H -->|"resolved"| J["SessionVideo<br/>(thumbnail + views)"]
style C fill:#f5a623,color:#000
style D fill:#f5a623,color:#000
style E fill:#f5a623,color:#000
style G fill:#f5a623,color:#000
style J fill:#f5a623,color:#000
style I fill:#4a90d9,color:#fff
style F fill:#4a90d9,color:#fff
style H fill:#4a90d9,color:#fff
Legend: 🟠 Orange = Server Component | 🔵 Blue = Client Component / Suspense
Key Takeaways
-
By default, in the Next.js App Router, everything is a Server Component. Add
'use client'only when necessary. -
Props passed from Server to Client must be serializable — no functions, no non-serializable class instances.
-
To pass a Server Component as a child of a Client Component: the Server Component must be instantiated in a parent Server Component and passed via
children. -
<Suspense>is the central mechanism for progressive rendering. Next.js handles it automatically vialoading.tsx. -
error.tsxmust always be a Client Component (it uses event handlers for the “retry” button). -
Server Functions (
'use server') enable server-side calls directly from HTML forms without an explicit REST endpoint. -
Migrating SPA → RSC drastically reduces the complexity of async state management code (no more
useState+useEffectfor loading/error). -
Deployment: apps with Server Components require a Node server (no purely static deployment).
Search Terms
server · component · fundamentals · react · typescript · frontend · development · components · client · suspense · async · boundaries · children · data · dynamic · error · next.js · rendering · routing · rsc · spa