JavaScript and React course Β· Module 10: React Ecosystem and Future

React Server Components (RSC) - The Future of React

13 min read
In this lesson6

The mission panel shows posts from the database and a like button. In a classic application, all of the component code, together with the libraries and query logic, ends up in the browser, and you fetch the data only after the page has loaded. React Server Components flip this split: some components run only on the server, have direct access to data and do not add a single byte to the JavaScript bundle. It is a fundamental change in how we think about React applications.

What Are React Server Components?

A Server Component renders on the server, at build time or on every request. Its output is not ready-made HTML, but the RSC Payload, a serialized description of the UI tree with slots for client components. The request cycle looks like this: the client asks for a page, the server components fetch data and create the payload, the client builds the tree from it, and finally the client components are hydrated, meaning they get their events and state. HTML can additionally be produced through SSR.

Differences Between RSC and SSR

Classic SSR renders the whole application to HTML, and then sends all of its code to the browser for hydration anyway. RSC lets you mix server and client components in one tree:

1// Classic SSR (Pages Router) - the data comes from getServerSideProps
2export async function getServerSideProps() {
3  const posts = await fetchPosts(); // Runs on the server on every request
4  return { props: { posts } };
5}
6
7export default function TraditionalSSRPage({ posts }) {
8  const [count, setCount] = useState(0);
9
10  // This component and all its imports also end up in the browser
11  return (
12    <div>
13      <PostsList posts={posts} />
14      <Counter count={count} setCount={setCount} />
15    </div>
16  );
17}
18
19// React Server Components - server and client mix
20async function ServerComponentPage() {
21  // This code runs ONLY on the server
22  const posts = await fetchPosts(); // Direct database call
23  const user = await getUser();
24
25  return (
26    <div>
27      {/* Server Component - rendered on server */}
28      <PostsList posts={posts} />
29
30      {/* Client Component - interactive on client */}
31      <Counter />
32
33      {/* Server Component with server data */}
34      <UserProfile user={user} />
35    </div>
36  );
37}

In classic SSR the data is fetched by a separate getServerSideProps function, and the page component receives it in its props. The server renders HTML from it, but the code of TraditionalSSRPage, PostsList and all their libraries still goes to the browser for hydration. ServerComponentPage is asynchronous and waits for the data itself: its code stays on the server, and only the interactive Counter reaches the browser.

Server Components Basics

In the Next.js App Router every component is a server component by default. The 'use client' directive at the very top of a file marks the boundary: this module and its imports go to the browser and can use hooks, events and state:

1// ServerComponent.jsx (server component by default)
2import { db } from './database';
3
4export default async function PostsList() {
5  // Direct database access
6  const posts = await db.posts.findMany({
7    include: { author: true },
8    orderBy: { createdAt: 'desc' }
9  });
10
11  return (
12    <div className="posts-list">
13      {posts.map(post => (
14        <article key={post.id}>
15          <h2>{post.title}</h2>
16          <p>By {post.author.name}</p>
17          <p>{post.excerpt}</p>
18
19          {/* Client Component for interaction */}
20          <LikeButton postId={post.id} initialLikes={post.likes} />
21        </article>
22      ))}
23    </div>
24  );
25}
26
27// LikeButton.jsx (client component)
28'use client'; // Directive marking a client component
29
30import { useState } from 'react';
31
32export default function LikeButton({ postId, initialLikes }) {
33  const [likes, setLikes] = useState(initialLikes);
34  const [isLiking, setIsLiking] = useState(false);
35
36  const handleLike = async () => {
37    setIsLiking(true);
38    try {
39      const response = await fetch(`/api/posts/${postId}/like`, {
40        method: 'POST'
41      });
42      const data = await response.json();
43      setLikes(data.likes);
44    } catch (error) {
45      console.error('Failed to like post:', error);
46    } finally {
47      setIsLiking(false);
48    }
49  };
50
51  return (
52    <button
53      onClick={handleLike}
54      disabled={isLiking}
55      className="like-button"
56    >
57      {likes} {isLiking && '...'}
58    </button>
59  );
60}

