App Router用fetcherの雛形を考えてみた
こんにちは!りょたです。
Next.js13 App Router用のfetcherの雛形を作成してみたので紹介します。いわゆる「俺俺フェッチャー」ってやつです。
前置き
まず、Next.js13からレンダリングタイプについて、fetchメソッドで制御できるようになりました。Next.jsのドキュメントを参考に、するとSG,SSR,ISRの指定は以下のようなインターフェースとなります。
export default async function Page() {
// This request should be cached until manually invalidated.
// Similar to `getStaticProps`.
// `force-cache` is the default and can be omitted.
const staticData = await fetch(`https://...`, { cache: 'force-cache' });
// This request should be refetched on every request.
// Similar to `getServerSideProps`.
const dynamicData = await fetch(`https://...`, { cache: 'no-store' });
// This request should be cached with a lifetime of 10 seconds.
// Similar to `getStaticProps` with the `revalidate` option.
const revalidatedData = await fetch(`https://...`, {
next: { revalidate: 10 },
});
return <div>...</div>;
}
必要な部分だけ抜き出すと、
//従来のgetStaticPropsの替わり
const staticData = await fetch(`https://...`, { cache: 'force-cache' });
//従来のgetServerSideProps替わり
const dynamicData = await fetch(`https://...`, { cache: 'no-store' });
//従来のrevalidate関数替わり
const revalidatedData = await fetch(`https://...`, {
next: { revalidate: 10 },
});
このようなfetchのoptionを利用して、レンダリングタイプを指定するようになりました。
アプリケーションのなかで、データフェッチを行う機会は非常に多いです。そんな中、そのたびに「fetchメソッドを書いて、その後に、cacheオプションを書いて、値を指定して。。。」と、このように考えるのは不便で仕方がありません。というわけで、これを解決するためには、雛形となるようなフェッチャーを作成するのが良いのではないかと思います。
続いて、満たすべき仕様について考えました。
- 第一引数でAPIエンドポイント(URL)を渡せること。
- レスポンスされたデータに型をつけられること。
- ISRのメソッドの時はrevalidateの値(frequency)を渡すようにすること。
このような仕様が必要ではないかと思います。
これらを満たすように、雛形を作成していきます。
完成するまでの試行錯誤
「プロセスはいいから、結論を教えてくれ!」という方はこちらのリンクで下部に飛んでください。
まず、第一号を紹介します。簡略化するために、一旦エラーハンドリングは抜きにしています。
このような形になりました!
type RENDERING_TYPE = "SG" | "SSR" | "ISR";
const baseFetcher =
(renderingType: RENDERING_TYPE) =>
async (url: string, frequency?: number) => {
let options: RequestInit = {};
switch (renderingType) {
case "SG":
options = { cache: "force-cache" };
break;
case "SSR":
options = { cache: "no-store" };
break;
case "ISR":
options = { next: { revalidate: frequency } };
break;
default:
break;
}
const response = await fetch(url, options);
const data = await response.json();
return data;
};
export const fetchDataWithSG = baseFetcher("SG");
export const fetchDataWithSSR = baseFetcher("SSR");
export const fetchDataWithISR = baseFetcher("ISR");
今、振り返ると残念な感じが否めません。。💦
いくつか問題点があります。レスポンスされたデータに型がついていないことと、レンダリングタイプがISRのときもfrequencyがオプショナル指定になってしまっています。
そこで、typescriptのオーバーロード(参考記事)を利用して、レンダリングタイプがISRのとき、frequencyを必須にするように修正しました。
type Fetcher = {
(renderingType: "SG" | "SSR", url: string): Promise<any>,
(renderingType: "ISR", url: string, frequency: number): Promise<any>,
}
const baseFetcher: Fetcher =...(略)
export const fetchDataWithSG = (url: string) => baseFetcher("SG", url);
export const fetchDataWithSSR = (url: string) => baseFetcher("SSR", url);
export const fetchDataWithISR = (url: string, frequency: number) => baseFetcher("ISR", url, frequency);
これで、レンダリングタイプがISRのとき、frequencyが必須となるような関数を作成できました。
あとは、typescriptのジェネリクスを利用して、型を引数として渡せるように修正します。
type Fetcher = {
<T>(renderingType: "SG" | "SSR", url: string): Promise<T>,
<T>(renderingType: "ISR", url: string, frequency: number): Promise<T>,
}
const baseFetcher: Fetcher =
async <T>(renderingType, url, frequency?) => {(略)}
export const fetchDataWithSG = <DataType>(url: string) => baseFetcher<DataType>("SG", url);
export const fetchDataWithSSR = <DataType>(url: string) => baseFetcher<DataType>("SSR", url);
export const fetchDataWithISR = <DataType>(url: string, frequency: number) => baseFetcher<DataType>("ISR", url, frequency);
VScode上で確認しても、問題なく利用できそうです。
ISRを指定する時は、frequencyを必須にすることもできました。
これで、当初やりたかった雛形を作成することができました!✨
📍full code
エラーハンドリングとoptionを引数に入れるように修正したものを載せておきます。
type RENDERING_TYPE = "SG" | "SSR" | "ISR";
type Fetcher = {
<T>(
renderingType: "SG" | "SSR",
url: string,
headers: RequestInit | undefined
): Promise<T>;
<T>(
renderingType: "ISR",
url: string,
headers: RequestInit | undefined,
frequency: number
): Promise<T>;
};
const baseFetcher: Fetcher = async <T>(
renderingType: RENDERING_TYPE,
url: string,
headers: RequestInit | undefined,
frequency?: number
) => {
let options: RequestInit = headers ? { ...headers } : {};
switch (renderingType) {
case "SG":
options = { ...options, cache: "force-cache" };
break;
case "SSR":
options = { ...options, cache: "no-store" };
break;
case "ISR":
if (frequency === undefined) {
throw new Error("🔥: frequencyを指定してください。");
}
options = { ...options, next: { revalidate: frequency } };
break;
default:
throw new Error(`🔥: renderingTypeに誤りがあります。 ${renderingType}`);
}
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`🔥: status200以外です: ${response.status}`);
}
const data = (await response.json()) as T;
return data;
};
export const fetchDataWithSG = <DataType>(url: string, headers?: RequestInit) =>
baseFetcher<DataType>("SG", url, headers);
export const fetchDataWithSSR = <DataType>(
url: string,
headers?: RequestInit
) => baseFetcher<DataType>("SSR", url, headers);
export const fetchDataWithISR = <DataType>(
url: string,
frequency: number,
headers?: RequestInit
) => baseFetcher<DataType>("ISR", url, headers, frequency);
まだまだ詰めの甘いところがあると思いますので、アドバイスお願いします!
おまけ
スキーマによる型チェックをしたかったので、zodを用いたフェッチャーも掲載しておきます。
import { z } from "zod";
type RENDERING_TYPE = "SG" | "SSR" | "ISR";
type Fetcher = {
<T>(
renderingType: "SG" | "SSR",
url: string,
schema: z.ZodSchema<T>,
headers: RequestInit | undefined
): Promise<T>;
<T>(
renderingType: "ISR",
url: string,
schema: z.ZodSchema<T>,
headers: RequestInit | undefined,
frequency: number
): Promise<T>;
};
const baseFetcher: Fetcher = async <T>(
renderingType: RENDERING_TYPE,
url: string,
schema: z.ZodSchema<T>,
headers?: RequestInit,
frequency?: number
) => {
let options: RequestInit = headers ? { ...headers } : {};
switch (renderingType) {
case "SG":
options = { ...options, cache: "force-cache" };
break;
case "SSR":
options = { ...options, cache: "no-store" };
break;
case "ISR":
if (frequency === undefined) {
throw new Error("🔥: frequencyを指定してください。");
}
options = { ...options, next: { revalidate: frequency } };
break;
default:
throw new Error(`🔥: renderingTypeに誤りがあります。 ${renderingType}`);
}
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`🔥: status200以外です: ${response.status}`);
}
const rawData = await response.json();
const data = await schema.parse(rawData);
return data;
};
export const fetchDataWithSG = <DataType>(
url: string,
schema: z.ZodSchema<DataType>,
headers?: RequestInit
) => baseFetcher("SG", url, schema, headers);
export const fetchDataWithSSR = <DataType>(
url: string,
schema: z.ZodSchema<DataType>,
headers?: RequestInit
) => baseFetcher<DataType>("SSR", url, schema, headers);
export const fetchDataWithISR = <DataType>(
url: string,
schema: z.ZodSchema<DataType>,
frequency: number,
headers?: RequestInit
) => baseFetcher("ISR", url, schema, headers, frequency);
//呼び出し方
const getTodoList = async () => {
const TodoSchema = z.object({
userId: z.number(),
id: z.number(),
title: z.string(),
completed: z.boolean(),
});
const data = await fetchDataWithSG<Todo[]>(
"https://jsonplaceholder.typicode.com/todos",
TodoSchema.array()
);
return data;
};
最後まで読んでいただきありがとうございます。
気ままにつぶやいているので、気軽にフォローをお願いします!
Discussion