Slide 1

Slide 1 text

React Server Components で API 不要の開発体験 @polidog Shizuoka Tech #1 1

Slide 2

Slide 2 text

自己紹介 @polidog パーティーハード株式会社という開発 会社を経営しています。 清水市出身、神奈川県在住 4 歳と0 歳の男の子のパパ Symfony(PHP) が好き 2

Slide 3

Slide 3 text

なぜReact Server Components の話をするのか? 3

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

実体験から Printgraph というサービスを開発 React Server Components 中心で実装 開発体験が予想以上に良かった API を書かない開発に感動 これは広めなければ! 5

Slide 6

Slide 6 text

SPA 開発あるある これ、全部経験ありませんか? 「このデータ、GET /users/:id/posts ?POST /posts ?」API 設計で1 日会 議 「token 、refreshToken 、どこに保存する?localStorage ?cookie ?」 「バックエンドでcreated_at 、フロントでcreatedAt... 」型のズレに悩む 「ローディング中... 」が3 秒も表示される 「401 エラーでリトライして、トークン更新して... 」実装が複雑すぎる 「フロントとバック、別々にデプロイしなきゃ... 」面倒くさい 6

Slide 7

Slide 7 text

昔はもっとシンプルだった 7

Slide 8

Slide 8 text

PHP の時代(2000 年代) query("SELECT * FROM posts"); ?>

シンプル!早い!分かりやすい! 8

Slide 9

Slide 9 text

そしてSPA 時代へ(2010 年代後半) フロントエンド(React ) import { useState, useEffect } from 'react'; export default function PostList() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { fetch('/api/posts') .then(res => res.json()) .then(data => { setPosts(data); setLoading(false); }); }, []); return (
{posts.map(post => (

{post.title}

{post.content}

))}
); } 9

Slide 10

Slide 10 text

そしてSPA 時代へ(2010 年代後半) バックエンド(Symfony ) getRepository(Post::class)->findAll(); return $this->json($posts); } } 10

Slide 11

Slide 11 text

フロントエンドとバックエンドが分離された結果、開発 が複雑に... 11

Slide 12

Slide 12 text

開発者の悲鳴 「API エンドポイント作るのめんどくさい」 「型の二重管理つらい」 「フロントとバックで仕様認識がズレる」 「OpenAPI 定義と実装が違ってる... 」 開発者同士のコミュニケーションコストも馬鹿にならない 12

Slide 13

Slide 13 text

そこで登場!React Server Components // app/posts/page.tsx - これだけ! export default async function PostsPage() { const posts = await db.query('SELECT * FROM posts'); return (
{posts.map(post => (

{post.title}

{post.content}

))}
); } 13

Slide 14

Slide 14 text

サーバー上で実行されるReact コンポーネント サーバー側でレンダリングされ、HTML として配信 データベース直接アクセスが可能 JavaScript バンドルに含まれない(バンドルサイズ削減) 機密情報(API キー等)を安全に使用可能 14

Slide 15

Slide 15 text

でも、Server Component には制約がある 15

Slide 16

Slide 16 text

できること async/await でデータ取得 データベース直接アクセス 環境変数やAPI キーを安全に使用 サーバー側ライブラリを利用 16

Slide 17

Slide 17 text

できないこと useState, useEffect などのHooks onClick, onChange などのイベントハンドラ ブラウザAPI (localStorage, sessionStorage など) クライアント側ライブラリ インタラクティブな機能はどうするの? 17

Slide 18

Slide 18 text

そこで Client Component ! 'use client'; // この一行でClient Componentに! import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return (

カウント: {count}

setCount(count + 1)}> +1
); } 18

Slide 19

Slide 19 text

Client Component とは? 従来のReact コンポーネント RSC と区別するために「Client Component 」と呼ぶように ブラウザで実行される Hooks (useState, useEffect など)やイベントハンドラが使える JavaScript バンドルに含まれる 19

Slide 20

Slide 20 text

実際の使い方 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 => (

{post.title}

{/* サーバーで生成 */}

{post.content}

{/* サーバーで生成 */} {/* クライアントで動作 */} )); } 20

Slide 21

Slide 21 text

実際の使い方 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 ( setLikes(likes + 1)}> いいね! {likes} ); } 21

Slide 22

Slide 22 text

役割分担 Server Component: データ取得、HTML 生成 Client Component: インタラクション、状態管理 22

Slide 23

Slide 23 text

