iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

Next.js/ReactServerComponentに入門してみる

こんにちは!木村です!

普段Reactを使用して開発をしているのですが、データの通信の扱いがなかなか難しいなと感じるこの頃です。そんな中で、なんだかちらちらと話に聞くReactServerComponentってどんな感じなんだろう。。。となったので、実際に触ってみました!今回はその際に学んだこと等をまとめました!よろしくお願いいたします!

CSR(クライアントサイドレンダリング

通常Reactでは、CSRの方法でレンダリングされます。一応復習しました。

大体の流れで言えば、

  1. サーバからHTMLの取得
  2. JavaScriptの実行→コンポーネントを実行。
  3. 仮想DOMを作成
  4. DOM(ブラウザ表示)を構築

このような感じで実行され、4のDOMを構築→表示するタイミングでようやくユーザーが確認できる形で表示されます。

Reactはまずコンポーネントツリー全体をレンダリングしてUIの骨格を決定します。その後、useEffectフックなどを利用して、クライアントサイドでデータ通信などの非同期処理が実行されます。

SSR(サーバーサイドレンダリング

ちょっと自分の知識が怪しかったのでこちらも復習です。

Webサーバ側でWEBページを生成して送信する方法です。CSRと比較して、クライアント側でHTMLを組み立てるのではなく、サーバー側でHTMLを組み立てます。

APIリクエスト等の処理を実行した後にHTMLを送信するため、データ表示のラグをなくすことができます。

ReactServerComponent

サーバコンポーネントは新しいタイプのコンポーネントです。クライアントアプリケーションや SSR サーバとは別の環境で、バンドル前に事前にレンダーされます。 https://ja.react.dev/reference/rsc/server-components

通常のReactでは、クライアント側でレンダーされていました。そうではなく、レンダリングされたものを呼び出すことができる、ということです。

ReactServerComponetはNext.jsのようなReactのフレームワークを使用して実装します。

なんとなく想像はできるんですが、一度作ってみた方がわかりやすいかと思うのでさっそく作ってみます。

つかってみる

プロジェクト作成

前提:Node.jsとnpmのインストール

npx create-next-app@latest

実行するとプロジェクトが作成されます。

初期設定だとsrc/appフォルダが作成されますので、こちらに実装していく形になります。

Next.js

Next.jsは、ReactをベースにしたフルスタックのWebアプリケーションを構築するためのフレームワークです。

React単体では主にクライアントサイド(ブラウザ)で動作するUIライブラリですが、Next.jsはそれに加えて、Server Componentsの実行環境、サーバーサイドレンダリングSSR)、静的サイト生成(SSG)、ファイルベースのルーティング、API開発など、現代的なWebアプリに必要な機能を網羅的に提供しています。

ファイルルーティング

フォルダ構造でルーティングを作成できます。

npx create-next-app@latest でプロジェクト作成をした際、srcフォルダ配下にapp フォルダが作成されるので、その内部でページごとにフォルダを作成します。

そのフォルダ名がパスに、そのフォルダ内に作成したPage.tsx がそのパスのルートコンポーネントになります。

例えば以下の例では、http://localhost:3000/client-demohttp://localhost:3000/slowhttp://localhost:3000/user のパスでそれぞれのページが表示されます。

/ServerComponent/
└── server_component/
    ├── src/
    │   └── app/
    │       ├── layout.tsx
    │       ├── page.module.css
    │       ├── page.tsx
    │       ├── client-demo/
    │       │   ├── page.module.css
    │       │   ├── page.tsx
    │       │   └── components/
    │       │       ├── Counter.module.css
    │       │       ├── Counter.tsx
    │       │       ├── ServerMessage.module.css
    │       │       └── ServerMessage.tsx
    │       ├──  components/
    │       │   ├── Navigation.module.css
    │       │   └── Navigation.tsx
    │       ├── slow/
    │       │   ├── loading.module.css
    │       │   ├── loading.tsx
    │       │   ├── page.module.css
    │       │   └── page.tsx
    │       └── users/
    │           ├── page.module.css
    │           └── page.tsx
    └── tsconfig.json

サーバーコンポーネント

Next.jsでは、デフォルトでサーバーコンポーネントとしてレンダリングされます。

また、サーバーコンポーネントではuseStateやuseEffectといった、状態管理用のhooksは使用できません。作成した場合、型エラーは特に出ませんが、以下のようにエラーになります

Error: × You're importing a component that needs useEffect. This React Hook only works in a Client Component. To fix, mark the file (or its parent) with the "use client" directive.

エラー: × useEffect を必要とするコンポーネントをインポートしています。この React フックはクライアントコンポーネントでのみ動作します。修正するには、ファイル(またはその親)に "use client" ディレクティブを追加してください。

また、通常のReactで作成するコンポーネントではasync/await の記述ができませんでしたが、こちらでは使用することができます。

作ってみる

JSONPlaceholderを使用して、ダミーのユーザーデータを取得するコンポーネントを作成します。

外部APIにリクエストを飛ばすようなコンポーネントを作成する際、通常のReactでは以下のようにuseEffectを使用して実装するのが基本だと思います。

import { useState, useEffect } from "react";
import styles from "./page.module.css";

type UserType = { 
    ...(割愛)
};
export default function UsersPage() {
  // 1. ユーザーデータ
  const [users, setUsers] = useState<UserType[]>([]);
  // 2. 読み込み状態
  const [loading, setLoading] = useState<boolean>(true);
  // 3. エラー状態
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    // データ取得用の非同期関数を定義
    const fetchUsers = async () => {
      try {
        const response = await fetch(
          "https://jsonplaceholder.typicode.com/users"
        );

        if (!response.ok) {
          throw new Error("Failed to fetch users");
        }

        const data: UserType[] = await response.json();
        setUsers(data); // 取得したデータで状態を更新
      } catch (err) {
        // エラーが発生したらエラー状態を更新
        setError(err instanceof Error ? err.message : "An unknown error occurred");
      } finally {
        setLoading(false);
      }
    };

    fetchUsers(); // 関数を実行
  }, []); 

  // 読み込み中の表示
  if (loading) {
    return (
      <div className={styles.container}>
        <h1 className={styles.title}>読み込み中...</h1>
      </div>
    );
  }

  // エラー発生時の表示
  if (error) {
    return (
      <div className={styles.container}>
        <h1 className={styles.title}>エラーが発生しました</h1>
        <p>{error}</p>
      </div>
    );
  }

     return <dev>省略</dev>
}

