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を利用します。最初にインストールをしておきましょう。すでにインストール済みの方はここのステップはスキップして大丈夫です。
shell
npm install -g yarn
shell
npm install -g yarn
Next.jsをセットアップする
最初にcreate-next-app
コマンドでプロジェクトを作成します。TypeScriptをベースにしたプロジェクトを作成するために --example with-typescript
を指定します。random-cat
はリポジトリ名となる部分です。この部分は好きな名前でも構いませんが、本チュートリアルではrandom-cat
として話を進めます。
sh
yarn create next-app --example with-typescript random-cat
sh
yarn create next-app --example with-typescript random-cat
このコマンドを実行すると、random-cat
ディレクトリが作成されるので、そのディレクトリに移動してください。
sh
cd random-cat
sh
cd 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
開発サーバーを起動する
次のコマンドを実行して、開発サーバーを起動してください。
sh
yarn dev
sh
yarn dev
開発サーバーが起動したら、ターミナルに表示されているURLにブラウザでアクセスしてください。
不要なファイルを消す
チュートリアルを進める前に、ここでは使わないファイルを削除します。プロジェクトをシンプルな状態にして、作業を進めやすくしましょう。
sh
rm -rf pages utils interfaces components
sh
rm -rf pages utils interfaces components
ページコンポーネントディレクトリを作る
Next.jsでは、pages
ディレクトリ配下のディレクトリ構造がページのルーティングに対応します。たとえば、pages/users.tsxとファイルを作成すると、/users
へアクセスしたとき、それが実行されます。pages/index.tsxの場合は、/
へアクセスしたときに実行されます。
このpages
ディレクトリに置かれたコンポーネントのことを、Next.jsの用語でページコンポーネント(page component)と呼びます。
次のコマンドを実行してページコンポーネントを置くためのディレクトリを作成してください。
sh
mkdir pages
sh
mkdir pages
トップページのページコンポーネントを作る
次のコマンドを実行して、トップページのページコンポーネントを作成してください。
sh
touch pages/index.tsx
sh
touch pages/index.tsx
ページコンポーネントの内容は、次のようにします。このIndexPage
関数がページコンポーネントです。これは「猫画像予定地」が表示されるだけの単純なものです。
pages/index.tsxtsx
import {NextPage } from "next";constIndexPage :NextPage = () => {return <div >猫画像予定地</div >;};export defaultIndexPage ;
pages/index.tsxtsx
import {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
関数はアロー関数です。これを関数宣言に書き換えると次のようになります。
tsx
import {ReactElement } from "react";export default functionIndexPage ():ReactElement <any, any> | null {return <div >猫画像予定地</div >;}
tsx
import {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
の後ろに追加してください。
ts
constfetchImage = async () => {constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};
ts
constfetchImage = 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.tsxtsx
import {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.tsxtsx
import {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
型です。そのため、呼び出し側で存在しないプロパティを参照しても気づけずにバグが発生する危険性があります。
ts
constfetchImage = 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 ); // 存在しないプロパティを参照している});
ts
constfetchImage = 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>
として型注釈します。
ts
typeImage = {url : string;};constfetchImage = async ():Promise <Image > => {// ^^^^^^^^^^^^^^^^型注釈constres = awaitfetch ("https://api.thecatapi.com/v1/images/search");constimages = awaitres .json ();console .log (images );returnimages [0];};
ts
typeImage = {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レスポンスに存在しないプロパティを参照するコードを書いてしまっても、コンパイルエラーが発生することで問題に気がつけるようになります。
ts
fetchImage ().then ((image ) => {Property 'alt' does not exist on type 'Image'.2339Property 'alt' does not exist on type 'Image'.console .log (image .); // 存在しないプロパティを参照している alt });
ts
fetchImage ().then ((image ) => {Property 'alt' does not exist on type 'Image'.2339Property 'alt' does not exist on type 'Image'.console .log (image .); // 存在しないプロパティを参照している alt });
上のコードは、サーバーサイドを100%信頼するコードになっています。クライアントサイドが期待するデータ構造を、サーバーサイドが必ず返すことを前提としたコードなのです。しかし、サーバーサイドは本当にデータ期待する構造を返してくれているでしょうか?
より防衛的にするなら、クライアントサイドではサーバーのレスポンスをチェックするほうが望ましいでしょう。チェックの一例として次のような実装も考えられます。
ts
constfetchImage = 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";};
ts
constfetchImage = 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.tsxtsx
import {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.tsxtsx
import {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つの状態を定義します。
tsx
const [imageUrl ,setImageUrl ] =useState ("");const [loading ,setLoading ] =useState (true);
tsx
const [imageUrl ,setImageUrl ] =useState ("");const [loading ,setLoading ] =useState (true);
imageUrl
は画像のURLが代入される変数です。初期値は空文字列です。loading
はAPIを呼び出し中かどうかを管理する変数です。初期値は呼び出し中を意味するtrue
です。
❷ 次に、コンポーネントがマウントされたときに、APIから猫の画像情報を取得する処理を定義します。
tsx
useEffect (() => {fetchImage ().then ((newImage ) => {setImageUrl (newImage .url ); // 画像URLの状態を更新するsetLoading (false); // ローディング状態を更新する});}, []);
tsx
useEffect (() => {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
を使って記述していることに気がついた方もいるかもしれません。その方の中には、次のように非同期関数を渡す書き方にして、コードが読みやすくしたいと思う方もいるでしょう。
ts
useEffect (async () => {constnewImage = awaitfetchImage ();setImageUrl (newImage .url );setLoading (false);}, []);
ts
useEffect (async () => {constnewImage = awaitfetchImage ();setImageUrl (newImage .url );setLoading (false);}, []);
しかし、useEffect
には非同期関数を直接渡すことはできません。渡そうとすると、コンパイルエラーになります。
ts
Argument of type '() => Promise<void>' is not assignable to parameter of type 'EffectCallback'. Type 'Promise<void>' is not assignable to type 'void | Destructor'. Type 'Promise<void>' is not assignable to type 'Destructor'. Type 'Promise<void>' provides no match for the signature '(): void | { [UNDEFINED_VOID_ONLY]: never; }'.2345Argument of type '() => Promise<void>' is not assignable to parameter of type 'EffectCallback'. Type 'Promise<void>' is not assignable to type 'void | Destructor'. Type 'Promise<void>' is not assignable to type 'Destructor'. Type 'Promise<void>' provides no match for the signature '(): void | { [UNDEFINED_VOID_ONLY]: never; }'.useEffect (async () => {/* 中略 */}, []);
ts
Argument of type '() => Promise<void>' is not assignable to parameter of type 'EffectCallback'. Type 'Promise<void>' is not assignable to type 'void | Destructor'. Type 'Promise<void>' is not assignable to type 'Destructor'. Type 'Promise<void>' provides no match for the signature '(): void | { [UNDEFINED_VOID_ONLY]: never; }'.2345Argument of type '() => Promise<void>' is not assignable to parameter of type 'EffectCallback'. Type 'Promise<void>' is not assignable to type 'void | Destructor'. Type 'Promise<void>' is not assignable to type 'Destructor'. Type 'Promise<void>' provides no match for the signature '(): void | { [UNDEFINED_VOID_ONLY]: never; }'.useEffect (async () => {/* 中略 */}, []);
❸ 最後に画像を表示するロジックです。||
は論理和演算子で、loading
がfalse
のときに<img>
要素を表示します。
tsx
return <div >{loading || <img src ={imageUrl } />}</div >;
tsx
return <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.tsxtsx
import {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.tsxtsx
import {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はウェブアプリケーションのレンダリングをサーバーサイドで行う技術のことです。通常、クライアントサイドレンダリング(CSR)では、ブラウザがHTML、CSS、JavaScriptファイルをダウンロードして、JavaScriptを使用してページをレンダリングします。これに対して、SSRではサーバーがHTMLを生成し、ブラウザに送信します。
Next.jsでSSRを行うには、次のデータフェッチAPIの関数を使います。
getServerSideProps
getStaticProps
getInitialProps
これらの関数を使うことで、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.tsxtsx
import {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.tsxtsx
import {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.tsxtsx
import {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.tsxtsx
import {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
を実行して本番用のアプリケーションを実行してみましょう。
sh
yarn build && yarn start
sh
yarn build && yarn start
アプリケーション起動後にhttp://localhost:3000へブラウザでアクセスをすることで、本番用のアプリケーションの実行を確認できます。