サーバーコンポーネントの特徴
サーバーでレンダリングされるのでjsはクライアントに送られない。
実装したJSのソースだったりとか、利用しているnpmパッケージなどすべてクライアントに送られません。あくまでサーバーでJSを実行した後の静的なHTMLなどだけが送付されることになります。
サーバーコンポーネントのメリット
サーバーでしか扱えないリソースにアクセスできる。
ファイルシステムだったり、データベースだったり。
ネットワークのレイテンシ(遅延)が小さくなる。
クライアントより性能がよいサーバー上で別サーバーにやり取りしたりするので、性能がまちまちなクライアントの端末でいろんなところにやり取りするよりも遅延が少なくなります。
環境変数を完全にシークレットにできる。
通常のReactやNext.jsの場合環境変数を使いたい場合は「REACT_APP」や「NEXT_APP」などの接頭辞を使ってアクセスしていたと思うのですがそれは完全にシークレットにすることはできませんでした。
サーバーコンポーネントであれば特にそれらの接頭辞がなくてもコンポーネント側から環境変数にアクセスできるので環境変数がクライアント側から完全シークレットになります。
環境変数にはAPIキーなどの完全に隠蔽したい情報も含まれていると思いますので、一つのメリットになります。
コンポーネントレベルで非同期処理構文(async、await)が使える。
従来のReactであれば非同期処理を記述する場合は、useEffectを使ったりとか、React Queryを使ったりとか、SWRを使ったりだとかサードパーティのライブラリを使ってデータフェッチするしかありませんでした。
バンドルサイズを低減できる。
必要な処理はサーバー側でやってしまうのでフロントエンドで必要なパッケージの数が減ります。ということで従来のNext.jsよりもバンドルサイズが減ります。
サーバーコンポーネントのデメリット
Browser API、React Hookなどは使えない。
例えば、localstrageだったりとか、windowだったりとかブラウザ特有のAPIは使えないです。ReactのuseEffectやuseStateなども使えないです。
Event listenerは利用できない。
例えば、onClickなどですね。なのでフォーム画面とかJSでアクションを実行させたい画面の場合はサーバーコンポーネントは使えないです。あくまで表示用です。
サーバーコンポーネントとクライアントコンポーネントの使い分け
用途 | サーバーコンポーネント | クライアントコンポーネント |
データフェッチを行うコンポーネント | 〇 | |
シークレットにしなければならないキーがある場合 | 〇 | |
npmパッケージが大きいものを使う場合 | 〇 | |
イベントリスナーがある場合 | 〇 | |
React Hooksを使う場合 | 〇 | |
ブラウザー APIがある場合 | 〇 |
実装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
async function fetchUsers() { const res = await fetch(リクエストURL) if (!res.ok) { throw new Error('Failed to fetch data in server') } const users = await res.json() return users } export default async function UsersList() { const notes = await fetchUsers() return ( <div> <ul className="m-3"> {users.map((user) => ( <li key={user.id}> <p> {user.name}</p> </li> ))} </ul> </div> ) } |
featch
JavaScriptのfetch APIです。Next.jsでキャッシュ機能などの拡張がされているのでフェッチする場合はこれを使うようにしましょう。(axiosやrequestは使わないこと)
ちなみに、キャッシュオプションを指定しない場合のデフォルトは「force-cache」になっていて、従来のNext.jsでいうところのstaticなページに対応したものになります。(getStaticProps)
cacheオプションの種類
オプション | 使用例 | 説明 |
force-cache | fetch(URL,{cache: 'force-cache'}) | デフォルト値、従来のgetStaticPropsと同じ挙動 |
no-store | fetch(URL,{cache: 'no-store'}) | レンダリングをダイナミック(getServerSideProps)のようにしたい場合 |
revalidate | fetch(URL,{next: {revalidate:10}}) | ISRのようにしたい場合はこれを使う。数値は秒数です。内部的に指定した秒数経過したらHTMLを再生成します。 |
res.json()
これをすることでシリアライズをして文字列化しています。こうすることでJavaScript内でオブジェクト形式で値を扱えるようにできる上、軽量化できるので基本的にはfetchした場合はjsonメソッドを使用します。
起動方法
サーバーコンポーネントはnpm run devでは起動できません。
npm run buildでビルドした後に、npm startで起動して動作確認を行いましょう。
注意点
ビルド時に生成されたHTMLがCDNにキャッシュされるのでサーバー側のデータが変更されてもその変更が画面に反映されません。ビルド時のデータになっている。(staticレンダリング)
staticレンダリングされたhtmlの場所(cacheオプションにforce-cacheを指定した場合)
1 |
.next/server/app/index.html |
サーバーコンポーネント←→クライアントコンポーネントを相互に呼び出せるか。
「サーバーコンポーネント」から「クライアントコンポーネント」の呼び出しは可能です。
ただし、逆に「クライアントコンポーネント」から「サーバーコンポーネント」を呼び出すということはできません。
例外
以下のようにしてサーバーコンポーネント内でクライアントコンポーネントを呼び出してさらにその中にサーバーコンポーネントを入れ込むというchildrenとしてであれば定義することは可能です。
1 2 3 4 5 6 |
function サーバーコンポーネント() { return ( <ClientComponent> <ServerComponent /> </ClientComponent> );} |
理由としては、上記の形を見た際にReact側がサーバーコンポーネントだけを事前にビルドしておかないといけないと判断できるためです。
ページをリロードせずにサーバーコンポーネントでフェッチする内容を最新化するには?
これはクライアントコンポーネントだけで使えるrouter.refreshという機能を使えば実現できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
'use client' import { useRouter } from 'next/navigation' export default function AAA() { const router = useRouter() return ( <button onClick={() => { router.refresh() }} > Refresh </button> ) } |
上記のように実装しサーバーコンポーネントの中にこのクライアントコンポーネントを組み込んであげたうえでボタンを押すことでサーバーコンポーネントの内容をページリロードすることなく最新化することが可能です。
なお、router.refreshは「クライアントコンポーネントのuseStateの内容は初期化されない」という特徴があります。(再レンダリングは普通に行われます。)あくまでサーバーコンポーネントの初期化だけ行う構文になります。
認証機能などを取り入れる際の設計上の注意点
例えば、認証基盤とかでアクセストークンをサーバーコンポーネントとクライアントコンポーネントで同じものを使っていることが要件などがあるかと思います。
そのばあい、同じトークンを使うようにしないといけないのでクライアントコンポーネント側で都度チェックするようにして、もしトークンが異なった場合はrouter.refreshなどでサーバーコンポーネントを再レンダリングさせてあげる必要があります。(でないとサーバーコンポーネントとクライアントコンポーネントで別ユーザーの情報が取得されてしまうということになってしまうため。)
この記事へのコメントはありません。