サーバーコンポーネントでは

  1. 非同期でかける
  2. useState,useEffect等の状態管理hooksは使用できない
  3. フォルダでページ作成

でした!

なのでまず、作成したプロジェクトのsrc/app に、slow フォルダを作成します。これで、[ローカルのURL]/slow のページが作成されます。

以下のように書くことができます。

import styles from "./page.module.css";

// ユーザー情報の型定義
type UserType = {
  id: number;
  name: string;
  username: string;
  email: string;
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
  };
  address: {
    city: string;
    zipcode: string;
  };
};

// サーバーコンポーネント - データを取得して表示
export default async function UsersPage() {
  // JSONPlaceholder APIからユーザーデータを取得
  const response = await fetch("https://jsonplaceholder.typicode.com/users", {
    cache: "force-cache", // データをキャッシュ
  });

  if (!response.ok) {
    throw new Error("Failed to fetch users");
  }

  const users: UserType[] = await response.json();

  return (
    <div className={styles.container}>
        省略
    </div>
  );
}

これで、データの取得・HTMLの組み立てが完了した状態でブラウザでレンダリングされます。

データの取得に関してuseEffectを使用した状態管理の必要がないため、シンプルに書くことができました!

また、通常のReactでは、データの表示に時間がかかるコンポーネントSuspense を使用してローディング状態を表示していましたが、loading ファイルを作成することで、Next.jsが内部で、page.tsx<Suspense>で自動的にラップし、そのfallbackとしてloading.tsxを渡してくれる、という便利な仕組みになっています。(もちろんSuspense を使用することもできます)

これも実際に作ってみます!

slowフォルダ内に、レンダリングに時間のかかる非同期コンポーネントpage.tsxloading.tsx を作成します。

import styles from "./page.module.css";

// 重い処理をシミュレートする関数
async function heavyProcessing(): Promise<string> {
  // 3秒待機(重い処理をシミュレート)
  await new Promise((resolve) => setTimeout(resolve, 3000));
  return "終了!";
}

// 時間のかかるサーバーコンポーネント
async function HeavyComponent() {
  const results = await heavyProcessing();

  return (
    <div className={styles.heavyComponent}>
      <h2 className={styles.heavyTitle}>{results}</h2>
    </div>
  );
}

// メインのページコンポーネント
export default function SlowPage() {
  console.log("🚀🚀🚀🚀SlowPage rendered on the server🚀🚀🚀🚀");

  return (
    <div className={styles.container}>
      <h1 className={styles.title}>⏰ 重い処理のデモページ</h1>

      <div className={styles.demoSection}>
        <HeavyComponent />
      </div>
    </div>
  );
}
export default function Loading() {
  return (
    <div className={styles.loadingContainer}>
        <h2 className={styles.loadingTitle}>⏳ Loading...</h2>
    </div>
  );
}

