Next App Router を考える
Next.js での App Router について考える
具体的には、Server/Client でのコードの棲み分けについてとその設計周りについて
Server/Client でコードを一緒にしたい
できることなら、同じところからインポートしたら実行環境でよしなに出し分けてほしい
しかし無理
そこで ↓ のような構成を考えてみる
/lib
|- hoge/
|- client.ts
|- index.ts
|- server.ts
index.ts
には、Server/Client どちらでも使うコードを書く。例えば、型定義とか
server.ts
, client.ts
はそれぞれの環境に合った実装を書く
import するときは
// in server
import {} from './lib/hoge/server'
// in client
'use client'
import {} from './lib/hoge/client'
みたいに書く。import の path に server/client がつくのでちょっとわかりやすい
I/F を統一する
一緒にしたいモチベーションとしてインターフェースを同じにしたい。
完全に同じとは言わなくても、返り値の型とかは一緒にしたい
例えば、型安全に cookie を扱う関数について考えてみる
// cookie/index.ts
export type TypedCookie<T extends string> = Record<
T,
string | number | object | boolean | null
>
export interface CookiesController<T extends string, S extends TypedCookie<T>> {
getItem: (key: T) => S[T] | null
setItem: (key: T, value: S[T]) => void
removeItem: (key: T) => void
}
export type Key = 'token'
export type Cookies = {
token: string
}
Interface をあわせたいので Inerface
を使う
Server/Client はそれに合った実装をする
使う側での理想は interface
を受け取ってそれを使う(=実装に依存しない)という形だが、実行環境によって DI なりなんなりをしないといけないので多分ダルい
API 周りのまとめたい
API 周りもまとめたい
Server では普通に Async/Await で Promise を剥がすが、Client では SWR を使いたい
普通に Fetch してそれを出すだけ、のコンポーネントだったら Client 側で API のハンドルは不要なのだけど、ユーザーの操作とかに依存することが多分多いはず
ディレクトリの構成は先程と同じような形。API の絡んだ操作周りを usecase
という名前にするのが好きなのでそうする
usecase/
|- preferences/
|- client.ts
|- index.ts
|- server.ts
書き分けとしても先程と同じ
index.ts では API からデータを取ってくる部分と key の定義を書く
import { agent } from '@/lib/bsky'
import { AppBskyActorDefs } from '@atproto/api'
// リクエストを一意に識別できるKey
export const key = () => {
return 'preferences'
}
// ある関心についてデータを取ってくるための関数
// 今回は Bluesky の Pin をした feed の情報を取ってくる
export const getPreferences = async () => {
const pref = await agent.app.bsky.actor.getPreferences()
const pinnedPref = pref.data.preferences.flatMap((x) => {
if (AppBskyActorDefs.isSavedFeedsPref(x)) {
return x.pinned
}
return []
})
return agent.app.bsky.feed.getFeedGenerators({
feeds: pinnedPref,
})
}
で、Server/Client では ↑ のコードを使って各々の環境に合ったデータフェッチをする
// in server
import { auth } from '@/lib/bsky/server'
import { getPreferences } from '.'
export const usePreferences = async () => {
// この関数は認証が必要なので認証を通す
await auth()
// fetcher の値をそのまま返す
return getPreferences()
}
// in client
import useSWR from 'swr'
import { getPreferences, key } from '.'
export const usePreferences = () => {
const { data, error } = useSWR(
// さっき定義した key を使う
key(),
() => {
// Client では認証が通っているときにしか使われないので認証のロジックは無し
return getPreferences()
},
{
suspense: true,
},
)
return {
data,
error,
}
}
あとはよしなにこいつらを使う
import clsx from 'clsx'
import { SWRConfig } from '@/lib/swr'
import { unstable_serialize } from 'swr'
import { usePreferences } from '@/usecase/preferences/server'
import * as UsePreferences from '@/usecase/preferences'
import { Feed } from './_components/Feed/server'
export default async function Home() {
const preferences = await usePreferences()
return (
<SWRConfig
value={{
fallback: {
[unstable_serialize(UsePreferences.key())]: preferences,
},
}}
>
<main className={clsx('flex', 'flex-row', 'h-full', 'space-x-2')}>
{preferences.data.feeds.map((feed) => {
return <Feed key={feed.uri} generator={feed} />
})}
</main>
</SWRConfig>
)
}
SWRConfig
で fallback の値を設定する。
こうすることで、Client で初回 fetch が完了する前に Server で fetch した値を使ってくれる
Client ではあとは普通のReactを書けば OK
server/client 問わず、use***
という命名はどうなんじゃというところはあるが、まぁ、はい
server action とかを使うようになったら、client の mutate に合わせて実装すれば良さそう(そうなったら Client はいらなくなる...?)
usecase/
|- preferences/
|- client.ts
|- index.ts
|- server.ts
最近は ↑ というディレクトリ構造があったら、index.ts
, server.ts
は get***
で client.ts
は use***
にしてる。
RSC で use***
を await したら lint に怒られたので get***
に変えた。
index.ts
は fetch***
とかのほうがいいのかな?とかも思ってる