React Logo

Can I use React Server Components (RSCs) today?

Frameworks (libraries): 7 | Test cases: 12

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">&copy; {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>
  );
}