白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

Next.js(App Router)を試してみる3(コンポーネント)

こんばんは。
除湿機の水を捨てるたびに「そんなに水が湧いてくるのおかしくない?」って思う私です。

前回はルーティング周りを確認したので、今回はレンダリング周りを確認します。
そもそもReactを触っていないので、Reactの機能なのかNext.jsの機能なのかを理解するのが難しい…。

コンポーネント

ReactではUIをコンポーネントという部品に分割し、表示とそれに必要な処理を1つにまとめます。コンポーネントJavaScriptの関数に近く、任意の引数を受け取り、表示するReact要素を返します。

function Hello(props) {
  return <p>Hello {props.name}</p>;
}

コンポーネントは他のコンポーネントにおいて利用でき、その結果はHTMLとしてレンダリングされます。つまりReactではコンポーネントの組み合わせでページをレンダリングします。

function Application() {
  return (
    <>
      <Hello name="Hoge">
      <Hello name="Fuga">
      <Hello name="Piyo">
    </>
  );
}

しれっと<>~</>という謎の表記が出てきていますが、React要素では複数要素を同時に返却する際にこのような書き方をします。(ルート要素が複数あるとエラーになります)

クライアントサイドとサーバーサイド

Next.jsでのWebアプリケーションを作る場合、サーバーサイドレンダリングとクライアントサイドレンダリングを意識する必要があります。レンダリングの種類はコンポーネントごとに選択でき、それぞれサーバーコンポーネント、クライアントコンポーネントと呼びます。

公式ページを参照すると、

The client refers to the browser on a user's device that sends a request to a server for your application code. It then turns the response from the server into an interface the user can interact with.

と記載のある通り、クライアントはユーザのデバイス上のブラウザを指し、サーバーからのレスポンスをユーザーが操作できるインターフェースに変換します。

The server refers to the computer in a data center that stores your application code, receives requests from a client, does some computation, and sends back an appropriate response.

同様にサーバーはデータセンター内のコンピュータを指し、クライアントからのリクエストを受け取り、レスポンスを送り返します。

意味としては通常のWebアプリケーションでいうところのクライアントサイドとサーバサイドの解釈と特に変わりはありません。サーバサイドの処理でJavaを利用し、クライアントサイドの処理でJavaScriptを利用するみたいなパターンはとても一般的です。

ややこしいのが、Next.jsではサーバサイドの処理もクライアントサイドの処理も同じように書けるということです。Next.jsはReactをベースとしたWebアプリケーションフレームワークなので、サーバーサイドの処理もReactライクに書けます。「全部クライアントサイドでも良くない?」と思うかもしれませんが、サーバーサイドでレンダリングすることによるメリットの例としては、

・サーバー側のリソースを使ってレンダリング処理を行うため、一般的にパフォーマンスが良くなる
・キャッシュが効くことによってパフォーマンスが良くなる
・静的にレンダリングされることによってSEO的に有利になる

などが挙げられます。そうなると逆に「全部サーバーサイドでも良くない?」と思ったりもしますが、画面の動きを制御するリアクティブな動きはクライアントサイドでの処理でしか実現できません。そこでコンポーネントごとにクライアントサイドとサーバーサイドを選んで使い分ける必要があります。

実際にはWebサーバーの後ろにAPIがあってその後ろにDBがあって…みたいなパターンも多いですよね。

余談ですが、クライアントサイドとサーバーサイドは割と認識が揃いやすいですが、フロントエンドとバックエンドは人や会社によって解釈が違いませんか…?昔よりもアプリケーションの構成が多様になったからだとは思いますが、フロントエンドというときにクライアントサイドのみと、それこそNext.jsなどのフロントエンド用のサーバサイド処理を含む場合もあってややこしいです。

クライアントサイドとサーバーサイドの使い分け

App Routerでは特に明示的に宣言しない限り、コンポーネントはサーバーコンポーネントになります。
つまり、基本的にサーバーコンポーネントで事足りるものに関してはなるべくサーバーコンポーネントを使いましょうということです。

クライアントコンポーネントにする必要があるのはざっくりいうと画面の動きに対してイベントドリブンの処理を書く必要がある場合です。コード上の表現としてはuse~系のReact Hooksを使うような場合です。
「ボタンを押した場合に要素を書き換える」とか「inputの値が変更された場合に連動して要素を絞り込む」とか。プレーンなJavaScriptではDOMの操作系とかAjaxリクエストみたいなところでしょうか。

Reactのサイトから引用するとこんなパターンとか。

'use client'
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      You pressed me {count} times
    </button>
  );
}

'use client'をファイルの先頭で宣言すると、そのコンポーネントそのコンポーネントからインポートするコンポーネントはクライアントコンポーネントになります。
公式ページでもServerComponentとClientComponentの「境界の定義」という記述になっています。

The "use client" directive is a convention to declare a boundary between a Server and Client Component module graph.

'use client'は境界で1回だけ使用することも推奨されています。

"use client" does not need to be defined in every file. The Client module boundary only needs to be defined once, at the "entry point", for all modules imported into it to be considered a Client Component.

サーバーコンポーネントをクライアントコンポーネントからインポートできませんという記述をちらほら見かけますが、コンポーネントの内容によっては普通にインポートできてしまいます。これはサーバーコンポーネントのつもりで作ったコンポーネントがクライアントコンポーネントとして動いてしまっている状態です。サーバーサイドでしか動かない処理だとエラーになります。

というわけでパフォーマンスとしてもなるべくサーバーコンポーネントを使いたいので、クライアントコンポーネントはなるべく末端(リーフ)に配置するのが良さそうです。じゃあ、一番外側をクライアントコンポーネントにしてしまったら全くサーバサイドレンダリングできないかというとそういうわけでもありません。そんな場合にはReact propsを使ってクライアントコンポーネントにサーバーコンポーネントを渡しましょうと書かれています。

'use client'
import { useState } from 'react';

export default function ClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <>
      <button onClick={handleClick}>
        You pressed me {count} times
      </button>
      {children}
    </>
  );
}

こうして、

import ClientComponent from './client-component'
import ServerComponent from './server-component'

export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}

こう。

境目がややこしい

App Routerになってだいぶわかりやすくなったようですが、クライアントサイドとサーバサイドが同じ言語なので便利だけどややこしい…。
もちろん他にも色々な機能はありますが、とりあえずこれで一通り書けるような気がしてきました。