Streaming Rendering with React Server Components & Suspense App Router in Next.js and Calling APIs Like RPC with Server Actions

reacttypescriptwebnext.js

Now the app/ directory-based App Router is the default, replacing the previous pages/ directory-based routing.

Next.jsのpre-rendering - sambaiz-net

$ npx create-next-app@latest nextjs-vercel-test
✔ Would you like to use the recommended Next.js defaults? › No, customize settings
✔ Would you like to use TypeScript? … No / Yes
✔ Which linter would you like to use? › ESLint
✔ Would you like to use React Compiler? … No / Yes
? Would you like to use Tailwind CSS? › No / Yes
godgo@sam-thinkpad:~/nextjs-test$ npx create-next-app@latest nextjs-vercel-test
✔ Would you like to use the recommended Next.js defaults? › No, customize settings
✔ Would you like to use TypeScript? … No / Yes
✔ Which linter would you like to use? › ESLint
✔ Would you like to use React Compiler? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack? (recommended) … No / Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes
Creating a new Next.js app in /home/godgo/nextjs-test/nextjs-vercel-test.

Using npm.

Initializing project with template: app-tw
...

$ tree -I node_modules
.
├── README.md
├── app
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── eslint.config.mjs
├── next-env.d.ts
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
│   ├── file.svg
│   ├── globe.svg
│   ├── next.svg
│   ├── vercel.svg
│   └── window.svg
└── tsconfig.json

3 directories, 17 files

App Router allows you to write components that execute only on the server side using React Server Components (RSC), reducing the client-side JavaScript bundle size. It also supports streaming rendering with Suspense, allowing parts of the page to be displayed first. Functions like getServerSideProps() are no longer needed. RSC is the default, and writing 'use client'; makes it a regular component that runs on the client. Addition to App Router, Server Actions (Functions) are also convenient for calling server APIs like RPC. In this example, zod is used for input validation.

Streaming rendering with Suspense and Server Action
// app/demo/page.tsx
import { Suspense } from 'react';
import { Counter } from './counter';

async function SlowComponent() {
  await new Promise(resolve => setTimeout(resolve, 3000));
  return (
    <div className="p-4 bg-green-100 dark:bg-green-900 rounded-lg">
      <h2 className="text-xl font-bold mb-2">Slow Server Component</h2>
      <p>Loaded after 3 seconds</p>
    </div>
  );
}

async function MediumComponent() {
  await new Promise(resolve => setTimeout(resolve, 1500));
  return (
    <div className="p-4 bg-blue-100 dark:bg-blue-900 rounded-lg">
      <h2 className="text-xl font-bold mb-2">Medium Server Component</h2>
      <p>Loaded after 1.5 seconds</p>
    </div>
  );
}

async function FastComponent() {
  await new Promise(resolve => setTimeout(resolve, 500));
  return (
    <div className="p-4 bg-purple-100 dark:bg-purple-900 rounded-lg">
      <h2 className="text-xl font-bold mb-2">Fast Server Component</h2>
      <p>Loaded after 0.5 seconds</p>
    </div>
  );
}

function LoadingSkeleton({ label }: { label: string }) {
  return (
    <div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg animate-pulse">
      <div className="h-6 bg-gray-300 dark:bg-gray-600 rounded w-32 mb-2"></div>
      <div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-48"></div>
      <p className="text-sm text-gray-500 mt-2">Loading...</p>
    </div>
  );
}

// Main page - Server Component
export default function StreamingDemo() {
  return (
    <div className="min-h-screen p-8 bg-white dark:bg-black">
      <div className="max-w-4xl mx-auto">
        <div className="space-y-4">
          <div className="p-4 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
            <h2 className="text-xl font-bold mb-2">Instant Server Component</h2>
            <p>Renders immediately</p>
          </div>

          <Counter />

          <Suspense fallback={<LoadingSkeleton label="Fast Component" />}>
            <FastComponent />
          </Suspense>

          <Suspense fallback={<LoadingSkeleton label="Medium Component" />}>
            <MediumComponent />
          </Suspense>

          <Suspense fallback={<LoadingSkeleton label="Slow Component" />}>
            <SlowComponent />
          </Suspense>
        </div>
      </div>
    </div>
  );
}

// app/demo/counter.tsx
'use client';

import { useState, useTransition } from 'react';
import { saveCount } from './actions';

export function Counter() {
  const [count, setCount] = useState(0);
  const [result, setResult] = useState('');
  const [error, setError] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleSave() {
    setError('');
    setResult('');

    startTransition(async () => {
      const response = await saveCount(count);

      if (response.success) {
        setResult(`Saved ${response.count} at ${response.timestamp}`);
      } else {
        setError(response.error || 'Unknown error');
      }
    });
  }

  return (
    <div className="p-4 bg-pink-100 dark:bg-pink-900 rounded-lg">
      <h2 className="text-xl font-bold mb-2">Client Component + Server Action + Zod</h2>
      <div className="flex items-center gap-4">
        <button
          onClick={() => setCount(count - 1)}
          className="px-4 py-2 bg-pink-500 text-white rounded hover:bg-pink-600"
        >
          -
        </button>
        <span className="text-2xl font-bold">{count}</span>
        <button
          onClick={() => setCount(count + 1)}
          className="px-4 py-2 bg-pink-500 text-white rounded hover:bg-pink-600"
        >
          +
        </button>
        <button
          onClick={handleSave}
          disabled={isPending}
          className="px-4 py-2 bg-pink-700 text-white rounded hover:bg-pink-800 disabled:opacity-50"
        >
          {isPending ? 'Saving...' : 'Save'}
        </button>
      </div>
      {result && <p className="mt-3 text-sm text-green-800 dark:text-green-200">{result}</p>}
      {error && <p className="mt-3 text-sm text-red-900 dark:text-red-200 font-semibold">{error}</p>}
    </div>
  );
}

// app/demo/actions.ts
'use server';

import { z } from 'zod';

const countSchema = z.number().refine((val) => val % 2 === 0, {
  message: 'Count must be an even number',
});

export async function saveCount(count: number) {
  await new Promise(resolve => setTimeout(resolve, 500));

  const result = countSchema.safeParse(count);

  if (!result.success) {
    return { success: false, error: result.error.issues[0].message };
  }

  return { success: true, count, timestamp: new Date().toLocaleTimeString() };
}

References

React Server Componentsを理解したい