MENU

Next.jsで、ただ犬の画像を表示するWebアプリを作成する

今回は、Next.jsを使用して、webアプリを作成しようと思います。
自分への備忘録も兼ねて、まとめていこうと思います。

CONTENTS

完成ソース

gitにソースを上げているので、よかったら参照ください。

GitHub
GitHub - eddymafin/doggy-app Contribute to eddymafin/doggy-app development by creating an account on GitHub.

プロジェクトファイルを作成

プロジェクトを作成したいディレクトリに移動して、下記のコマンドを打ちます。

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です笑

あわせて読みたい
Dog API Dog CEO's Dog API Documentation. Over 20,000 images of dogs programmaticaly accessible by over 120 breeds. Image supplied by the Stanford Dogs Dataset.

これを使って何か簡単なアプリを作ってみようと思います。

先に完成してアプリは、こちら。ただ犬の画像が表示されるだけのアプリです笑

あわせて読みたい
Create Next App Generated by create next app

作成したかったアプリは下記のようなものです。

  • 選択した犬種に応じて、画像を表示する
  • 20件の画像を表示して、ボタンをクリックすると他の画像をシャッフルで表示
  • ページネーション

追加でやりたいのは、気に入った画像をお気に入り登録とかする機能なんてあったらいいなと思ってます。

APIからデータを取得する

そもそもAPIからデータを取得ってどうするんだろう。。よくfetchとかaxiosとか聞くけど、よくわかっていませんでした。

fetchよく聞きますが、外部からデータを取得してくることなんですね。下記の記事がとても参考になりました。

向日市 ホームページ制作 | 株式会...
JavaScriptのfetch()の基本的な使い方とAPIの仕組みを解説【エンドポイント、クエリ、HTTP、ヘッダー、ボデ... JavaScriptを使ったAPI処理は色んな方法があり最近ではライブラリを使うことが当たり前になりました。しかし初学者の方からすると基本を知らない状態でライブラリを見ても...

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さんに説明していただきました。

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とかもおもしろうそう。

Qiita
【随時更新】一風変わったWeb APIをまとめてみた - Qiita 用途がイマイチよくわからない。風変わりなWeb APIをまとめてみました。※一部リンク切れやサ終で動かないものもありますがご了承ください。ジョーク系Official Joke APIhtt...

追記:axiosを使って、データを取得する

以前は、データの取得をfech関数を使用していたのですが、axiosというライブラリのほうがよく使用されているので、そちらに修正しました。

axiosのほうが、記述が少しシンプルになりそうです。

Qiita
axiosとfetchの違いについて - Qiita axiosとfetchは、どちらもJavaScriptでHTTPリクエストを行うための方法です。axiosとfetchの違いインターフェースfetchブラウザの標準APIであり、グローバル関…

修正したコードはこちら。

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の双方向のフローと比べて、やりづらくしているのでしょうか。。

下記の記事を参考にさせていただきました。

ICS MEDIA
ベストな手法は? Reactのステート管理方法まとめ - ICS MEDIA Reactでのシングルページアプリケーションを作成していると、必ず意識しなくてはいけないのが状態管理です。HooksAPIの登場により、アプリケーションの状態管理方法にも選...

子コンポーネント: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にソースを上げているので、よかったら参照ください。

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
CONTENTS