yasudacloudの日記

札幌に住むソフトウェアエンジニア

NextJS13のSSRをキャッチアップする

NextJS13のappディレクトリが便利そうですが変更点が多いようです。いざって時に使えるようにSSRの基本的な挙動を確認していきます。

ドキュメントはこちらに記載されてますが、まだ出来てないページもあります。

自分の手元のNextJSは13.2.3です。

セットアップ

最新のcreate-next-appを実行してインタラクティブに設定を入力していきます。設定値はこんな感じでappディレクトリをYesにして他はデフォルトのままにします。

What is your project named? my-app

Would you like to use TypeScript with this project? No / Yes

Would you like to use ESLint with this project? No / Yes

Would you like to use `src/` directory with this project? No / Yes

Would you like to use experimental `app/` directory with this project? No / Yes

What import alias would you like configured? @/*

ルーティング

画面のルーティング

index.*はpage.*になったようです。例えばsrc/app/example/index.tsxにReactコンポーネントを作ってみてもこのように404になりました。

src/app/example/page.tsxに変更すればとりあえずOKです。

APIのルーティング

シンプルなパス

例えば/api/helloというAPIリソースを作りたい場合、src/app/hello/route.tsを作成。このようにexportする関数名でHTTPメソッドを分けれるので便利そうです。

// route.ts

export async function GET(request: Request) {
return new Response('/api/helloGET')
}

export async function POST(request: Request) {
return new Response('/api/helloPOST')
}

残念ながら全メソッド共通のANY的なものは今のところ無さそう。

Routing: Route Handlers | Next.js

パスパラメータ

src/app/api/hello/[id]/route.tsを作成して、パスの[id]はこんな感じで取得できました。

interface Params {
id: string
}

export async function GET(request: Request, context: { params: Params }) {
console.log(context.params.id)
return new Response('/api/hello/[id]GET')
}

SSRのレンダリング

最も気になる部分。なぜならappディレクトリではgetStaticPropsやgetServerSidePropsが使えないのでどうやってサーバー/クライアントを区別するのか謎だったからです。

ややこしいので段階を踏んで検証してみました。

1. サーバーサイドでコンポーネントを静的に表示

import * as React from "react";

// src/app/example/page.tsx
export default function Page() {
return <span>シンプルなページ</span>
}

特に非同期処理やブラウザJSでごにょごにょすることもなく、ただサーバーサイドでHTMLをレンダリング。単にspanで囲われたシンプルなページというHTMLが生成されます。

サーバーサイドでレンダリングされたかどうかはChromeだと右クリック→ソースの表示でHTMLのプレーンな中身が見れるので確認できます。

2. サーバーサイドでレンダリングしつつ、クライアント側でStateを持つ

1のSSRに加えてクライアント(ブラウザJavaScript)で定番のStateを持たせます。ポイントは頭に'use client';を付ける必要がある点です。

'use client';
import React, {useState} from "react";

// src/app/example/page.tsx
export default function Page() {
const [value, setValue] = useState(1)
return (
<>
<span>SSRと一部クライアント動的ページ</span>
<button onClick={() => setValue(value + 1)}>counter</button>
{value}
</>
)
}

上記の実装でSSRされるHTMLはこのようになります。useStateのデフォルト値がサーバーサイドのHTMLに入ってます。

ボタンを押すとクライアントサイドのReact上でインクリメントされていきます。

3. サーバーサイドで非同期でデータ取得してSSR

話がやや複雑になります。まず、2の実装に下記のようなasync/awaitを追加するとエラーになります。

'use client';
import React, {useState} from "react";

// src/app/example/page.tsx
export default async function Page() {
const res = await fetch('https://google.jp')
console.log(await res.text())

const [value, setValue] = useState(1)
return (
<>
<span>SSRと一部クライアント動的ページ</span>
<button onClick={() => setValue(value + 1)}>counter</button>
{value}
</>
)
}

おそらく'use client'が付いているとクライアントサイドでもrenderを通るためawaitがあるとエラーになるのだと思います。SPAのReactを始めた際に最初にやってしまいがちなあれですね。

そこでサーバーサイドのコンポーネントからクライアントのコンポーネントを分けて書くと解決しました。こんな感じ。

import React from "react";
import {Counter} from "@/component/counter";

// src/app/example/page.tsx
export default async function Page() {
const res = await fetch('http://localhost:3000/api/hello/123')
const body = await res.text()

return (
<>
<span>SSRと一部クライアント動的ページ</span>
{body}
<Counter/>
</>
)
}



'use client';
import React, {useState} from "react";

// src/component/counter.tsx
export const Counter = () => {
const [value, setValue] = useState(1)
return (
<>
<button onClick={() => setValue(value + 1)}>counter</button>
{value}
</>
)
}

SSRアプリを作る場合は基本的にSSRのコードだけsrc/appに置いておくのがベターな気がします。ちなみに上記のfetchはサーバーサイドで行われるのでクロスドメインのエラーは起きません。

あとキャッシュやDynamic Renderingも触れたかったのですが、、長くなったのでこの辺で一旦おわり_| ̄|○