Upgrade to Pro — share decks privately, control downloads, hide ads and more …

React Server ComponentsでAPI不要の開発体験

Avatar for polidog polidog PRO
August 10, 2025

React Server ComponentsでAPI不要の開発体験

このスライドは、React Server Components(RSC)を活用したAPI不要の開発体験について解説するプレゼンテーションです。Server ComponentsとClient Componentsの使い分け、Server
Actionsによるフォーム処理、Zodによる型安全なバリデーションなど、実践的なコード例を交えて、フルスタックエンジニアへの時代の変化を提示しています。

Avatar for polidog

polidog PRO

August 10, 2025
Tweet

More Decks by polidog

Other Decks in Technology

Transcript

  1. 4

  2. SPA 開発あるある これ、全部経験ありませんか? 「このデータ、GET /users/:id/posts ?POST /posts ?」API 設計で1 日会

    議 「token 、refreshToken 、どこに保存する?localStorage ?cookie ?」 「バックエンドでcreated_at 、フロントでcreatedAt... 」型のズレに悩む 「ローディング中... 」が3 秒も表示される 「401 エラーでリトライして、トークン更新して... 」実装が複雑すぎる 「フロントとバック、別々にデプロイしなきゃ... 」面倒くさい 6
  3. PHP の時代(2000 年代) <?php // index.php - これだけで動く! $posts =

    $db->query("SELECT * FROM posts"); ?> <html> <body> <?php foreach($posts as $post): ?> <article> <h2><?= $post['title'] ?></h2> <p><?= $post['content'] ?></p> </article> <?php endforeach; ?> </body> </html> シンプル!早い!分かりやすい! 8
  4. そしてSPA 時代へ(2010 年代後半) フロントエンド(React ) import { useState, useEffect }

    from 'react'; export default function PostList() { const [posts, setPosts] = useState<Post[]>([]); const [loading, setLoading] = useState(true); useEffect(() => { fetch('/api/posts') .then(res => res.json()) .then(data => { setPosts(data); setLoading(false); }); }, []); return ( <div> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.content}</p> </article> ))} </div> ); } 9
  5. そしてSPA 時代へ(2010 年代後半) バックエンド(Symfony ) <?php // src/Controller/ApiController.php namespace App\Controller;

    use App\Entity\Post; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; class ApiController extends AbstractController { #[Route('/api/posts', methods: ['GET'])] #[IsGranted('ROLE_USER')] public function getPosts(EntityManagerInterface $em): JsonResponse { $posts = $em->getRepository(Post::class)->findAll(); return $this->json($posts); } } 10
  6. そこで登場!React Server Components // app/posts/page.tsx - これだけ! export default async

    function PostsPage() { const posts = await db.query('SELECT * FROM posts'); return ( <div> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.content}</p> </article> ))} </div> ); } 13
  7. できないこと useState, useEffect などのHooks onClick, onChange などのイベントハンドラ ブラウザAPI (localStorage, sessionStorage

    など) クライアント側ライブラリ インタラクティブな機能はどうするの? 17
  8. そこで Client Component ! 'use client'; // この一行でClient Componentに! import

    { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>カウント: {count}</p> <button onClick={() => setCount(count + 1)}> +1 </button> </div> ); } 18
  9. Client Component とは? 従来のReact コンポーネント RSC と区別するために「Client Component 」と呼ぶように ブラウザで実行される

    Hooks (useState, useEffect など)やイベントハンドラが使える JavaScript バンドルに含まれる 19
  10. 実際の使い方 Server Component + Client Component の組み合わせ // app/posts/page.tsx (Server

    Component) import { db } from '@/lib/db'; import LikeButton from './LikeButton'; export default async function PostsPage() { const posts = await db.query('SELECT * FROM posts'); return posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> {/* サーバーで生成 */} <p>{post.content}</p> {/* サーバーで生成 */} <LikeButton postId={post.id} /> {/* クライアントで動作 */} </article> )); } 20
  11. 実際の使い方 Server Component + Client Component の組み合わせ // app/posts/LikeButton.tsx 'use

    client'; // これでClient Componentになる import { useState } from 'react'; export default function LikeButton({ postId }: { postId: number }) { const [likes, setLikes] = useState(0); return ( <button onClick={() => setLikes(likes + 1)}> いいね! {likes} </button> ); } 21
  12. SSR とは何が違うの? 従来のSSR (Server-Side Rendering ) ページ全体をサーバーでレンダリング すべてのJavaScript がクライアントに送信される Hydration

    (サーバーHTML にイベントを付与)が必要 データ取得後もコンポーネントのコードがバンドルに含まれる React Server Components コンポーネント単位でサーバー実行を選択 Server Component のJS はクライアントに送られない 必要な部分だけをClient Component に バンドルサイズを大幅削減 23
  13. なぜこれが画期的なのか? 従来のIsomorphism 全コードが両環境で実行 → バンドルサイズ肥大化 機密情報の扱いが困難 → 環境変数の複雑な管理 サーバー/ クライアントの区別が曖昧

    → 実行環境の判定が必要 RSC のIsomorphism 適材適所で実行 → 最小限のJavaScript のみクライアントへ セキュリティ向上 → API キーやDB アクセスをサーバー側に隔離 開発者体験の向上 → 明確な責任分離 パラダイムシフト: 「同じコードを両方で」から「統一モデルで適切な場所で」へ 25
  14. Server Actions - フォーム処理も簡単! Server Actions の実装 // app/posts/actions.ts 'use

    server'; import { redirect } from 'next/navigation'; export async function createPost(formData: FormData) { const title = formData.get('title'); const content = formData.get('content'); await db.query('INSERT INTO posts (title, content) VALUES (?, ?)', [title, content]); redirect('/posts'); // リダイレクトも簡単 } 31
  15. Server Actions - フォーム処理も簡単! Form の実装 // app/posts/new/page.tsx import {

    createPost } from './actions'; export default function NewPost() { return ( <form action={createPost}> <input name="title" placeholder="タイトル" /> <textarea name="content" placeholder="内容" /> <button type="submit">投稿</button> </form> ); } 32
  16. Server Actions + useActionState + Zod Server Actions の実装 //

    actions.ts - Server Actions用ファイル 'use server'; import { z } from 'zod'; // Zodスキーマで型安全なバリデーション const schema = z.object({ name: z.string().min(3, 'ユーザー名は3文字以上必要です'), email: z.string().email('有効なメールアドレスを入力してください') }) export async function createUser(prevState: any, formData: FormData) { const result = schema.safeParse({ name: formData.get('name'), email: formData.get('email') }); if (!result.success) { return { errors: result.error.flatten().fieldErrors }; } // バリデーション通過後の処理 await db.user.create({ data: result.data }); return { success: true }; } 33
  17. Server Actions + useActionState + Zod フォーム側の実装 // UserForm.tsx -

    Client Component 'use client'; import { useActionState } from 'react'; import { createUser } from './actions'; export default function UserForm() { const [state, formAction] = useActionState(createUser, null); return ( <form action={formAction}> <input name="name" placeholder="ユーザー名" /> {state?.errors?.name && <p>{state.errors.name[0]}</p>} <input name="email" placeholder="メールアドレス" /> {state?.errors?.email && <p>{state.errors.email[0]}</p>} <button type="submit">登録</button> {state?.success && <p>登録完了!</p>} </form> ); } 34
  18. まとめ React Server Components + Server Actions = API 不要開発

    開発が劇的に速くなる API エンドポイントの設計・実装が不要 フロントとバックの型の二重管理から解放 データベースから画面まで一気通貫で実装 開発者同士の不毛なコミュニケーションも減らせる エンジニアに求められるスキルの変化 フロントエンドエンジニア → DB やサーバー側の知識が必要に バックエンドエンジニア → React/TypeScript の習得が必須に フルスタックWeb アプリケーションエンジニア が当たり前の時代へ 35
  19. 型安全なデータベースアクセス Prisma と組み合わせると... DB スキーマもTypeScript の型として扱える! // DBスキーマが変更されたら... const posts

    = await prisma.post.findMany(); posts[0].title; // 型安全 posts[0].tytle; // コンパイル時にエラー! DB スキーマ変更時も エディタレベルで即座にエラーを検知 SQL のタイポによる実行時エラーから解放 スキーマ変更の影響をコード全体で自動チェック IDE の補完機能でDB カラム名も間違えない 37
  20. 実際に使ってみよう! 1. Next.js プロジェクトの作成 npx create-next-app@latest my-app --app cd my-app

    プロンプトで聞かれる選択: TypeScript? → Yes ESLint? → Yes Tailwind CSS? → お好みで App Router? → Yes (必須) 38
  21. 2. Server Component でデータ取得 // app/posts/page.tsx export default async function

    PostsPage() { // 直接データベースにアクセス! const response = await fetch('https://jsonplaceholder.typicode.com/posts'); const posts = await response.json(); return ( <div> <h1>記事一覧</h1> {posts.map((post: any) => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.body}</p> </article> ))} </div> ); } ポイント:async/await で直接データ取得! 39
  22. 3. Client Component でインタラクション追加 // app/posts/LikeButton.tsx 'use client'; // この一行でClient

    Componentに! import { useState } from 'react'; export default function LikeButton() { const [liked, setLiked] = useState(false); return ( <button onClick={() => setLiked(!liked)} style={{ color: liked ? 'red' : 'gray' }} > {liked ? ' ' : '♡'} いいね </button> ); } 40
  23. 4. 組み合わせて使う // app/posts/page.tsx import LikeButton from './LikeButton'; export default

    async function PostsPage() { const response = await fetch('https://jsonplaceholder.typicode.com/posts'); const posts = await response.json(); return ( <div> <h1>記事一覧</h1> {posts.slice(0, 5).map((post: any) => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.body}</p> <LikeButton /> {/* Client Componentを配置 */} </article> ))} </div> ); } 41
  24. 5. Server Actions でフォーム処理 // app/posts/new/page.tsx async function createPost(formData: FormData)

    { 'use server'; // Server Actionの宣言 const title = formData.get('title'); const body = formData.get('body'); // データベースに保存(実際の処理) console.log('投稿を作成:', { title, body }); // リダイレクト等の処理 } export default function NewPostPage() { return ( <form action={createPost}> <input name="title" placeholder="タイトル" required /> <textarea name="body" placeholder="本文" required /> <button type="submit">投稿する</button> </form> ); } 42
  25. 6. Zod でバリデーション追加 npm install zod // app/posts/new/page.tsx import {

    z } from 'zod'; const PostSchema = z.object({ title: z.string().min(1, 'タイトルは必須です'), body: z.string().min(10, '本文は10文字以上必要です') }); async function createPost(formData: FormData) { 'use server'; const result = PostSchema.safeParse({ title: formData.get('title'), body: formData.get('body') }); if (!result.success) { return { errors: result.error.flatten().fieldErrors }; } // 成功時の処理 return { success: true }; 43
  26. よくある質問 Q: 既存のSPA プロジェクトはどうする? A: 段階的に移行可能!新機能からRSC で実装 Q: API が本当に不要?

    A: 外部連携やモバイルアプリ用には必要。でもフロントエンドとバックエンド 間では不要! Q: 学習コストは? A: SPA を知っていれば1 週間で基本をマスター Q: デメリットは? A: Node.js サーバーが必要(Vercel などで解決) 44