PostsList queries the database directly, and LikeButton handles clicks through useState. The server component renders the client one as a child and passes it serializable props, such as postId. In a project these are two separate files, because the directive only works at the beginning of a file.

Fetching Data in Server Components

A Server Component can safely use secrets and database queries, because its code never reaches the browser. React's cache function remembers the result within a single request:

1// utils/api.js (server-side only)
2import { cache } from 'react';
3
4// Automatic caching for the same request
5export const getPosts = cache(async () => {
6  const response = await fetch('https://api.example.com/posts', {
7    // Server Components can safely use secrets
8    headers: {
9      'Authorization': `Bearer ${process.env.API_SECRET}`
10    }
11  });
12  return response.json();
13});
14
15export const getUser = cache(async (id) => {
16  // Direct database query
17  return await db.user.findUnique({
18    where: { id },
19    include: { profile: true }
20  });
21});
22
23// components/PostsPage.jsx
24import { getPosts, getUser } from '../utils/api';
25import { Suspense } from 'react';
26
27export default async function PostsPage({ userId }) {
28  // Parallel data fetching
29  const [posts, user] = await Promise.all([
30    getPosts(),
31    getUser(userId)
32  ]);
33
34  return (
35    <main>
36      <h1>Welcome, {user.name}!</h1>
37
38      {/* Nested Server Component with its own data fetching */}
39      <Suspense fallback={<div>Loading posts...</div>}>
40        <PostsList posts={posts} />
41      </Suspense>
42
43      {/* Client Component for interaction */}
44      <CreatePostForm userId={userId} />
45    </main>
46  );
47}

Promise.all fetches the posts and the user in parallel. cache works only in Server Components and deduplicates calls with the same arguments, so several components can call getUser(userId) without extra queries.

Streaming and Suspense

Not every part of the page loads equally fast. Suspense with the fallback property marks a boundary: the server first sends a placeholder skeleton, and streams the finished sections afterwards:

1// components/DashboardPage.jsx
2import { Suspense } from 'react';
3
4export default function DashboardPage() {
5  return (
6    <div className="dashboard">
7      <h1>Dashboard</h1>
8
9      {/* Fast loading content */}
10      <Suspense fallback={<SkeletonCard />}>
11        <UserStats />
12      </Suspense>
13
14      {/* Slower loading content */}
15      <Suspense fallback={<SkeletonChart />}>
16        <AnalyticsChart />
17      </Suspense>
18
19      {/* Very slow loading content */}
20      <Suspense fallback={<SkeletonTable />}>
21        <ReportsTable />
22      </Suspense>
23    </div>
24  );
25}
26
27// components/UserStats.jsx (Server Component)
28async function UserStats() {
29  // Fast query - cache hit
30  const stats = await getUserStats();
31
32  return (
33    <div className="stats-grid">
34      <StatCard title="Total Users" value={stats.totalUsers} />
35      <StatCard title="Active Today" value={stats.activeToday} />
36      <StatCard title="Revenue" value={`$${stats.revenue}`} />
37    </div>
38  );
39}
40
41// components/AnalyticsChart.jsx (Server Component)
42async function AnalyticsChart() {
43  // Medium-length query
44  const chartData = await getAnalyticsData();
45
46  return (
47    <div className="chart-container">
48      {/* Client Component for interactive chart */}
49      <InteractiveChart data={chartData} />
50    </div>
51  );
52}
53
54// components/ReportsTable.jsx (Server Component)
55async function ReportsTable() {
56  // Long query - complex aggregations
57  const reports = await getComplexReports();
58
59  return (
60    <div className="reports-table">
61      <table>
62        <thead>
63          <tr>
64            <th>Report</th>
65            <th>Status</th>
66            <th>Date</th>
67          </tr>
68        </thead>
69        <tbody>
70          {reports.map(report => (
71            <tr key={report.id}>
72              <td>{report.name}</td>
73              <td>{report.status}</td>
74              <td>{report.date}</td>
75            </tr>
76          ))}
77        </tbody>
78      </table>
79    </div>
80  );
81}

