Next.jsで猫画像ジェネレーターを作ろう
Next.jsの概要
Next.jsは、オープンソースのUIライブラリReactをベースにしたフロントエンドフレームワークです。
Reactで開発する場合、webpackのようなバンドラーを用いるのが普通です。webpackの設定ファイルを記述するには、一定の知識が必要です。特に、チャンク分割やCSSモジュールの設定は複雑だったりと、設定ファイルのメンテナンスが大変です。Next.jsは、webpackの設定があらかじめなされた状態で開発が始められるようになっています。
Next.jsはルーティング時のプリフェッチや画像の最適化などのパフォーマンス最適化をフレームワーク内で内包しており、ゼロコンフィグで簡単にパフォーマンスの高いアプリケーションを構築することができます。ページ単位のサーバーサイドレンダリング(SSR)や静的サイト生成(SSG)の機能も提供しており、用途に合わせて柔軟にアーキテクチャを選択できるのも特徴です。
Next.jsはVercel社が開発しています。同社はVercelというホスティングサービスを提供しているので、Next.jsで構築したアプリケーションは簡単に公開できます。
これから作るもの
このチュートリアルでは、題して「猫画像ジェネレーター」です。どんなものかというと、ボタンを押したら、猫画像のAPIから画像のURLを取得し、ランダムに可愛い猫画像を表示するシンプルなウェブアプリケーションです。
最終的な成果物はデモサイトで確認できます。チュートリアルを開始する前に事前に触ってみることで、各ステップでどんな実装をしているかのイメージが掴みやすくなります。また、完成形のソースコードはGitHubで確認することができます。
このチュートリアルに必要なもの
このチュートリアルで必要なものは次のとおりです。
- Node.js v16以上
- Yarn v1系 (このチュートリアルはv1.22.19で動作確認しています)
- ブラウザ (このチュートリアルではGoogle Chromeを想定しています)
Node.jsの導入については、開発環境の準備をご覧ください。
パッケージ管理ツールとしてYarnを利用します。最初にインストールをしておきましょう。すでにインストール済みの方はここのステップはスキップして大丈夫です。
shellnpm install -g yarn
shellnpm install -g yarn
Next.jsをセットアップする
最初にyarn create next-appコマンドでプロジェクトを作成します。TypeScriptをベースにしたプロジェクトを作成するために --example with-typescript を指定します。random-cat はリポジトリ名となる部分です。この部分は好きな名前でも構いませんが、本チュートリアルではrandom-catとして話を進めます。
shyarn create next-app --example with-typescript random-cat
shyarn create next-app --example with-typescript random-cat
このコマンドを実行すると、random-catディレクトリが作成されるので、そのディレクトリに移動してください。
shcd random-cat
shcd random-cat
プロジェクトのファイル構成が次のようになっているか確認してください。
text.├── components ---- ディレクトリ├── interfaces ---- ディレクトリ├── node_modules -- ディレクトリ├── pages --------- ディレクトリ├── utils --------- ディレクトリ├── README.md├── next-env.d.ts├── package.json├── tsconfig.json└── yarn.lock
text.├── components ---- ディレクトリ├── interfaces ---- ディレクトリ├── node_modules -- ディレクトリ├── pages --------- ディレクトリ├── utils --------- ディレクトリ├── README.md├── next-env.d.ts├── package.json├── tsconfig.json└── yarn.lock
開発サーバーを起動する
次のコマンドを実行して、開発サーバーを起動してください。
shyarn dev
shyarn dev
開発サーバーが起動したら、ターミナルに表示されているURLにブラウザでアクセスしてください。
不要なファイルを消す
チュートリアルを進める前に、ここでは使わないファイルを削除します。プロジェクトをシンプルな状態にして、作業を進めやすくしましょう。
shrm -rf pages utils interfaces components
shrm -rf pages utils interfaces components
ページコンポーネントディレクトリを作る
Next.jsでは、pagesディレクトリ配下のディレクトリ構造がページのルーティングに対応します。たとえば、pages/users.tsxとファイルを作成すると、/usersへアクセスしたとき、それが実行されます。pages/index.tsxの場合は、/ へアクセスしたときに実行されます。
このpagesディレクトリに置かれたコンポーネントのことを、Next.jsの用語でページコンポーネント(page component)と呼びます。
次のコマンドを実行してページコンポーネントを置くためのディレクトリを作成してください。
shmkdir pages
shmkdir pages
トップページのページコンポーネントを作る
次のコマンドを実行して、トップページのページコンポーネントを作成してください。
shtouch pages/index.tsx
shtouch pages/index.tsx
ページコンポーネントの内容は、次のようにします。このIndexPage関数がページコンポーネントです。これは「猫画像予定地」が表示されるだけの単純なものです。
pages/index.tsxtsximport {NextPage } from "next";constIndexPage :NextPage = () => {return <div >猫画像予定地</div >;};export defaultIndexPage ;
pages/index.tsxtsximport {NextPage } from "next";constIndexPage :NextPage = () => {return <div >猫画像予定地</div >;};export defaultIndexPage ;
Next.jsでは、1ファイルにつき1ページコンポーネントを作成します。Next.jsはpagesディレクトリの各tsxファイルを読み込み、デフォルトエクスポートされた関数をページコンポーネントとして認識します。上のコードでIndexPage関数をexport defaultにしているのは、Next.jsにページコンポーネントと認識させるためです。NextPageはページコンポーネントを表す型です。この型を注釈しておくと、関数の実装がページコンポーネントの要件を満たしているかがチェックできます。
コンポーネントを実装したら、ブラウザをリロードして画面に「猫画像予定地」と表示されているか確認してください。
JavaScriptで関数を作るには、大きく分けてアロー関数と関数宣言を使った方法の2種類があります。上で書いたIndexPage関数はアロー関数です。これを関数宣言に書き換えると次のようになります。
tsximport {ReactElement } from "react";export default functionIndexPage ():ReactElement <any, any> | null {return <div >猫画像予定地</div >;}
tsximport {ReactElement } from "react";export default functionIndexPage ():ReactElement <any, any> | null {return <div >猫画像予定地</div >;}
Next.jsでは、アロー関数と関数宣言のどちらで書いても構いません。このチュートリアルでアロー関数を採用しているのは、ページコンポーネントにNextPage型の型注釈をつけるのが、アロー関数のほうがやりやすいためです。
The Cat API
このチュートリアルでは猫の画像をランダムに表示するにあたりThe Cat APIを利用します。このAPIは特定の条件で猫の画像を取得したり、品種ごとの猫の情報を取得することができます。今回のチュートリアルではAPIドキュメントのQuickstartに記載されている/v1/images/searchへリクエストを投げてランダムな猫の画像を取得します。
試しにブラウザでhttps://api.thecatapi.com/v1/images/searchへアクセスしてみてください。ランダムな結果が返ってくるので値は少し違いますが、次のような構造のデータがレスポンスとして取得できます。レスポンスのデータ構造が配列になっている点に注意してください。
The Cat APIのレスポンスのサンプルjson[{"id": "co9","url": "https://cdn2.thecatapi.com/images/co9.jpg","width": 900,"height": 600}]
The Cat APIのレスポンスのサンプルjson[{"id": "co9","url": "https://cdn2.thecatapi.com/images/co9.jpg","width": 900,"height": 600}]
レスポンスにあるurlが猫画像のURLです。この値を取得して猫の画像をランダムに表示します。
画像を取得する関数を実装する
このステップでは、The Cat APIにリクエストし猫画像を取得する関数を実装します。次の実装をしたfetchImage関数をexport default IndexPageの後ろに追加してください。
tsconstfetchImage = async () => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};
tsconstfetchImage = async () => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};
fetchはHTTPリクエストでリソースを取得するブラウザ標準のAPIです。戻り値としてResponseオブジェクトを返します。Responseオブジェクトのjson()メソッドを実行することで、レスポンスのボディーをJSONとしてパースし、JavaScriptのオブジェクトとして取得できます。
fetchImage関数についているasyncキーワードは、この関数が非同期処理を行うことを示すものです。fetchとres.jsonは非同期関数で、これらの処理を待つために、それぞれにawaitキーワードがついています。
fetchImage関数がAPIを呼び出せているかテストするために、これを呼び出す処理をfetchImage関数の後ろに追加してください。
pages/index.tsxtsximport {NextPage } from "next";constIndexPage :NextPage = () => {return <div >猫画像予定地</div >;};export defaultIndexPage ;constfetchImage = async () => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};fetchImage (); // 追加
pages/index.tsxtsximport {NextPage } from "next";constIndexPage :NextPage = () => {return <div >猫画像予定地</div >;};export defaultIndexPage ;constfetchImage = async () => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};fetchImage (); // 追加
Chromeの開発者ツールを開いてからページをリロードしてください。「コンソール」タブで次のようなテキストが表示されていたら成功です。

