Advanced

Server Component Fundamentals in React 19

React Server Components — async data, Suspense, Next.js routing and migrating a SPA.

Table of Contents

  1. Course Overview
  2. Introduction to Server Components
  3. Advanced Server Component Scenarios
  4. Async Data in Server Components
  5. Suspense Boundaries with Server Components
  6. Next.js: Toolchain, Progressive Rendering, and Error Handling
  7. URL Routing with Server Components
  8. Migrating a SPA to Server Components
  9. Reference Tables
  10. 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-only modules
  • Render Client Components as children

Why Use React Server Components?

AdvantageDescription
Improved performanceReduces client-side JavaScript, speeds up initial load
Reduced network trafficOnly the necessary HTML is sent to the client
Better SEOServer-rendered content is indexed by search engines
Simplified data fetchingData fetched directly in the component, no useEffect needed
Clear separationServer = data/display; Client = interactivity
Better UXProgressive loading via React’s concurrent rendering

Difference Between Server Components and SSR

AspectClassic SSRReact Server Components
RenderingAll HTML at onceProgressive streaming
SuspenseNot nativeBuilt-in with <Suspense>
StateNot possibleNot possible (but Client Components can have state)
Async dataBlocks the entire renderOnly the waiting component is suspended

React 19 Note: In React 19, all components in the /app folder 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

FileRoleType
page.tsxMain component for the route (required)Server or Client Component
layout.tsxShared layout across child routesServer or Client Component
loading.tsxAutomatic Suspense fallbackServer Component
error.tsxAutomatic Error BoundaryClient Component (required)
not-found.tsx404 page for the routeServer Component
template.tsxLike layout but recreates components on each navigationServer 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

AspectSPA (before)RSC (after)
Loading handlinguseState(true) + conditionalsloading.tsx or <Suspense>
Error handlingtry/catch + stateerror.tsx or <ErrorBoundary>
JS bundle sizeLarge (everything included)Reduced (Server Components excluded)
Data fetchinguseEffect on the browserDirect async/await on the server
DeploymentPossible on static CDNRequires a Node server
Code complexityHigh (lifecycle management)Low (simple async/await)

9. Reference Tables

Server Components vs Client Components

FeatureServer ComponentClient Component
DirectiveNone (default in /app)'use client' at top of file
EnvironmentNode serverBrowser
React hooks❌ Not supporteduseState, useEffect, etc.
EventsonClick, 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?

NeedComponent type
Display data from an API/DBServer Component
Make a REST callServer Component (preferred)
Form with visual feedbackServer Component + Client Component for the button
Local state (useState)Client Component
Timers, animationsClient Component
npm libraries with hooksClient Component
Dark/light theme toggleClient Component (Context Provider)
SEO-critical contentServer Component
Static layout/navigationServer Component
Local search/filteringClient Component
useContext as consumerClient Component

Props Passing Rules

Prop typeServer → ClientClient → Server
string, number, boolean❌ (not direct)
null, undefined
Plain objects (serializable)
Arrays of serializable types
Functions
Class instances
children (JSX)✅ via wrapper patternchildren 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

  1. By default, in the Next.js App Router, everything is a Server Component. Add 'use client' only when necessary.

  2. Props passed from Server to Client must be serializable — no functions, no non-serializable class instances.

  3. 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.

  4. <Suspense> is the central mechanism for progressive rendering. Next.js handles it automatically via loading.tsx.

  5. error.tsx must always be a Client Component (it uses event handlers for the “retry” button).

  6. Server Functions ('use server') enable server-side calls directly from HTML forms without an explicit REST endpoint.

  7. Migrating SPA → RSC drastically reduces the complexity of async state management code (no more useState + useEffect for loading/error).

  8. 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.