Each section has its own boundary, so the slow reports table does not block the statistics. Suspense handles the loading state, and errors are caught by an Error Boundary, so together they cover the whole cycle of an asynchronous operation. It is the same streaming you learned about in the Streaming SSR lesson.

Composing Server and Client Components

Here are the composition patterns that work, and one that fails:

1// patterns/CompositionPatterns.jsx
2
3// GOOD - Server Component can render Client Component
4async function ServerParent() {
5  const data = await fetchData();
6
7  return (
8    <div>
9      <h1>Server Rendered Title</h1>
10      <ClientChild data={data} />
11    </div>
12  );
13}
14
15// GOOD - Client Component can render Server Component via children
16'use client';
17function ClientWrapper({ children }) {
18  const [isOpen, setIsOpen] = useState(false);
19
20  return (
21    <div>
22      <button onClick={() => setIsOpen(!isOpen)}>
23        Toggle Content
24      </button>
25      {isOpen && children}
26    </div>
27  );
28}
29
30// Usage:
31function ParentPage() {
32  return (
33    <ClientWrapper>
34      <ServerContent /> {/* Server Component as children */}
35    </ClientWrapper>
36  );
37}
38
39// BAD - Client Component cannot directly import Server Component
40'use client';
41function BadClientComponent() {
42  const [show, setShow] = useState(false);
43
44  return (
45    <div>
46      {show && <ServerContent />} {/* This won't work! */}
47    </div>
48  );
49}
50
51// GOOD - Server Component passed as a prop from JSX (slot)
52'use client';
53function GoodClientComponent({ content }) {
54  const [show, setShow] = useState(false);
55
56  return (
57    <div>
58      <button onClick={() => setShow(!show)}>Toggle</button>
59      {show && content}
60    </div>
61  );
62}
63
64// Usage:
65function ParentComponent() {
66  return (
67    <GoodClientComponent
68      content={<ServerContent />}
69    />
70  );
71}

The recommended pattern is a Server Component rendering a Client Component as a child. When a client component needs to contain a server one, pass it as children or as a prop from JSX. You cannot send a function, such as a render prop, from the server to the client, because it cannot be serialized, and importing a server component in a file with 'use client' turns it into a client component.

Advanced Patterns

Mutations with Server Functions

For changing data there are Server Functions with the 'use server' directive, called Server Actions in forms. The form calls them without you writing your own endpoint:

1// actions/postActions.js
2'use server'; // Server Actions
3
4import { revalidatePath } from 'next/cache';
5
6export async function createPost(prevState, formData) {
7  const title = formData.get('title');
8  const content = formData.get('content');
9
10  // Server-side validation
11  if (!title || !content) {
12    return { error: 'Title and content are required' };
13  }
14
15  try {
16    const post = await db.post.create({
17      data: { title, content, userId: getCurrentUserId() }
18    });
19
20    // Revalidate cache for the posts page
21    revalidatePath('/posts');
22
23    return { success: true, post };
24  } catch (error) {
25    return { error: 'Failed to create post' };
26  }
27}
28
29export async function deletePost(postId) {
30  try {
31    await db.post.delete({
32      where: { id: postId }
33    });
34
35    revalidatePath('/posts');
36    return { success: true };
37  } catch (error) {
38    return { error: 'Failed to delete post' };
39  }
40}
41
42// components/CreatePostForm.jsx
43'use client';
44
45import { createPost } from '../actions/postActions';
46import { useActionState } from 'react';
47import { useFormStatus } from 'react-dom';
48
49function SubmitButton() {
50  const { pending } = useFormStatus();
51
52  return (
53    <button type="submit" disabled={pending}>
54      {pending ? 'Creating...' : 'Create Post'}
55    </button>
56  );
57}
58
59export default function CreatePostForm() {
60  const [state, formAction] = useActionState(createPost, null);
61
62  return (
63    <form action={formAction}>
64      <div>
65        <label htmlFor="title">Title:</label>
66        <input
67          type="text"
68          id="title"
69          name="title"
70          required
71        />
72      </div>
73
74      <div>
75        <label htmlFor="content">Content:</label>
76        <textarea
77          id="content"
78          name="content"
79          required
80        />
81      </div>
82
83      {state?.error && (
84        <div className="error">{state.error}</div>
85      )}
86
87      {state?.success && (
88        <div className="success">Post created successfully!</div>
89      )}
90
91      <SubmitButton />
92    </form>
93  );
94}

