EY-Office ブログ

Next.jsのReact Server Componentsを試してみた

React Server Components(RSC)React v18.0の目玉機能ですが今のところ実験的な機能です。React開発チームからリリースされた派手な機能はありませんが、Next.jsHydrogen等で開発が進められています。

今回Next.js 13.4でリリースされたApp RouterにReact Server Componentsが含まれているので試してみました。

結論から言うと、Reactフロントエンド+Node.jsバックエンドが簡単に構築できる素晴らしい技術です!

Next.jsのReact Server Components Bing Image Creatorが生成した画像です

React Server Componentsとは?

React Server Componentsの日本語の説明としてはReact Server Componentsの仕組み:詳細ガイドが詳しいのでぜひ読んで下さい。

まとめると、

  1. 従来のReactのJavaScriptコードはクライアント(ブラウザー)上で動いていた
    • したがってサーバーにあるデータベースの内容を表示するにはサーバーと通信する必要があり
    • 通信中はユーザーを待たせる、コードも複雑になる
  2. 1.を解決するためにSSR(Server Side Rendering)があります、サーバー上でReactを動かし生成されたHTMLをブラウザーに送ります
    • ただしHTMLではインタラクティブな処理は出来ないのでHTMLに続きReactのJavaScriptコードをブラウザーに送り実行します
  3. それならReactのコンポーネント単位でサーバーで実行するもの、クライアント(ブラウザー)で実行するものを組み合わせて実行できるようにする事で、サーバー・サイドとクラインアント・サイドのいいとこ取りをしようとするアプローチです

Next.jsのReact Server Components

Next.js 13.4でリリースされたApp RouterにはReact Server Componentsが含まれていいます。

通常の手順でNext.jsプロジェクトを作り、設定ファイルに以下を追加すると、今回の目玉Server Actionsが使えるようになります(後で説明します)。

  • next.config.js
module.exports = {
  experimental: {
    serverActions: true,
  }
}

まずは、以下のコードを見てください。データベースに入っているジャンケンの結果をテーブルで表示するReact(Next.js)のコードです。

  • ① このコンポーネントはサーバーで動かす事を指定しています
    • このコンポーネントはトップレベルなのでデフォルトでサーバー動作で。したがって、この指定は無くてもよいです
  • ② データベースのアクセスはPrisma ORMを使っています
    • このコードはサーバーで動くのでデータベースが直接アクセスできます
    • Prismaの設定ファイル、モデル等は省略します
  • ③ PrismaのfindManyメソッドを使って、データを取得しています
    • orderByでソート順を指定しています。ここではidが大きい(後に挿入したデータ)順に取得できます
  • ④ 以下は普通のReactコードです
"use server";                                   // ← ①
import { PrismaClient } from '@prisma/client';  // ← ②

const prisma = new PrismaClient();              // ← ②

export const Home = async () => {
  const scores = await prisma.score.findMany({orderBy: {id: 'desc'}});  // ← ③
                                                                        // ↓ ④
  const teString = ["グー","チョキ", "パー"];
  const judgmentString = ["引き分け","勝ち", "負け"];

  return (
    <table>
      <thead>
        <tr>
          <th>あなた</th>
          <th>コンピュター</th>
          <th>勝敗</th>
        </tr>
      </thead>
      <tbody>
        {scores.map((scrore, ix) =>
          <tr key={ix}>
            <td>{teString[scrore.human]}</td>
            <td>{teString[scrore.computer]}</td>
            <td>{judgmentString[scrore.judgment]}</td>
          </tr>
        )}
      </tbody>
    </table>
  );
}

いつものジャンケンアプリをReact Server Componentsで実装してみた

このブログに度々登場するジャンケンアプリをNext.jsのReact Server Componentsで実装してみた。 オリジナルのReactのコードはここです。

結果表示のコンポーネント

ジャンケンの結果表示コンポーネントScoreBox.tsxですが、まったく変更はありません。😃

export enum Jjudgment { Draw = 0, Win, Lose };
export type ScoreType = {
  human: number;
  computer: number;
  judgment: Jjudgment;
};

type ScoreListProps = {
  scores: ScoreType[];
}
const ScoreBox: React.FC<ScoreListProps> = ({scores}) => {
  console.log('- ScoreBox');
  const teString = ["グー","チョキ", "パー"];
  const judgmentString = ["引き分け","勝ち", "負け"];

  const tableStyle: React.CSSProperties = {marginTop: 20, borderCollapse: "collapse"};
  const thStyle: React.CSSProperties = {border: "solid 1px #888", padding: "3px 15px"};
  const tdStyle: React.CSSProperties = {border: "solid 1px #888", padding: "3px 15px",
      textAlign: "center"};

  return (
    <table style={tableStyle}>
      <thead>
        <tr>
          <th style={thStyle}>あなた</th>
          <th style={thStyle}>コンピュター</th>
          <th style={thStyle}>勝敗</th>
        </tr>
      </thead>
      <tbody>
        {scores.map((scrore, ix) =>
          <tr key={ix}>
            <td style={tdStyle}>{teString[scrore.human]}</td>
            <td style={tdStyle}>{teString[scrore.computer]}</td>
            <td style={tdStyle}>{judgmentString[scrore.judgment]}</td>
          </tr>
        )}
      </tbody>
    </table>
  );
}

