Can I use React Server Components (RSCs) today?
Frameworks (libraries): 7 | Test cases: 12
- ✅ Rendering async component on the server
- ✅ Mixing server and client components
- ✅ Hydrating client component
- ✅ Passing server function to client component
- ✅ Client component imports server function
- ✅ Inlined server function (inside a server component)
- ✅ Server Functions with Actions
- ✅ Server Functions with useActionState
- ✅ Passing promise from server to client component
- ✅ Inspirational quote generator app
- ✅ Nested Client Components
- ✅ Inlined server action to access variables in the near scope
Vite
Official Vite plugin for React Server Components. Framework agnostic.
- ✅ Rendering async component on the server
- ✅ Mixing server and client components
- ✅ Hydrating client component
- ✅ Passing server function to client component
- ✅ Client component imports server function
- ✅ Inlined server function (inside a server component)
- ✅ Server Functions with Actions
- ✅ Server Functions with useActionState
- ✅ Passing promise from server to client component
- ✅ Inspirational quote generator app
- ✅ Nested Client Components
- ✅ Inlined server action to access variables in the near scope
Waku
A React framework with first-class support for React Server Components
- ✅ Rendering async component on the server
- ✅ Mixing server and client components
- ✅ Hydrating client component
- ✅ Passing server function to client component
- ✅ Client component imports server function
- ✅ Inlined server function (inside a server component)
- ✅ Server Functions with Actions
- ✅ Server Functions with useActionState
- ✅ Passing promise from server to client component
- ✅ Inspirational quote generator app
- ✅ Nested Client Components
- ✅ Inlined server action to access variables in the near scope
Forket
A framework agnostic library that produces server and client version of your code. Plugable to your current setup.
- ✅ Rendering async component on the server
- ✅ Mixing server and client components
- ✅ Hydrating client component
- ✅ Passing server function to client component
- ✅ Client component imports server function
- ✅ Inlined server function (inside a server component)
- ✅ Server Functions with Actions
- ✅ Server Functions with useActionState
- ✅ Passing promise from server to client component
- ✅ Inspirational quote generator app
- ❌ Nested Client Components
- ❌ Inlined server action to access variables in the near scope
Parcel
Official Parcel support for React Server Components. Framework agnostic.
- ✅ Rendering async component on the server
- ✅ Mixing server and client components
- ✅ Hydrating client component
- ✅ Passing server function to client component
- ✅ Client component imports server function
- ❌ Inlined server function (inside a server component)
- ✅ Server Functions with Actions
- ✅ Server Functions with useActionState
- ✅ Passing promise from server to client component
- ✅ Inspirational quote generator app
- ✅ Nested Client Components
- ❌ Inlined server action to access variables in the near scope
ReactRouter
Experimental version of React Router with RSC support
- ✅ Rendering async component on the server
- ✅ Mixing server and client components
- ✅ Hydrating client component
- ✅ Passing server function to client component
- ✅ Client component imports server function
- ❌ Inlined server function (inside a server component)
- ✅ Server Functions with Actions
- ✅ Server Functions with useActionState
- ✅ Passing promise from server to client component
- ✅ Inspirational quote generator app
- ✅ Nested Client Components
- ❌ Inlined server action to access variables in the near scope
- ✅ Rendering async component on the server
- ✅ Mixing server and client components
- ✅ Hydrating client component
- ✅ Passing server function to client component
- ✅ Client component imports server function
- ❌ Inlined server function (inside a server component)
- ✅ Server Functions with Actions
- ✅ Server Functions with useActionState
- ✅ Passing promise from server to client component
- ✅ Inspirational quote generator app
- ✅ Nested Client Components
- ❌ Inlined server action to access variables in the near scope
Test Cases
(01) Rendering async component on the server
Basic example of an async server component.
// Page.tsx
import React from 'react';
export default async function Page() {
await new Promise((resolve) => setTimeout(resolve, 1000));
return <div>SSR Async Page</div>;
}
(02) Mixing server and client components
Passing server generated data as a child of a client component.
// Page.tsx
import React from "react";
import db from "./db";
import Expandable from "./Expandable";
export default async function Page() {
const notes = await db.notes.getAll();
return (
<div>
{notes.map((note) => (
<Expandable key={note.id}>
<p>{note.content}</p>
</Expandable>
))}
</div>
);
}
// Expandable.tsx
"use client";
import React from "react";
export default function Expandable({ key, children }: { key: number; children: React.ReactNode }) {
return (
<div>
{children}
</div>
);
}
// db.ts
const db = {
notes: {
getAll: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return [
{ id: 1, content: "Note 1" },
{ id: 2, content: "Note 2" },
{ id: 3, content: "Note 3" }
];
}
}
};
export default db;
(03) Hydrating client component
A client component that is hydrated with children passed from the server component.
// Page.tsx
import React from "react";
import db from "./db";
import Expandable from "./Expandable";
export default async function Page() {
const notes = await db.notes.getAll();
return (
<div>
{notes.map((note) => (
<Expandable key={note.id}>
<p>{note.content}</p>
</Expandable>
))}
</div>
);
}
// Expandable.tsx
"use client";
import React, { useState } from "react";
export default function Expandable({ children }) {
const [expanded, setExpanded] = useState(false);
return (
<div>
<button onClick={() => setExpanded(!expanded)}>Toggle</button>
{expanded && children}
</div>
);
}
// db.ts
const db = {
notes: {
getAll: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return [
{ id: 1, content: "Note 1" },
{ id: 2, content: "Note 2" },
{ id: 3, content: "Note 3" }
];
}
}
};
export default db;
(04) Passing server function to client component
A client component receives a server function as a prop and calls it.
// Page.tsx
import React from "react";
import LikeButton from "./LikeButton";
import { getLikeCount, incrementLike } from "./actions";
export default async function Page() {
return (
<div>
<LikeButton initialCount={await getLikeCount()} incrementLike={incrementLike}/>
</div>
);
}
// LikeButton.tsx
'use client';
import React, { useState, useTransition } from "react";
export default function LikeButton({ initialCount, incrementLike }) {
const [isPending, startTransition] = useTransition();
const [likeCount, setLikeCount] = useState(initialCount);
const onClick = () => {
startTransition(async () => {
const currentCount = await incrementLike();
setLikeCount(currentCount);
});
};
return (
<>
<p>Total Likes: {likeCount}</p>
<button onClick={onClick} disabled={isPending}>
Like
</button>
</>
);
}
// actions.ts
"use server";
let likeCount = 0;
export async function incrementLike() {
likeCount++;
return likeCount;
}
export async function getLikeCount() {
return likeCount;
}
(05) Client component imports server function
A client component imports server function and uses it.
// Page.tsx
import React from "react";
import LikeButton from "./LikeButton";
import { getLikeCount } from "./actions";
export default async function Page() {
return (
<div>
<LikeButton initialCount={await getLikeCount()}/>
</div>
);
}
// LikeButton.tsx
'use client';
import React, { useState, useTransition } from "react";
import { incrementLike } from "./actions";
export default function LikeButton({ initialCount }) {
const [isPending, startTransition] = useTransition();
const [likeCount, setLikeCount] = useState(initialCount);
const onClick = () => {
startTransition(async () => {
const currentCount = await incrementLike();
setLikeCount(currentCount);
});
};
return (
<>
<p>Total Likes: {likeCount}</p>
<button onClick={onClick} disabled={isPending}>
Like
</button>
</>
);
}
// actions.ts
"use server";
let likeCount = 0;
export async function incrementLike() {
likeCount++;
return likeCount;
}
export async function getLikeCount() {
return likeCount;
}
(06) Inlined server function (inside a server component)
Creating a server function and pass it as a prop to client component
// Page.tsx
import React from "react";
import db from './db';
import Button from "./Button";
export default async function Page() {
return (
<EmptyNote />
);
}
function EmptyNote() {
async function createNoteAction() {
"use server";
return await db.notes.create();
}
return <Button onClick={createNoteAction} />;
}
// Button.tsx
"use client";
import React from "react";
export default function Button({ onClick }) {
return (
<button onClick={() => {
onClick().then(data => alert(JSON.stringify(data)));
}}>
Create Empty Note
</button>
);
}
// db.ts
const db = {
notes: {
create: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
ok: true
};
}
}
}
export default db;
(07) Server Functions with Actions
A server function is called inside a client component within a form submit handler to demonstrate server-side processing in response to a form submission.
// Page.tsx
import React from "react";
import UpdateMyName from "./UpdateMyName";
export default async function Page() {
return <UpdateMyName />;
}
// UpdateMyName.tsx
"use client";
import React, { useState, useTransition } from "react";
import { updateMyName } from "./actions";
export default function UpdateMyName() {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();
const submitAction = async () => {
startTransition(async () => {
const response = await updateMyName(name);
alert("Response from updateName:" + JSON.stringify(response));
if (response.error) {
setError(response.error);
} else {
setError(null);
setName("");
}
});
};
return (
<form action={submitAction}>
<input
type="text"
name="name"
disabled={isPending}
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="type your name here"
/>
{error && <p>Failed: {error}</p>}
</form>
);
}
// actions.ts
"use server";
import db from './db';
export async function updateMyName(name) {
if (!name) {
return { error: "Name is required" };
}
return await db.users.updateName(name);
}
// db.ts
const db = {
users: {
updateName: async (name) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return {
ok: true
};
}
}
}
export default db;
(08) Server Functions with useActionState
A server function is used as the action for a form to demonstrate server-side processing in response to a form submission.
// Page.tsx
import React from "react";
import UpdateName from "./UpdateName";
export default async function Page() {
return (
<div>
<UpdateName />
</div>
);
}
// UpdateName.tsx
"use client";
import React, { useState, useActionState } from "react";
import { updateUsername } from "./actions";
export default function UpdateName() {
const [name, setName] = useState("");
const [state, submitAction, isPending] = useActionState(updateUsername, { error: null });
return (
<form action={submitAction}>
{!state.ok ? (
<input
type="text"
name="name"
disabled={isPending}
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="type your name here"
/>
) : <p>Name saved successfully</p>}
<p style={{ marginTop: '2rem' }}>{JSON.stringify(state)}</p>
</form>
);
}
// actions.ts
"use server";
import db from './db';
export async function updateUsername(currentState, formData) {
if (!formData.get("name")) {
return { error: "Name is required" };
}
return await db.users.updateName(formData.get("name"));
}
// db.ts
const db = {
users: {
updateName: async (name) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return {
ok: true
};
}
}
}
export default db;
(09) Passing promise from server to client component
A server component passes a promise to a client component which "awaits" it and renders the result.
// Page.tsx
import React, { Suspense } from "react";
import db from './db';
import Comments from "./Comments"
export default async function Page() {
const note = await db.notes.get(42);
const commentsPromise = db.comments.get(note.id);
return (
<div>
{note.title}
<Suspense fallback={<p>Loading Comments...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}
// Comments.tsx
"use client";
import React, { use } from "react";
export default function Comments({ commentsPromise }) {
const comments = use(commentsPromise);
return comments.map(({ id, content }) => <p key={id}>Comment: {content}</p>);
}
// db.ts
const db = {
notes: {
async get(id) {
await new Promise((r) => setTimeout(r, 1000));
return { id, title: "Some note" };
}
},
comments: {
async get(id) {
await new Promise((r) => setTimeout(r, 1000));
return [
{ id: 1, noteId: id, content: "First comment" },
{ id: 2, noteId: id, content: "Second comment" }
];
}
}
}
export default db;
(10) Inspirational quote generator app
An app that generates and displays inspirational quotes to motivate users. Little app from [here](https://react.dev/reference/rsc/use-client#how-use-client-marks-client-code).
// Page.tsx
import React from "react";
import FancyText from "./FancyText";
import InspirationGenerator from "./InspirationGenerator";
import Copyright from "./Copyright";
export default async function Page() {
return (
<div>
<FancyText title text="Get Inspired App" />
<InspirationGenerator>
<Copyright year={2025} />
</InspirationGenerator>
</div>
);
}
// Copyright.tsx
import React from 'react';
export default function Copyright({ year }) {
return <p className="small">© {year}</p>;
}
// FancyText.tsx
import React from 'react';
export default function FancyText({ title, text }: { title?: boolean; text: string }) {
return title ? <h1 className="fancy title">{text}</h1> : <h3 className="fancy cursive">{text}</h3>;
}
// InspirationGenerator.tsx
"use client";
import React, { useState } from "react";
import inspirations from "./inspirations";
import FancyText from "./FancyText";
export default function InspirationGenerator({ children }) {
const [index, setIndex] = useState(0);
const quote = inspirations[index];
const next = () => setIndex((index + 1) % inspirations.length);
return (
<>
<p>Your inspirational quote is:</p>
<FancyText text={quote} />
<button onClick={next}>Inspire me again</button>
{children}
</>
);
}
// inspirations.ts
export default [
"Don't let yesterday take up too much of today.",
"Ambition is putting a ladder against the sky.",
"A joy that's shared is a joy made double."
];
(11) Nested Client Components
A test case for nested client components
// Page.tsx
import React from 'react';
import Player from './Player';
import Controls from './Controls';
function getTracks() {
return new Promise((resolve) =>
setTimeout(() => resolve(['Track 1', 'Track 2', 'Track 3']), 1000)
);
}
export default async function Page() {
const tracks = await getTracks();
return <Player tracks={tracks}><Controls /></Player>;
}
// Controls.tsx
"use client";
import React, { useState } from "react";
export default function Controls() {
const [status, setStatus] = useState("idle");
return (
<div>
<button onClick={() => setStatus("play")}>Play</button>
<button onClick={() => setStatus("pause")}>Pause</button>
<button onClick={() => setStatus("next")}>Next</button>
<button onClick={() => setStatus("previous")}>Previous</button>
<div>{status}</div>
</div>
);
}
// Player.tsx
"use client";
import React, { useState } from 'react';
export default function Player({ tracks, children }) {
return (
<div>
<h1>Music Player</h1>
<ul>
{tracks.map((track, index) => (
<li key={index}>{track}</li>
))}
</ul>
{children}
</div>
);
}
(12) Inlined server action to access variables in the near scope
When we define an inlined server action inside a component, it should be able to access variables in the near scope. This means that the server function is aware of its surrounding context.
// Page.tsx
import React from 'react';
import Player from './Player';
function getTracks() {
return new Promise((resolve) =>
setTimeout(() => resolve(['Track 1', 'Track 2', 'Track 3']), 1000)
);
}
export default async function Page() {
const tracks = await getTracks();
async function saveSelectedTrack(currentTrack) {
"use server";
console.log(`Selected track: ${currentTrack}/${tracks.length}`);
}
return <Player tracks={tracks} saveSelectedTrack={saveSelectedTrack}/>;
}
// Player.tsx
"use client";
import React, { useState } from 'react';
export default function Player({ tracks, saveSelectedTrack }) {
const [currentTrack, setCurrentTrack] = useState();
return (
<div>
<h1>Music Player {currentTrack ? `(${currentTrack})` : ""}</h1>
{tracks.map((track, index) => (
<button key={index} onClick={() => setCurrentTrack(index + 1)}>
{track}
</button>
))}
<div>
{currentTrack && <button onClick={() => saveSelectedTrack(currentTrack)}>Save current track</button>}
</div>
</div>
);
}