useActionState passes the previous state to the action as the first argument and the form data as the second, which is why createPost takes prevState, formData. useFormStatus disables the button while submitting, and revalidatePath('/posts') refreshes the list. Validation in the action is mandatory, because an action is a public endpoint.

Caching Strategies

React's cache only works within a single request. To remember the result of an expensive query between requests, Next.js 16 has the 'use cache' directive, which you enable with the cacheComponents: true option in next.config.js. cacheLife sets how long an entry lives, and cacheTag gives it a label for invalidation:

1// lib/posts.js
2import { cacheLife, cacheTag } from 'next/cache';
3
4export async function getCachedPosts() {
5  'use cache';
6  cacheLife('minutes'); // Background refresh roughly every minute
7  cacheTag('posts');
8
9  return db.post.findMany({
10    include: { author: true },
11    orderBy: { createdAt: 'desc' },
12  });
13}
14
15export async function getCachedUser(userId) {
16  'use cache';
17  cacheLife('hours');
18  cacheTag('users');
19
20  // userId becomes part of the cache key
21  return db.user.findUnique({
22    where: { id: userId },
23    include: { profile: true },
24  });
25}
26
27// components/PostsWithCache.jsx
28import { getCachedPosts } from '../lib/posts';
29
30export default async function PostsWithCache() {
31  const posts = await getCachedPosts();
32
33  return (
34    <div>
35      <h1>Cached Posts</h1>
36      {posts.map(post => (
37        <article key={post.id}>
38          <h2>{post.title}</h2>
39          <p>By {post.author.name}</p>
40        </article>
41      ))}
42    </div>
43  );
44}
45
46// actions/cacheActions.js
47'use server';
48
49import { revalidateTag, updateTag } from 'next/cache';
50
51export async function refreshPosts() {
52  revalidateTag('posts', 'max'); // Old data stays, fresh data loads in the background
53}
54
55export async function publishPost(formData) {
56  await db.post.create({ data: { title: formData.get('title') } });
57  updateTag('posts'); // The author sees their post right away
58}

Function arguments, such as userId, automatically become part of the cache key, so every user gets a separate entry. revalidateTag('posts', 'max') marks the data as stale and refreshes it in the background on the next request, while updateTag, available only in Server Actions, expires it immediately so the author sees their change. The older unstable_cache was replaced by 'use cache' in Next.js 16, and the single-argument revalidateTag is deprecated.

Performance and Optimizations

The biggest gain from RSC is not visible in the code: server components and their imports do not end up in the client bundle:

1// Server Components don't end up in the client bundle!
2
3// ServerHeavyComponent.jsx (Server Component)
4import { someHeavyLibrary } from 'heavy-library'; // Won't be in client bundle!
5import { complexCalculation } from './utils/complex';
6
7export default async function ServerHeavyComponent() {
8  const data = await fetchData();
9  const processed = complexCalculation(data); // Happens on server
10  const result = someHeavyLibrary.process(processed);
11
12  return (
13    <div>
14      <h2>Processed Data</h2>
15      <pre>{JSON.stringify(result, null, 2)}</pre>
16    </div>
17  );
18}
19
20// ClientLightComponent.jsx (Client Component)
21'use client';
22
23import { useState } from 'react';
24
25export default function ClientLightComponent({ serverData }) {
26  const [selected, setSelected] = useState(null);
27
28  return (
29    <div>
30      {serverData.map(item => (
31        <button
32          key={item.id}
33          onClick={() => setSelected(item)}
34          className={selected?.id === item.id ? 'selected' : ''}
35        >
36          {item.name}
37        </button>
38      ))}
39      {selected && <ItemDetails item={selected} />}
40    </div>
41  );
42}

