今回は、Next.jsを使用して、webアプリを作成しようと思います。
自分への備忘録も兼ねて、まとめていこうと思います。
完成ソース
gitにソースを上げているので、よかったら参照ください。
プロジェクトファイルを作成
プロジェクトを作成したいディレクトリに移動して、下記のコマンドを打ちます。
npx create-next-app@latest
プロジェクト名を入力して、案内にそって、YesかNoを選んでいきます。
今回は、TypeScript、Tailwind Cssを使用したいので、わたしはYesとしてます。
import aliasについては、デフォルトでNoが選択されてますが、わたしは使用したかったので、Yesとしました。
create-next-app@15.0.0
Ok to proceed? (y) y
What is your project named? › next-doggy
Vscodeでプロジェクトファイルを開いて、下記のコマンドを打って、サーバーを立ち上げる。
npm run dev
ローカルサーバーが立ち上がり、アクセスすると、下記のように初期画面が表示されます。
犬の画像が表示されるWeb API
何かおもしろいwebAPIないかなと探していると、これを見つけました!ただ犬の画像が変えてくるAPIです笑
これを使って何か簡単なアプリを作ってみようと思います。
先に完成してアプリは、こちら。ただ犬の画像が表示されるだけのアプリです笑
作成したかったアプリは下記のようなものです。
- 選択した犬種に応じて、画像を表示する
20件の画像を表示して、ボタンをクリックすると他の画像をシャッフルで表示- ページネーション
追加でやりたいのは、気に入った画像をお気に入り登録とかする機能なんてあったらいいなと思ってます。
APIからデータを取得する
そもそもAPIからデータを取得ってどうするんだろう。。よくfetchとかaxiosとか聞くけど、よくわかっていませんでした。
fetchよく聞きますが、外部からデータを取得してくることなんですね。下記の記事がとても参考になりました。
fetch関数を利用して、APIからjsonデータを取得
まず、データを取得します。コンソールにすべてのデータをまず出力させます。
ドキュメントによると、messageのObjectに犬種のデータが格納されているので、そちらをbreedsという変数に格納します。
async function fetchData() {
try {
const res = await fetch("https://dog.ceo/api/breeds/list/all");
if (!res.ok) {
throw new Error("リストの取得に失敗しました");
}
const data = await res.json();
console.log(data);
const breeds = data.message;
console.log(breeds);
} catch (error) {
console.error("エラーです:", error);
}
}
fetchData();
これだけだと、Next.jsでは、レンダリングがされてしまうので、無限ループが起こります。
なので、UseEffectを使用しないといけないのですが、うまく説明できないので、chatGDPさんに説明していただきました。
useEffect
がない場合、Next.js(React)コンポーネントはレンダリングされるたびに、関数内部のすべてのコードが実行されます。特にfetch
のようなサーバーリクエストが含まれていると、レンダリングごとにリクエストが発行され、結果的に無限ループのようにfetch
が繰り返されてしまいます。
初回の一回だけ、dataを取得したいので、UseEffectを使用します。依存配列(第二引数)が、空の場合は、デフォルトで初回のレンダリングのときというようになるようです。
また、こちらの内容をuseStateに格納させます。
typescriptを入れていない人は、空の配列を初期値にいれてください。
"use client";
import { useState, useEffect } from "react";
export default function Page() {
const [categories, setCategories] = useState<string[]>([]); // カテゴリーを管理するための状態
useEffect(() => {
async function fetchData() {
try {
const res = await fetch("https://dog.ceo/api/breeds/list/all");
if (!res.ok) {
throw new Error("リストの取得に失敗しました");
}
const data = await res.json();
console.log(data);
const breeds = data.message;
console.log(breeds);
const categoriesList = Object.keys(breeds);
setCategories(categoriesList);
} catch (error) {
console.error("エラーです:", error);
}
}
fetchData();
}, []);
}
フロント側を構築
これで、犬種のデータが取得できたので、これをselectのオプションタグにまずはループさせます。
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
export default function Page() {
const [categories, setCategories] = useState<string[]>([]); // カテゴリーを管理するための状態
useEffect(() => {
async function fetchData() {
try {
const res = await fetch("https://dog.ceo/api/breeds/list/all");
if (!res.ok) {
throw new Error("リストの取得に失敗しました");
}
const data = await res.json();
console.log(data);
const breeds = data.message;
console.log(breeds);
const categoriesList = Object.keys(breeds);
setCategories(categoriesList);
} catch (error) {
console.error("エラーです:", error);
}
}
fetchData();
}, []);
return (
<div className="flex flex-col justify-center items-center gap-3 bg-green-100 min-h-screen bg-[url('/dog.svg')] bg-repeat">
<div className="py-20 max-w-[90%] md:max-w-[80%] w-full flex flex-col gap-3">
<p className="text-center text-lg">Choose your doggy </p>
<select className="border p-2">
{categories.map((post, index) => (
<option value={post} key={index}>
{post}
</option>
))}
</select>
</div>
</div>
);
}
これで、犬種の選択リストがセレクトタグに表示されるようになります。
選択した犬種の画像を表示
次に、onChangeイベントで、選択した犬の犬種ごとの画像をfetchして、配列の最初の20件だけ表示させるようにします。
ドキュメントによると、下記からデータをfetchすれば犬種ごとの画像が取得できそうです。
https://dog.ceo/api/breed/犬種のカテゴリ/images
handleSelectChangeという関数を作成して、selectのonChangeイベントのときに、発火するようにします。
selectValに選択したvalueを取得して、格納。さらに犬種ごとの画像をfetchして、画像のurlの配列をlistsに格納しています。
このあたりは、useStateの概念を覚えていないといつも混乱するので、要注意!
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
export default function Page() {
const [categories, setCategories] = useState<string[]>([]); // カテゴリーを管理するための状態
const [selectVal, setSelectedVal] = useState("");
const [lists, setList] = useState([]);
useEffect(() => {
async function fetchData() {
try {
const res = await fetch("https://dog.ceo/api/breeds/list/all");
if (!res.ok) {
throw new Error("リストの取得に失敗しました");
}
const data = await res.json();
// console.log(data);
const breeds = data.message;
// console.log(data);
const categoriesList = Object.keys(breeds);
setCategories(categoriesList);
} catch (error) {
console.error("エラーです:", error);
}
}
fetchData();
}, []);
const handleSelectChange = async (
e: React.ChangeEvent<HTMLSelectElement> //これはtypescriptの記述なので、入れていない人はe:でOK!
) => {
const category = e.target.value;
setSelectedVal(category);
if (category) {
const response = await fetch(
`https://dog.ceo/api/breed/${category}/images`
);
const dogData = await response.json();
const array = dogData.message;
setList(array.slice(0, 20));
}
};
return (
<div className="flex flex-col justify-center items-center gap-3 bg-green-100 min-h-screen bg-[url('/dog.svg')] bg-repeat">
<div className="py-20 max-w-[90%] md:max-w-[80%] w-full flex flex-col gap-3">
<p className="text-center text-lg">Choose your doggy </p>
<select className="border p-2" onChange={handleSelectChange}>
{categories.map((post, index) => (
<option value={post} key={index}>
{post}
</option>
))}
</select>
<p className="text-center text-2xl font-bold mb-2">
Your choice doggy<span className="ml-2 ">{selectVal}</span>
</p>
{selectVal && (
<div className="flex flex-col gap-3 mt-5">
<p className="text-center font-bold text-lg">
Did you find your favorite doggy?
</p>
</div>
)}
<ul className="grid grid-cols-2 md:grid-cols-4 gap-2 mt-4">
{lists.map((list, index) => (
<li
key={index}
className="flex items-center justify-center md:max-w-[18.75rem] border-none overflow-hidden bg-white"
>
<Image
src={list}
alt={`Dog ${index}`}
className="w-auto object-contain h-[9.375rem]"
width={150}
height={150}
loading="lazy"
/>
</li>
))}
</ul>
</div>
</div>
);
}
一点、画像の最適化のために、next.jsのImageタグを使用しているのですが、これが外部ファイルだとエラーがでるので、下記のようにconfigファイルに追加が必要です。詳細は、公式のドキュメント参考ください。
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.dog.ceo",
port: "",
},
],
},
};
export default nextConfig;
ボタンクリックで、画像がシャッフルするようにする:ページネーションに変更
最後に、shuffleButtonという関数で、ボタンをクリックすると、listsに格納した画像のurlの配列をシャッフルにしました。これで完成!
↑改修して、ページネーションに変更しました。こちらのほうがすべての犬の画像を見れるなと思ったので。
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
export default function Page() {
const [categories, setCategories] = useState<string[]>([]); // カテゴリーを管理するための状態
const [selectVal, setSelectedVal] = useState("");
const [lists, setList] = useState([]);
useEffect(() => {
async function fetchData() {
try {
const res = await fetch("https://dog.ceo/api/breeds/list/all");
if (!res.ok) {
throw new Error("リストの取得に失敗しました");
}
const data = await res.json();
// console.log(data);
const breeds = data.message;
// console.log(data);
const categoriesList = Object.keys(breeds);
setCategories(categoriesList);
} catch (error) {
console.error("エラーです:", error);
}
}
fetchData();
}, []);
const handleSelectChange = async (
e: React.ChangeEvent<HTMLSelectElement>
) => {
const category = e.target.value;
setSelectedVal(category);
if (category) {
const response = await fetch(
`https://dog.ceo/api/breed/${category}/images`
);
const dogData = await response.json();
const array = dogData.message;
// console.log(array);
setList(array.slice(0, 20));
}
};
// 配列をシャッフルする関数
const shuffleButton = () => {
const shuffledList = [...lists].sort(() => Math.random() - 0.5);
setList(shuffledList);
};
return (
<div className="flex flex-col justify-center items-center gap-3 bg-green-100 min-h-screen bg-[url('/dog.svg')] bg-repeat">
<div className="py-20 max-w-[90%] md:max-w-[80%] w-full flex flex-col gap-3">
<p className="text-center text-lg">Choose your doggy </p>
<select className="border p-2" onChange={handleSelectChange}>
{categories.map((post, index) => (
<option value={post} key={index}>
{post}
</option>
))}
</select>
<p className="text-center text-2xl font-bold mb-2">
Your choice doggy<span className="ml-2 ">{selectVal}</span>
</p>
{selectVal && (
<div className="flex flex-col gap-3 mt-5">
<p className="text-center font-bold text-lg">
Did you find your favorite doggy?
</p>
<button
onClick={shuffleButton}
className="bg-green-900 text-white p-3 rounded-md w-full md:max-w-[21.875rem] mx-auto"
>
Want see more pics?
</button>
</div>
)}
<ul className="grid grid-cols-2 md:grid-cols-4 gap-2 mt-4">
{lists.map((list, index) => (
<li
key={index}
className="flex items-center justify-center md:max-w-[18.75rem] border-none overflow-hidden bg-white"
>
<Image
src={list}
alt={`Dog ${index}`}
className="w-auto object-contain h-[9.375rem]"
width={150}
height={150}
loading="lazy"
/>
</li>
))}
</ul>
</div>
</div>
);
}
いかがでしょうか?今までアプリ制作なんて難しそうと思ってたのですが、わたしでも作成することができました!
APIとかも難しそうだなと思っていたのですが、これを機会に簡単なアプリを作ってみようかなと思いました。
ちなみに今回の犬のAPIは、下記にまとめていただいている中から見つけました。他にもおもしろいAPIがあるので、また時間ができたら、見てみたいな。Harry Potter APIとかもおもしろうそう。
追記:axiosを使って、データを取得する
以前は、データの取得をfech関数を使用していたのですが、axiosというライブラリのほうがよく使用されているので、そちらに修正しました。
axiosのほうが、記述が少しシンプルになりそうです。
修正したコードはこちら。
getをしたときに、jsonに変換してくれるようで、fetch関数を使用したときよりも、シンプルな記述になりました。
// axiosの記述に変更
const fetchData = async () => {
const apiUrl = "https://dog.ceo/api/breeds/list/all";
const result = await axios
.get(apiUrl)
.then((response) => {
return response.data;
// console.log(response.data);
})
.catch((error) => {
console.error("エラーです:", error);
});
const breeds = result.message;
// console.log(breeds);
const categoriesList = Object.keys(breeds);
setCategories(categoriesList);
};
追記:ページネーションをコンポーネント化する
ページネーションを画像の上と下に、表示させたかったので、コンポーネント化しました。
子から親へのpropsへの渡し方が、苦労しました。。。vueより難解な気がします。。
確かReactの基本設計が単一方向のフローなので、vueの双方向のフローと比べて、やりづらくしているのでしょうか。。
下記の記事を参考にさせていただきました。
子コンポーネント:Paginationでの記述は下記。Typescriptを入れているので、interfaceでpropsの型を宣言して、渡しています。
import { Dispatch, SetStateAction } from "react";
interface Props {
lists: Array<string[]>;
currentNumber: number; // 現在のページ番号を受け取る
setCurrentNumber: Dispatch<SetStateAction<number>>;
}
export default function Pagination({
lists,
currentNumber,
setCurrentNumber,
}: Props) {
const buttonCommonClass = `font-bold text-lg disabled:opacity-30`;
console.log(lists);
const changePagination = (e: React.MouseEvent<HTMLButtonElement>) => {
// number型に変更
return setCurrentNumber(Number(e.currentTarget.value));
};
const prevButton = () => {
if (currentNumber === 0) return;
setCurrentNumber((currentNumber) => {
return currentNumber - 1;
});
};
const nextButton = () => {
if (currentNumber === lists.length - 1) return;
setCurrentNumber((currentNumber) => {
return currentNumber + 1;
});
};
return (
<div className="grid grid-cols-[1fr,auto,1fr] gap-3 justify-center mt-5 items-center">
<button
className={buttonCommonClass}
disabled={currentNumber === 0}
onClick={prevButton}
>
Prev
</button>
<ul className="flex gap-3 justify-center flex-wrap items-center">
{lists.map((listItem, index) => {
return (
<li key={index}>
<button
className={`${
currentNumber === index ? "bg-green-600" : "bg-green-900"
} w-[2.5rem] h-[2.5rem] md:w-[3.125rem] md:h-[3.125rem] flex items-center justify-center rounded-full text-white md:hover:bg-green-600`}
value={index}
onClick={changePagination}
>
{index + 1}
</button>
</li>
);
})}
</ul>
<button
className={buttonCommonClass}
disabled={currentNumber === lists.length - 1}
onClick={nextButton}
>
Next
</button>
</div>
);
}
親コンポーネント:page.tsxでの記述は、こちら。
<Pagination
lists={lists}
currentNumber={currentPage}
setCurrentNumber={setCurrentPage}
/>
詳細は、gitにソースを上げているので、よかったら参照ください。