動かしてみると、自動でLoading.tsxが表示されていると思います。

ローディング中のUI表示切り替えも自動で行ってくれるのが地味に便利だなと感じました。

クライアントコンポーネント

リクエスト等を管理するのにサーバーコンポーネントは非常に便利だと感じましたが、onClick時のリッチな画面UIを作成するにあたってはやはり状態管理が必要だと思います。Next.jsを使用した実装では、デフォルトでサーバーコンポーネントが作成されますが、ファイルに ‘use client’ を記述することで、従来通りuseStateuseEffect のようなhooksも今まで通り使用できます。

以下、Reactハンズオンでよく実装されるカウントアップのコンポーネントです。

useStateとuseEffectを使用して、最終更新時刻を表示・更新するようにしています。

"use client";

import { useState, useEffect } from "react";
import styles from "./Counter.module.css";

export default function Counter() {
  const [count, setCount] = useState(0);
  const [lastUpdated, setLastUpdated] = useState<string>("");

  const increment = () => setCount((prev) => prev + 1);
  const decrement = () => setCount((prev) => prev - 1);
  const reset = () => setCount(0);

  // useEffect フック: カウントが変更されるたびに実行される
  useEffect(() => {
    // 初回マウント時は実行しない(カウントが0で初期化される時)
    if (count === 0 && lastUpdated === "") return;

    // 現在の時刻を取得して表示用にフォーマット
    const now = new Date();
    const timeString = now.toLocaleTimeString("ja-JP", {
      hour: "2-digit",
      minute: "2-digit",
      second: "2-digit",
    });
    setLastUpdated(`最終更新: ${timeString}`);
  }, [count]); // 依存配列: countが変更された時のみエフェクトを実行

  return (
    <div className={styles.container}>
      <div className={`${styles.display} ${styles.updated}`}>
        <span className={styles.count}>{count}</span>
      </div>

      {lastUpdated && <div className={styles.lastUpdated}>{lastUpdated}</div>}

      <div className={styles.controls}>
        <button
          onClick={decrement}
          className={`${styles.button} ${styles.decrementButton}`}
        >
          -1
        </button>
        <button
          onClick={reset}
          className={`${styles.button} ${styles.resetButton}`}
        >
          リセット
        </button>
        <button
          onClick={increment}
          className={`${styles.button} ${styles.incrementButton}`}
        >
          +1
        </button>
      </div>
    </div>
  );
}

このコンポーネントを、通常通りPage. tsxで呼び出すことで使用できます。

Node.js

JavaScriptは本来、ブラウザ上で動作するクライアントサイドのプログラミング言語であり、セキュリティ上の理由からOSのファイル操作や直接的なネットワーク通信などの機能は制限されています。

Node.jsは、そのJavaScriptをサーバーサイドで実行するための環境です。これにより、JavaScriptを使ってファイルの読み書き、ネットワーク通信、データベースとの接続といったサーバーならではの処理が可能になり、WebサーバーやAPIなどのバックエンドアプリケーションを開発できるようになります。

Next.jsのサーバー機能(Server Componentsの実行、SSRなど)は、このNode.jsが提供する環境の上で動作しています。

すこし話が脱線しますが、公式ドキュメントの説明に「別の環境で動作する」という文言があったと思います。先ほども述べたように、SSRの実行はNode.jsの環境上で実行されているようです。なら「クライアントアプリケーションや SSR サーバとは別の環境」って、どこ?とわからなくなったので、私なりに調べました。結論としては、「従来のプロセスとは違った形でレンダリングされる仕組み」で実行されることを指していると解釈できるかと思います。

単純に従来のNext.jsのSSRサーバでは、単にHTMLを組み立てて返すだけ(getServerSideProps()という関数を使用した同期的な処理)でしたが、ServerComponentでは非同期の処理が可能になっています。そういった意味で、「別の環境」と言われているのかなと考えました。(違いましたら是非ご指摘お願いします🙇‍♀️)

さいごに

Next.jsを使用して色々な機能に触ってみました。通常の読み込みではレンダリングの仕組みについて意識しないと意図しない挙動を起こしてしまうことが度々ありましたが、読み込んだ状態を前提にしてコンポーネントを組み立てられるのは非常に楽だなと感じました。

また、(記事の内容に直接関係はしませんが、)デモページを作成するにあたってページのスタイルや枠組みはほとんどcopilotに作ってもらいました。勉強するにあたってどこまでAIに頼るかの是非はあると思いますが、「試しに触りたい!」がすごく気軽にできて楽しかったです。

この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!

下記リンクよりご応募お待ちしております!

iimon採用サイト / Wantedly / Green

参考・引用