heavy-library runs only on the server, so the user does not download it. Only the lightweight ClientLightComponent reaches the browser.

Error Boundaries for Server Components

An error in a server component goes to the nearest error boundary on the client side. An Error Boundary has to be a class, so the file starts with 'use client':

1// components/ServerErrorBoundary.jsx
2'use client';
3
4import { Component } from 'react';
5
6export class ServerErrorBoundary extends Component {
7  constructor(props) {
8    super(props);
9    this.state = { hasError: false, error: null };
10  }
11
12  static getDerivedStateFromError(error) {
13    return { hasError: true, error };
14  }
15
16  componentDidCatch(error, errorInfo) {
17    console.error('Server component error:', error, errorInfo);
18    // Send to monitoring service
19  }
20
21  render() {
22    if (this.state.hasError) {
23      return (
24        <div className="error-boundary">
25          <h2>Something went wrong on the server</h2>
26          <details>
27            <summary>Error details</summary>
28            <pre>{this.state.error?.message}</pre>
29          </details>
30          <button
31            onClick={() => this.setState({ hasError: false })}
32          >
33            Try again
34          </button>
35        </div>
36      );
37    }
38
39    return this.props.children;
40  }
41}
42
43// Usage in layout
44export default function Layout({ children }) {
45  return (
46    <html>
47      <body>
48        <ServerErrorBoundary>
49          {children}
50        </ServerErrorBoundary>
51      </body>
52    </html>
53  );
54}

getDerivedStateFromError switches the view to a fallback screen, and componentDidCatch reports the error. In production Next.js hides the content of server errors, so you will see a generic message, and you will find the details in the logs. In Next.js you do not have to write this class yourself: an error.js file creates an error boundary for a whole route segment, and since version 16.3 the catchError function from next/error builds one from a function component and passes it retry(), which also re-renders Server Components.

The first framework to fully integrate RSC was Next.js with the App Router. I recommend starting every component as a server component and adding 'use client' only where you need state or events. RSC give you better performance, automatic code splitting and safer server-side operations, and in the Partial Pre-rendering lesson you will combine them with a static page shell.

Remember: Server Components are the ground station crew - they prepare the data and send up a ready plan, and only the modules someone actually steers make it on board. In the example below the "Show what reaches the browser" button marks which code stays on the server and which runs in the browser.