export default ScoreBox;

ジャンケンボタンのコンポーネント

ジャンケンボタンのコンポーネントは、ボタンを押したイベントを処理する必要がありクライアントで動かす必要があります。

"use client;" でクライアントで動かす指定をしています。 変更はこれだけです。😃

"use client";          // ← ①
export enum Te { Guu = 0, Choki, Paa};

type JyankenBoxProps = {
  actionPon: (te: number) => void;
}

const JyankenBox: React.FC<JyankenBoxProps> = ({actionPon}) => {
  console.log('- JyankenBox');
  const divStyle: React.CSSProperties = {margin: "0 20px"};
  const buttonStyle: React.CSSProperties = {margin: "0 10px",
     padding: "3px 10px", fontSize: 14};

  return (
    <div style={divStyle}>
      <button onClick={() => actionPon(Te.Guu)} style={buttonStyle}>グー</button>
      <button onClick={() => actionPon(Te.Choki)} style={buttonStyle}>チョキ</button>
      <button onClick={() => actionPon(Te.Paa)} style={buttonStyle}>パー</button>
    </div>
  );
}

export default JyankenBox;

ジャンケン本体のコポーネント

ジャンケン本体のコンポーネントはサーバーで動かします。
ただしジャンケンのボタンを押した時の処理は、クライアントからサーバー側にあるジャンケンの判定・データベース書き込み処理を呼び出し必要があります。これを④のServer Actionsを使って実行します。

ジャンケンの結果取得は、Next.jsのReact Server Componentsと同じです。

  • ① このコンポーネントはサーバーで動かす事を指定しています
  • ② データベースのアクセスはPrisma ORMを使っています、設定ファイル等は省略します
  • ③ Prismaの全データ取得findManyメソッドを使って、データを取得しています
"use server";                                    // ← ①
import { PrismaClient } from '@prisma/client';   // ← ②
import ScoreBox, { ScoreType, Jjudgment } from './ScoreBox';
import JyankenBox, { Te } from './JyankenBox';
import { revalidatePath } from 'next/cache';

const prisma = new PrismaClient();               // ← ②

const Jyanken = async () => {
  console.log('- Jyanken');
  const scores = await prisma.score.findMany({orderBy: {id: 'desc'}}); // ← ③

  const pon = async (human: Te) => {                                   // ← ④
    "use server";                                                      // ← ④
    const computer: Te = Math.floor(Math.random() * 3);
    const judgment: Jjudgment = (computer - human + 3) % 3;
    const score: ScoreType = {human, computer, judgment};
    await prisma.score.create({ data: score });                        // ← ⑤
    revalidatePath('/');                                               // ← ⑥
  };

  return (
    <>
      <h1>じゃんけん ポン!</h1>
      <JyankenBox actionPon={pon} />
      <ScoreBox scores={scores} />
    </>
  );
}

export default Jyanken;
  • ④ pon関数はクライアントからこの関数が呼び出されると、サーバー側にあるpon関数が動きます
    • 関数の先頭に"use server";があるのでServer Actionsと認識されます
    • pon関数の呼び出しは実行時にはクライアントからサーバーへの通信になります
  • ⑤ PrismaのCreateメソッドでジャンケンの結果データをデータベースに書き込みます
  • revalidatePathはクライアント側にキャッシュされたデータを消し、引数で指定されたパスに対応するサーバーコンポーネントを実行した結果をクライアントに戻します
    • ようするに、このJyankenコンポーネントが再実行され、表示が最新になります。
    • この際にクライアントに戻る結果はHTMLではなくRSCワイヤーフォーマットです

まとめ

今回のNext.jsのReact Server Componentsのコードはどうだったでしょうか? クライアントのReactコードと、サーバー側のデータベース・アクセス等のコードのみが書かれていて、通信やステート管理のコードはありません!

もし、Node.jsが動くサーバーが用意出来ればReact Server Componentsを使う事で、短時間でReactフロントエンドと対応するNode.jsバックエンドを使ったサービスが構築できます、面倒なREST APIやGrpahQLを実行するバックエンドの開発は不要です。

PHPでお手軽にWebサービスを作れるように、React Server Componentsを使うと今時なUI/UXのReactを使ったフロントエンドを持つWebサービスを手軽に作れます。ネット上ではReact Server Components (RSC) are just PHPなどと呟かれています。

また、これは以前書いたRemixと同じアプローチですね。やはりネット上ではWhy Do I Need RSC(react server components) if I Already Have Remixなどと書かれています。

今後、React Server ComponentsやRemixがどのように発展して行くのか/行かないのかは判りませんが、お手軽にReactフロントエンド+Node.jsバックエンドを試せる時代になったということは確かです。😃

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