SSR とは何が違うの? 従来のSSR (Server-Side Rendering ) ページ全体をサーバーでレンダリング すべてのJavaScript がクライアントに送信される Hydration (サーバーHTML にイベントを付与)が必要 データ取得後もコンポーネントのコードがバンドルに含まれる React Server Components コンポーネント単位でサーバー実行を選択 Server Component のJS はクライアントに送られない 必要な部分だけをClient Component に バンドルサイズを大幅削減 23

Slide 24

Slide 24 text

SSR は「どこで」レンダリングするか、RSC は「何を」レン ダリングするか 24

Slide 25

Slide 25 text

なぜこれが画期的なのか? 従来のIsomorphism 全コードが両環境で実行 → バンドルサイズ肥大化 機密情報の扱いが困難 → 環境変数の複雑な管理 サーバー/ クライアントの区別が曖昧 → 実行環境の判定が必要 RSC のIsomorphism 適材適所で実行 → 最小限のJavaScript のみクライアントへ セキュリティ向上 → API キーやDB アクセスをサーバー側に隔離 開発者体験の向上 → 明確な責任分離 パラダイムシフト: 「同じコードを両方で」から「統一モデルで適切な場所で」へ 25

Slide 26

Slide 26 text

Server Component とClient Component でのデータの受け 渡しの制約 26

Slide 27

Slide 27 text

データの受け渡しの制約 Server → Client Component に渡せないもの 関数(メソッドを含む) Date オブジェクト undefined 値 Symbol クラスのインスタンス 27

Slide 28

Slide 28 text

データ受け渡しの制約 渡せるもの プリミティブ値(string, number, boolean, null ) プレーンなオブジェクト(JSON シリアライズ可能) 配列 理由:props はJSON.stringify() でシリアライズされるため 28

Slide 29

Slide 29 text

Server Actions 29

Slide 30

Slide 30 text

Server Actions とは? サーバー側で実行される関数 'use server' ディレクティブで宣言 フォームのaction 属性に直接指定可能 データベースアクセスやAPI キーが安全に使える API エンドポイントが不要 フォーム処理がこれまでになくシンプルに! 30

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Server Actions - フォーム処理も簡単! Form の実装 // app/posts/new/page.tsx import { createPost } from './actions'; export default function NewPost() { return ( <button type="submit">投稿</button> </form> ); } 32

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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 ( {state?.errors?.name &&

{state.errors.name[0]}

} {state?.errors?.email &&

{state.errors.email[0]}

} 登録 {state?.success &&

登録完了!

} ); } 34

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

おまけ 36

Slide 37

Slide 37 text

型安全なデータベースアクセス Prisma と組み合わせると... DB スキーマもTypeScript の型として扱える! // DBスキーマが変更されたら... const posts = await prisma.post.findMany(); posts[0].title; // 型安全 posts[0].tytle; // コンパイル時にエラー! DB スキーマ変更時も エディタレベルで即座にエラーを検知 SQL のタイポによる実行時エラーから解放 スキーマ変更の影響をコード全体で自動チェック IDE の補完機能でDB カラム名も間違えない 37

Slide 38

Slide 38 text

実際に使ってみよう! 1. Next.js プロジェクトの作成 npx create-next-app@latest my-app --app cd my-app プロンプトで聞かれる選択: TypeScript? → Yes ESLint? → Yes Tailwind CSS? → お好みで App Router? → Yes (必須) 38

Slide 39

Slide 39 text

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 (

記事一覧

{posts.map((post: any) => (

{post.title}

{post.body}

))}
); } ポイント:async/await で直接データ取得! 39

Slide 40

Slide 40 text

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 ( setLiked(!liked)} style={{ color: liked ? 'red' : 'gray' }} > {liked ? ' ' : '♡'} いいね ); } 40

Slide 41

Slide 41 text

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 (

記事一覧

{posts.slice(0, 5).map((post: any) => (

{post.title}

{post.body}

{/* Client Componentを配置 */} ))}
); } 41

Slide 42

Slide 42 text

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 ( <button type="submit">投稿する</button> </form> ); } 42

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

よくある質問 Q: 既存のSPA プロジェクトはどうする? A: 段階的に移行可能!新機能からRSC で実装 Q: API が本当に不要? A: 外部連携やモバイルアプリ用には必要。でもフロントエンドとバックエンド 間では不要! Q: 学習コストは? A: SPA を知っていれば1 週間で基本をマスター Q: デメリットは? A: Node.js サーバーが必要(Vercel などで解決) 44

Slide 45

Slide 45 text

参考リンク React Server Components RFC Next.js App Router Documentation 45