Code for this lesson: App.jsx
1import { Suspense, use, useState } from 'react';
2
3// === React Server Components - a simulation in the browser ===
4// A real Server Component is an asynchronous function that runs on the server:
5//   async function PostsList() { const posts = await db.post.findMany(); ... }
6// The browser cannot run such code, so we fake the "server" with a promise read
7// by use(), and Suspense handles the waiting, just like when the RSC Payload is
8// streamed.
9
10const database = {
11  stats: { posts: 156, crew: 89, online: 23 },
12  posts: [
13    { id: 1, title: 'Discovery of a new galaxy', author: 'Commander Nova', likes: 42 },
14    { id: 2, title: 'Space propulsion technologies', author: 'Astro', likes: 28 },
15    { id: 3, title: 'Life on Mars - a report', author: 'Ra', likes: 56 },
16  ],
17};
18
19const serverCache = new Map();
20
21// "Database query" - the same promise for the same table
22function queryServer(table, delay) {
23  if (!serverCache.has(table)) {
24    serverCache.set(table, new Promise(resolve => setTimeout(() => resolve(database[table]), delay)));
25  }
26  return serverCache.get(table);
27}
28
29function Tag({ type, show }) {
30  if (!show) return null;
31  const server = type === 'server';
32  return (
33    <span style={{ ...styles.tag, ...(server ? styles.serverTag : styles.clientTag) }}>
34      {server ? 'Server Component: 0 KB of code in the JS bundle' : "Client Component ('use client'): code in the JS bundle"}
35    </span>
36  );
37}
38
39// SERVER COMPONENT (simulation): no state or events, just data and JSX
40function ServerStats({ showBundle }) {
41  const stats = use(queryServer('stats', 600));
42
43  return (
44    <section style={{ ...styles.box, ...(showBundle ? styles.serverBox : null) }}>
45      <Tag type="server" show={showBundle} />
46      <div style={styles.statsGrid}>
47        <div style={styles.stat}><strong style={styles.statValue}>{stats.posts}</strong>Posts</div>
48        <div style={styles.stat}><strong style={styles.statValue}>{stats.crew}</strong>Crew members</div>
49        <div style={styles.stat}><strong style={styles.statValue}>{stats.online}</strong>Online</div>
50      </div>
51    </section>
52  );
53}
54
55// SERVER COMPONENT (simulation): renders a client component as a child
56// and passes it only serializable props (the number of likes)
57function ServerPostsList({ showBundle }) {
58  const posts = use(queryServer('posts', 1200));
59
60  return (
61    <section style={{ ...styles.box, ...(showBundle ? styles.serverBox : null) }}>
62      <Tag type="server" show={showBundle} />
63      {posts.map(post => (
64        <article key={post.id} style={styles.post}>
65          <h3 style={styles.postTitle}>{post.title}</h3>
66          <div style={styles.postMeta}>
67            <span>Author: {post.author}</span>
68            <LikeButton initialLikes={post.likes} showBundle={showBundle} />
69          </div>
70        </article>
71      ))}
72    </section>
73  );
74}
75
76// CLIENT COMPONENT: in a project, a separate file starting with 'use client'.
77// Only this code reaches the browser and can use state and events.
78function LikeButton({ initialLikes, showBundle }) {
79  const [liked, setLiked] = useState(false);
80  const likes = initialLikes + (liked ? 1 : 0);
81
82  return (
83    <span style={showBundle ? styles.clientBox : null}>
84      <Tag type="client" show={showBundle} />
85      <button onClick={() => setLiked(l => !l)} style={{ ...styles.like, color: liked ? '#ff6b6b' : '#8892b0' }}>
86        {liked ? 'Liked' : 'Like'} ({likes})
87      </button>
88    </span>
89  );
90}
91
92export default function App() {
93  const [showBundle, setShowBundle] = useState(false);
94
95  return (
96    <div style={styles.page}>
97      <div style={{ maxWidth: '700px', margin: '0 auto' }}>
98        <h1 style={styles.title}>React Server Components - simulation</h1>
99        <p style={styles.subtitle}>
100          Server components fetch the data, and only the interactive like button runs in the browser
101        </p>
102        <button onClick={() => setShowBundle(s => !s)} style={styles.toggle}>
103          {showBundle ? 'Hide the server and client split' : 'Show what reaches the browser'}
104        </button>
105
106        <Suspense fallback={<p style={styles.fallback}>The server is counting statistics...</p>}>
107          <ServerStats showBundle={showBundle} />
108        </Suspense>
109
110        <h2 style={styles.h2}>Posts from the database (Server Component + client LikeButton)</h2>
111        <Suspense fallback={<p style={styles.fallback}>The server is fetching posts from the database...</p>}>
112          <ServerPostsList showBundle={showBundle} />
113        </Suspense>
114      </div>
115    </div>
116  );
117}
118
119const styles = {
120  page: { minHeight: '100vh', background: 'linear-gradient(135deg, #0a0e27 0%, #1a1a3e 100%)', padding: '24px', color: '#e0e1dd', fontFamily: 'system-ui, sans-serif' },
121  title: { textAlign: 'center', color: '#00d4ff', margin: '0 0 6px', fontSize: '22px' },
122  subtitle: { textAlign: 'center', color: '#8892b0', margin: '0 0 14px', fontSize: '13px' },
123  toggle: { display: 'block', margin: '0 auto 16px', padding: '6px 14px', background: 'rgba(0,212,255,0.15)', color: '#00d4ff', border: '1px solid rgba(0,212,255,0.4)', borderRadius: '6px', cursor: 'pointer' },
124  fallback: { color: '#8892b0', padding: '16px', border: '1px dashed rgba(0,212,255,0.25)', borderRadius: '8px' },
125  box: { borderRadius: '10px', padding: '10px', marginBottom: '16px', border: '1px solid transparent' },
126  serverBox: { border: '1px dashed #ffa500', background: 'rgba(255,165,0,0.05)' },
127  clientBox: { display: 'inline-flex', flexDirection: 'column', alignItems: 'flex-end', gap: '2px', padding: '4px', border: '1px solid #00ff88', borderRadius: '6px' },
128  tag: { display: 'inline-block', fontSize: '10px', padding: '2px 6px', borderRadius: '4px', marginBottom: '6px' },
129  serverTag: { background: 'rgba(255,165,0,0.15)', color: '#ffa500' },
130  clientTag: { background: 'rgba(0,255,136,0.15)', color: '#00ff88', marginBottom: 0 },
131  statsGrid: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '10px' },
132  stat: { padding: '12px', background: 'rgba(0,212,255,0.08)', borderRadius: '8px', textAlign: 'center', fontSize: '12px', color: '#8892b0' },
133  statValue: { display: 'block', fontSize: '24px', color: '#00d4ff' },
134  h2: { color: '#64ffda', fontSize: '16px', margin: '0 0 8px' },
135  post: { padding: '10px 12px', margin: '6px 0', background: 'rgba(0,0,0,0.3)', borderRadius: '8px', border: '1px solid rgba(0,212,255,0.15)' },
136  postTitle: { color: '#00d4ff', margin: '0 0 6px', fontSize: '15px' },
137  postMeta: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '12px', color: '#8892b0', gap: '8px' },
138  like: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '12px' },
139};

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What does the 'use client' directive at the beginning of a component file mean?

  2. 2. What is the key difference between React Server Components and traditional SSR?

