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/helloのGET')
}
export async function POST(request: Request) {
return new Response('/api/helloのPOST')
}
残念ながら全メソッド共通のANY的なものは今のところ無さそう。
Routing: Route Handlers | Next.js
パスパラメータ
src/app/api/hello/[id]/route.tsを作成して、パスの[id]はこんな感じで取得できました。
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も触れたかったのですが、、長くなったのでこの辺で一旦おわり_| ̄|○