fetchImage関数の動作確認が済んだら、この関数の呼び出しは不要になるので消してください。
関数の戻り値に型をつける
上で作ったfetchImage関数の戻り値の型はany型です。そのため、呼び出し側で存在しないプロパティを参照しても気づけずにバグが発生する危険性があります。
tsconstfetchImage = async () => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};fetchImage ().then ((image ) => {console .log (image .alt ); // 存在しないプロパティを参照している});
tsconstfetchImage = async () => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};fetchImage ().then ((image ) => {console .log (image .alt ); // 存在しないプロパティを参照している});
imageにはaltプロパティがありませんが、imageがany型なので、上のような誤ったコードを書いても、コンパイル時に誤りに気づけません。
APIレスポンスの取り扱いはフロントエンドでバグが混在しやすい箇所なので、型を指定することで安全にAPIレスポンスを扱えるようにしていきます。
レスポンスに含まれる画像情報の型をImageとして定義します。そして、fetchImage関数の戻り値をPromise<Image>として型注釈します。
tstypeImage = {url : string;};constfetchImage = async ():Promise <Image > => {// ^^^^^^^^^^^^^^^^型注釈constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};
tstypeImage = {url : string;};constfetchImage = async ():Promise <Image > => {// ^^^^^^^^^^^^^^^^型注釈constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};
APIレスポンスにはurl以外のプロパティも含まれていますが、このアプリケーションで必要な情報はurlだけなので、他のプロパティの型の定義は省略しています。もし、他のプロパティも必要になった場合でも、Imageにプロパティの定義を追加していけばよいです。
fetchImage関数の戻り値が正しく型注釈がされていると、万が一APIレスポンスに存在しないプロパティを参照するコードを書いてしまっても、コンパイルエラーが発生することで問題に気がつけるようになります。
tsfetchImage ().then ((image ) => {Property 'alt' does not exist on type 'Image'.2339Property 'alt' does not exist on type 'Image'.console .log (image .); // 存在しないプロパティを参照している alt });
tsfetchImage ().then ((image ) => {Property 'alt' does not exist on type 'Image'.2339Property 'alt' does not exist on type 'Image'.console .log (image .); // 存在しないプロパティを参照している alt });
上のコードは、サーバーサイドを100%信頼するコードになっています。クライアントサイドが期待するデータ構造を、サーバーサイドが必ず返すことを前提としたコードなのです。しかし、サーバーサイドは本当に期待するデータ構造を返してくれているでしょうか?
より防衛的にするなら、クライアントサイドではサーバーのレスポンスをチェックするほうが望ましいでしょう。チェックの一例として次のような実装も考えられます。
tsconstfetchImage = async ():Promise <Image > => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages : unknown = awaitres .json ();// 配列として表現されているか?if (!Array .isArray (images )) {throw newError ("猫の画像が取得できませんでした");}constimage : unknown =images [0];// Imageの構造をなしているか?if (!isImage (image )) {throw newError ("猫の画像が取得できませんでした");}returnimage ;};// 型ガード関数constisImage = (value : unknown):value isImage => {// 値がオブジェクトなのか?if (!value || typeofvalue !== "object") {return false;}// urlプロパティが存在し、かつ、それが文字列なのか?return "url" invalue && typeofvalue .url === "string";};
tsconstfetchImage = async ():Promise <Image > => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages : unknown = awaitres .json ();// 配列として表現されているか?if (!Array .isArray (images )) {throw newError ("猫の画像が取得できませんでした");}constimage : unknown =images [0];// Imageの構造をなしているか?if (!isImage (image )) {throw newError ("猫の画像が取得できませんでした");}returnimage ;};// 型ガード関数constisImage = (value : unknown):value isImage => {// 値がオブジェクトなのか?if (!value || typeofvalue !== "object") {return false;}// urlプロパティが存在し、かつ、それが文字列なのか?return "url" invalue && typeofvalue .url === "string";};
このチェック処理では、型が不明な値を安全に型付けするunknown型や、値の型をチェックしながら型付する型ガード関数などのTypeScriptのテクニックも用いています。これらについては、ここでは理解する必要はありませんが、興味のある方はチュートリアルを終えてから解説をご覧ください。
このチュートリアルでは厳密さよりもシンプルさに重心を置くため、上のようなチェック処理はあえて追加せずに話を進めます。
ページを表示したときに画像を表示する
ページを表示したときにfetchImageを呼び出して、猫の画像を表示する処理を書いています。IndexPage関数の中身を次のように変更してください。
pages/index.tsxtsximport {NextPage } from "next";import {useEffect ,useState } from "react";constIndexPage :NextPage = () => {// ❶ useStateを使って状態を定義するconst [imageUrl ,setImageUrl ] =useState ("");const [loading ,setLoading ] =useState (true);// ❷ マウント時に画像を読み込む宣言useEffect (() => {fetchImage ().then ((newImage ) => {setImageUrl (newImage .url ); // 画像URLの状態を更新するsetLoading (false); // ローディング状態を更新する});}, []);// ❸ ローディング中でなければ、画像を表示するreturn <div >{loading || <img src ={imageUrl } />}</div >;};export defaultIndexPage ;typeImage = {url : string;};constfetchImage = async ():Promise <Image > => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};
pages/index.tsxtsximport {NextPage } from "next";import {useEffect ,useState } from "react";constIndexPage :NextPage = () => {// ❶ useStateを使って状態を定義するconst [imageUrl ,setImageUrl ] =useState ("");const [loading ,setLoading ] =useState (true);// ❷ マウント時に画像を読み込む宣言useEffect (() => {fetchImage ().then ((newImage ) => {setImageUrl (newImage .url ); // 画像URLの状態を更新するsetLoading (false); // ローディング状態を更新する});}, []);// ❸ ローディング中でなければ、画像を表示するreturn <div >{loading || <img src ={imageUrl } />}</div >;};export defaultIndexPage ;typeImage = {url : string;};constfetchImage = async ():Promise <Image > => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};
変更内容をひとつひとつ見ていきましょう。
❶ まず、ReactのuseState関数を使い、imageUrlとloadingの2つの状態を定義します。
tsxconst [imageUrl ,setImageUrl ] =useState ("");const [loading ,setLoading ] =useState (true);
tsxconst [imageUrl ,setImageUrl ] =useState ("");const [loading ,setLoading ] =useState (true);
imageUrlは画像のURLが代入される変数です。初期値は空文字列です。loadingはAPIを呼び出し中かどうかを管理する変数です。初期値は呼び出し中を意味するtrueです。
❷ 次に、コンポーネントがマウントされたときに、APIから猫の画像情報を取得する処理を定義します。
tsxuseEffect (() => {fetchImage ().then ((newImage ) => {setImageUrl (newImage .url ); // 画像URLの状態を更新するsetLoading (false); // ローディング状態を更新する});}, []);
tsxuseEffect (() => {fetchImage ().then ((newImage ) => {setImageUrl (newImage .url ); // 画像URLの状態を更新するsetLoading (false); // ローディング状態を更新する});}, []);
ReactのuseEffect関数を使用します。useEffectは2つの引数を指定しています。第1引数は処理内容、第2引数はどのタイミングで処理内容を実行するかの指定です。第2引数は空の配列[]になっています。空配列であるため一見すると不要そうに見えますが、これには「コンポーネントがマウントされたときのみ実行する」という重要な役割があるので省略しないでください。
useEffect関数の第1引数となる関数の処理を見てみましょう。fetchImage関数は非同期処理です。非同期処理が完了したタイミングで、imageUrlに画像URLをセットするためにsetImageUrl関数を呼び出します。同時に、ローディング状態をfalseに更新するためにsetLoading関数も呼び出します。
useEffectには非同期関数は渡せないuseEffectに渡している関数は非同期処理をしているのに、asyncキーワードを使わずにthenを使って記述していることに気がついた方もいるかもしれません。その方の中には、次のように非同期関数を渡す書き方にして、コードが読みやすくしたいと思う方もいるでしょう。
tsuseEffect (async () => {constnewImage = awaitfetchImage ();setImageUrl (newImage .url );setLoading (false);}, []);
tsuseEffect (async () => {constnewImage = awaitfetchImage ();setImageUrl (newImage .url );setLoading (false);}, []);
しかし、useEffectには非同期関数を直接渡すことはできません。渡そうとすると、コンパイルエラーになります。
tsArgument of type '() => Promise<void>' is not assignable to parameter of type 'EffectCallback'. Type 'Promise<void>' is not assignable to type 'void | Destructor'.2345Argument of type '() => Promise<void>' is not assignable to parameter of type 'EffectCallback'. Type 'Promise<void>' is not assignable to type 'void | Destructor'.useEffect (async () => {/* 中略 */}, []);
tsArgument of type '() => Promise<void>' is not assignable to parameter of type 'EffectCallback'. Type 'Promise<void>' is not assignable to type 'void | Destructor'.2345Argument of type '() => Promise<void>' is not assignable to parameter of type 'EffectCallback'. Type 'Promise<void>' is not assignable to type 'void | Destructor'.useEffect (async () => {/* 中略 */}, []);
❸ 最後に画像を表示するロジックです。||は論理和演算子で、loadingがfalseのときに<img>要素を表示します。
tsxreturn <div >{loading || <img src ={imageUrl } />}</div >;
tsxreturn <div >{loading || <img src ={imageUrl } />}</div >;
上の条件分岐を見て「なぜ素直にif文を使わないのか?」と疑問の思ったかもしれません。これには理由があります。JSXの{}で囲った部分には、JavaScriptの式だけが書けます。ifは文であるため使うことができません。もし使おうとすると次の例のようにコンパイルエラーになります。
JSXの式には文が使えないtsx<Expression expected.div >{if (!loading) { <img src ={imageUrl } /> }} </div >
Unexpected token. Did you mean `{'}'}` or `}`?1109
1381Expression expected.
Unexpected token. Did you mean `{'}'}` or `}`?
JSXの式には文が使えないtsx<Expression expected.div >{if (!loading) { <img src ={imageUrl } /> }} </div >
Unexpected token. Did you mean `{'}'}` or `}`?1109
1381Expression expected.
Unexpected token. Did you mean `{'}'}` or `}`?
したがって、JSXの式で条件分岐するには論理演算子や三項演算子を使う必要があります。
tsx<div >{loaded && <img src ="..." />} ── 論理積演算子{loading || <img src ="..." />} ── 論理和演算子{loading ? "読み込み中" : <img src ="..." />} ── 三項演算子</div >;
tsx<div >{loaded && <img src ="..." />} ── 論理積演算子{loading || <img src ="..." />} ── 論理和演算子{loading ? "読み込み中" : <img src ="..." />} ── 三項演算子</div >;
変更内容の詳細は以上です。IndexPageの変更が済んだら、猫の画像が表示されているか確認してみてください。画像がちゃんと表示されているでしょうか。

ボタンをクリックしたときに画像が更新されるようにする
ここではボタンをクリックしたときに、APIから新しい画像情報を取得し、表示中の画像を新しい画像に置き換える機能を作ります。次のようにIndexPageコンポーネントに、handleClick関数とボタンを追加してください。
pages/index.tsxtsximport {NextPage } from "next";import {useEffect ,useState } from "react";constIndexPage :NextPage = () => {const [imageUrl ,setImageUrl ] =useState ("");const [loading ,setLoading ] =useState (true);useEffect (() => {fetchImage ().then ((newImage ) => {setImageUrl (newImage .url );setLoading (false);});}, []);// ボタンをクリックしたときに画像を読み込む処理consthandleClick = async () => {setLoading (true); // 読込中フラグを立てるconstnewImage = awaitfetchImage ();setImageUrl (newImage .url ); // 画像URLの状態を更新するsetLoading (false); // 読込中フラグを倒す};return (<div ><button onClick ={handleClick }>他のにゃんこも見る</button ><div >{loading || <img src ={imageUrl } />}</div ></div >);};export defaultIndexPage ;typeImage = {url : string;};constfetchImage = async ():Promise <Image > => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};
pages/index.tsxtsximport {NextPage } from "next";import {useEffect ,useState } from "react";constIndexPage :NextPage = () => {const [imageUrl ,setImageUrl ] =useState ("");const [loading ,setLoading ] =useState (true);useEffect (() => {fetchImage ().then ((newImage ) => {setImageUrl (newImage .url );setLoading (false);});}, []);// ボタンをクリックしたときに画像を読み込む処理consthandleClick = async () => {setLoading (true); // 読込中フラグを立てるconstnewImage = awaitfetchImage ();setImageUrl (newImage .url ); // 画像URLの状態を更新するsetLoading (false); // 読込中フラグを倒す};return (<div ><button onClick ={handleClick }>他のにゃんこも見る</button ><div >{loading || <img src ={imageUrl } />}</div ></div >);};export defaultIndexPage ;typeImage = {url : string;};constfetchImage = async ():Promise <Image > => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};
これでクリックしたら画像が更新されるようになります。うまく動いているかブラウザで確認してみてください。
Next.jsのSSRとデータフェッチAPI
Reactはクライアントサイドでのレンダリングに特化していますが、Next.jsはサーバーサイドレンダリング(server-side rendering; SSR)をサポートしています。これにより、初回読み込みの速度を向上させることができ、SEOやパフォーマンスにもよい影響を与えます。
SSRはウェブアプリケーションのレンダリングをサーバーサイドで行う技術のことです。通常、クライアントサイドレンダリング(client-side rendering; CSR)では、ブラウザがHTML、CSS、JavaScriptファイルをダウンロードして、JavaScriptを使用してページをレンダリングします。これに対して、SSRではサーバーがHTMLを生成し、ブラウザに送信します。
Next.jsでSSRを行うには、次のデータフェッチAPIの関数を使います。
getServerSidePropsgetStaticPropsgetInitialProps
これらの関数を使うことで、Next.jsで簡単にSSRを実装できます。
getServerSideProps
getServerSidePropsは、ページがリクエストされるたびにサーバーサイドで実行され、ページのプロパティを返す関数です。この関数を使用すると、リクエストごとにページのデータを取得できます。また、クライアントサイドでルーティングが発生した場合も、この関数がサーバーサイドで実行されます。
サーバーサイドでのみ実行されるため、getServerSideProps内でのみ利用しているモジュールや関数は、クライアントのコードにバンドルされません。これは、配信するファイルサイズを削減することにも繋がります。
サーバーサイドで実行されるため、データベースなどウェブに公開していないミドルウェアから直接データを取得するような処理も記述できます。
getStaticProps
getStaticPropsは、静的生成するページのデータを取得するための関数で、ビルド時に実行されます。この関数を使用すると、ビルド時にページのデータを取得しておき、クライアントからのリクエスト時にはそのキャッシュからデータを返すようになります。この関数は、リクエスト時や描画時にはデータ取得が実行されないことに注意が必要です。ユーザーログインが不要なランディングページや、内容のリアルタイムさが不要なブログなどの静的なページを構築するときに利用します。
getInitialProps
getInitialPropsは、SSR時にサーバーサイドでデータ取得の処理が実行されます。また、クライアントサイドでルーティングが発生した場合は、クライアント側でもデータの取得が実行されます。このAPIはサーバーとクライアントの両方で実行されるため、両方の環境で動作するように実装する必要があります。
getInitialPropsは、Next.js 9までのバージョンで使われていた古い方法です。現在でもサポートされていますが、Next.js 10以降では、代わりに getServerSidePropsやgetStaticPropsの使用を推奨しています。
データフェッチAPIを使ってリクエスト時に初期画像を取得する
これまでに作ってきたIndexPageコンポーネントには、クライアントサイドで最初の画像を取得し表示していました。ここでは、データフェッチAPIのgetServerSidePropsを使って、サーバーサイドで初期画像を取得するように変更します。先に完成形のコードを示すと、次のようになります。
pages/index.tsxtsximport {GetServerSideProps ,NextPage } from "next";import {useState } from "react";// getServerSidePropsから渡されるpropsの型typeProps = {initialImageUrl : string;};// ページコンポーネント関数にpropsを受け取る引数を追加するconstIndexPage :NextPage <Props > = ({initialImageUrl }) => {const [imageUrl ,setImageUrl ] =useState (initialImageUrl ); // 初期値を渡すconst [loading ,setLoading ] =useState (false); // 初期状態はfalseにしておく// useEffect(() => {// fetchImage().then((newImage) => {// setImageUrl(newImage.url);// setLoading(false);// });// }, []);consthandleClick = async () => {setLoading (true);constnewImage = awaitfetchImage ();setImageUrl (newImage .url );setLoading (false);};return (<div ><button onClick ={handleClick }>他のにゃんこも見る</button ><div >{loading || <img src ={imageUrl } />}</div ></div >);};export defaultIndexPage ;// サーバーサイドで実行する処理export constgetServerSideProps :GetServerSideProps <Props > = async () => {constimage = awaitfetchImage ();return {props : {initialImageUrl :image .url ,},};};typeImage = {url : string;};constfetchImage = async ():Promise <Image > => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};
pages/index.tsxtsximport {GetServerSideProps ,NextPage } from "next";import {useState } from "react";// getServerSidePropsから渡されるpropsの型typeProps = {initialImageUrl : string;};// ページコンポーネント関数にpropsを受け取る引数を追加するconstIndexPage :NextPage <Props > = ({initialImageUrl }) => {const [imageUrl ,setImageUrl ] =useState (initialImageUrl ); // 初期値を渡すconst [loading ,setLoading ] =useState (false); // 初期状態はfalseにしておく// useEffect(() => {// fetchImage().then((newImage) => {// setImageUrl(newImage.url);// setLoading(false);// });// }, []);consthandleClick = async () => {setLoading (true);constnewImage = awaitfetchImage ();setImageUrl (newImage .url );setLoading (false);};return (<div ><button onClick ={handleClick }>他のにゃんこも見る</button ><div >{loading || <img src ={imageUrl } />}</div ></div >);};export defaultIndexPage ;// サーバーサイドで実行する処理export constgetServerSideProps :GetServerSideProps <Props > = async () => {constimage = awaitfetchImage ();return {props : {initialImageUrl :image .url ,},};};typeImage = {url : string;};constfetchImage = async ():Promise <Image > => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};
上で行った変更をひとつひとつ見ていきましょう。まず、getServerSideProps関数を追加しました。この関数は、サーバーサイドで実行する処理を書きます。上のコードは画像情報を取得する処理になっています。getServerSideProps関数は、IndexPageコンポーネントが引数として受け取るpropを戻り値に含めます。getServerSideProps関数は、Next.jsに認識させるためにexportしておく必要があります。
次に、IndexPage関数はgetServerSidePropsが返すpropsを受け取れるように引数を追加してあります。propsのinitialImageUrlには初期画像のURLが入っていて、この値をimageの初期値となるように、useStateの引数に渡します。
初期画像はサーバーサイドで取得するようにしたので、クライアントサイドで初期画像を取得していたuseEffectの部分は不要になります。
これで、ページをリクエストするタイミングで、サーバーサイドで画像情報が取得され、ランダムに猫画像が表示されるようになります。
ビジュアルを作り込む
機能面が完成したので、最後にビジュアルデザインを作り込んでいきましょう。まず、スタイルシートを作成します。スタイルシートの内容は長くなるので、次のURLからスタイルシートをダウンロードしてください。ダウンロードしたら、pagesディレクトリにindex.module.cssとして保存してください。
https://raw.githubusercontent.com/yytypescript/random-cat/main/pages/index.module.css
このスタイルをIndexPageコンポーネントに当てます。まず、index.module.cssをインポートします。.module.cssで終わるファイルはCSSモジュール(CSS Modules)と言うもので、CSSファイル内で定義したクラス名をTypeScriptからオブジェクトとして参照できるようになります。次に、各要素にclassName属性でクラス名を指定してください。
pages/index.tsxtsximport {GetServerSideProps ,NextPage } from "next";import {useState } from "react";importstyles from "./index.module.css";constIndexPage :NextPage <Props > = ({initialImageUrl }) => {// 中略return (<div className ={styles .page }><button onClick ={handleClick }className ={styles .button }>他のにゃんこも見る</button ><div className ={styles .frame }>{loading || <img src ={imageUrl }className ={styles .img } />}</div ></div >);};// 以下略
pages/index.tsxtsximport {GetServerSideProps ,NextPage } from "next";import {useState } from "react";importstyles from "./index.module.css";constIndexPage :NextPage <Props > = ({initialImageUrl }) => {// 中略return (<div className ={styles .page }><button onClick ={handleClick }className ={styles .button }>他のにゃんこも見る</button ><div className ={styles .frame }>{loading || <img src ={imageUrl }className ={styles .img } />}</div ></div >);};// 以下略
以上でNext.jsを使った猫画像ジェネレーターの開発は完了です。
プロダクションビルドと実行
Next.jsではnext buildを実行することで最適化されたプロダクション用のコードを生成でき、next startで生成されたプロダクションコードを実行できます。このチュートリアルではボイラテンプレートを利用しているので、package.jsonにbuildコマンドとstartコマンドがすでに用意されています。yarn buildとyarn startを実行して本番用のアプリケーションを実行してみましょう。
shyarn build && yarn start
shyarn build && yarn start
アプリケーション起動後にhttp://localhost:3000へブラウザでアクセスをすることで、本番用のアプリケーションの実行を確認できます。