These are 2 of 5 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Click in order

    Arrange the Suspense boundary syntax with a fallback component:

  • Code editor

    Show constellation data from the observatory with Suspense and use() from React 19. ___BLANK1___: getConstellation stores in the cache the promise from fetchConstellation(name), so that every name is fetched only once. ___BLANK2___: ConstellationCard reads the data with use() from the promise returned by getConstellation(name) (do not create a new promise during render). ___BLANK3___: the fallback of the Suspense boundary is an element with a loading text, e.g. <p>Loading telescope data...</p>. After clicking another constellation you will see the fallback for a moment, and when you go back to one that was already fetched, the data appears at once.

  • Vertical ordering

    Order the stages of the Streaming SSR pipeline in React (since version 18):

  • Code editor

    Finish the MissionBoundary class, the error boundary for the bridge panels. The fuel sensor is broken and its panel throws an error, while the other panels must keep working. ___BLANK1___: getDerivedStateFromError returns the new state with an error field that holds the caught error ({ error }). ___BLANK2___: componentDidCatch reports the crash with reportCrash(error, info.componentStack). ___BLANK3___: when the state holds an error, render returns the view from this.props.renderFallback, passing it the error from state and the this.reset function (the class stays free of JSX). Clicking Restart sensor repairs the sensor and resets the boundary, so the fuel panel comes back showing 50%.

  • Horizontal ordering

    Arrange the syntax for wrapping a component in Error Boundary with fallback:

  • Vertical ordering

    Order the request lifecycle in the React Server Components architecture:

  • Click in order

    Order the Server Component implementation:

  • Code editor

    Complete the buttons of the Fleet component library. ___BLANK1___: the button is disabled when it has the disabled prop or while it is loading (loading). ___BLANK2___: the padding comes from the tokens: tokens.spacing for the chosen size (sm, md or lg). ___BLANK3___: IconButton shows only an icon, so pass Button an aria-label attribute with the value of label, so that a screen reader announces the button name. The components have no state and no effects, so they also work in Server Components.

Useful articles