LoginSignup
18
18

More than 1 year has passed since last update.

【React / TypeScript】axios と QiitaAPI を使ったサンプルアプリ

Last updated at Posted at 2021-09-04

はじめに

検索キーワードからQiitaAPIを叩いて検索結果を一覧表示するアプリを作ってみた。

QiitaAPIで利用したものは、「投稿の一覧を作成日時の降順で取得できる」こちらのAPI /api/v2/items

使用技術

ざっくり

  • Next.js
  • TypeScript
  • axios
  • Tailwind CSS

一応このプロジェクトのソースコードっす!!↓↓
https://github.com/TomoyukiMatsuda/React-pra/tree/axios-qiitaapi/next-sample

おまけ

後日、このアプリを拡張したものを作成しました。記事にもしていますのでよろしければこちらもご覧ください。
【React / TypeScript】Recoil での状態管理サンプルアプリ

こんな感じのものを作りました

フォームに検索ワードを入力して、検索キーワードに該当するQiita記事を取得して一覧表示するアプリ。

qiitaapi_axios.gif

ローディング中

スクリーンショット 2021-09-04 8.59.21.png

エラー時

画像はオフラインの時のエラーメッセージ

スクリーンショット 2021-09-04 8.58.27.png

検索結果0件

スクリーンショット 2021-09-04 9.00.11.png

ディレクトリ構成

ディレクトリ構成はこんな感じ
スクリーンショット 2021-09-04 9.04.03.png

各ファイルのコード

_app.tsx

_app.tsx
import 'tailwindcss/tailwind.css'
import type { AppProps } from 'next/app'

// 特に何もしていない
function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}
export default MyApp

ApiClient(lib/apiClient.tsx)

apiClientにはaxiosを利用。

axios.create()でデフォルトの設定をしておく

  • baseURL:APIを叩くときに付与する url の前に付加される url を設定
  • responseType:レスポンスの形式を設定( arraybuffer, document, json, text, streamが指定できるっぽい、ほとんどの場合jsonを指定すると思いますが、、 )
  • headers: HTTPヘッダーを指定
  • params: 送信するリクエストパラメータを指定。今回はAPI叩くタイミングhooks/useListQiitaArticles.tsの中で設定してます。(今回叩くAPI/api/v2/itemsにはpage per_page query を付与できる)

他にも設定がたくさんある。こちらに詳しくのってます。

lib/apiClient.ts
import axios from "axios";

export const apiClient = axios.create({
  baseURL: "https://qiita.com/api/v2",
  responseType: "json",
  headers: {
    "Content-Type": "application/json",
  },
});

型定義(types/QiitaItem.tsx)

2つの型を定義

  • アプリ内で利用するプロパティ(ID、タイトル、LGTM数、ユーザー )のみの型QiitaItem
  • APIのレスポンスを受け取るための型QiitaItemResponse
types/QiitaItem.tsx
// アプリ内で利用するためのQiita記事 型定義
export type QiitaItem = Pick<
  QiitaItemResponse,
  "id" | "title" | "likes_count" | "user"
>; // Pick で利用したいプロパティのみを抽出

// Qiita Api レスポンスの型定義
export interface QiitaItemResponse {
  rendered_body: string;
  body: string;
  coediting: boolean;
  comments_count: number;
  created_at: string;
  group: {
    created_at: string;
    description: string;
    name: string;
    private: boolean;
    updated_at: string;
    url_name: string;
  };
  id: string;
  likes_count: number;
  private: boolean;
  reactions_count: number;
  tags: [
    {
      name: string;
      versions: string[];
    }
  ];
  title: string;
  updated_at: string;
  url: string;
  user: {
    description: string;
    facebook_id: string;
    followees_count: number;
    followers_count: number;
    github_login_name: string;
    id: string;
    items_count: number;
    linkedin_id: string;
    location: string;
    name: string;
    organization: string;
    permanent_id: number;
    profile_image_url: string;
    team_only: boolean;
    twitter_screen_name: string;
    website_url: string;
  };
  page_views_count: number;
  team_membership: {
    name: string;
  };
}

API処理のhooks(hooks/useListQiitaArticles.ts)

API叩くフェッチ処理のカスタムフックを作る。
以下をの5つを返す。

  • articles: 取得したQiita記事リストのデータ
  • searchWord: 検索クエリーテキスト
  • errorMessage: エラー時のレスポンスメッセージ
  • isLoading: API叩いているとき、ローディング中かどうかを判断するフラグ
  • fetchArticles: APIを叩く関数
hooks/useListQiitaArticles.ts
import { Dispatch, FormEvent, SetStateAction, useState } from "react";
import { apiClient } from "../lib/apiClient";
import { QiitaItem, QiitaItemResponse } from "../types/QiitaItem";

export const useListQiitaArticles = () => {
  const [articles, setArticles] = useState<Array<QiitaItem>>([]);
  const [searchWord, setSearchWord] = useState("");
  const [errorMessage, setErrorMessage] = useState<string>("");
  const [isLoading, setIsLoading] = useState(false);

  const fetchArticles = async (
    e: FormEvent<HTMLFormElement>,
    formText: string,
    setFormText: Dispatch<SetStateAction<string>>
  ) => {
    e.preventDefault(); // フォームのデフォルトの動作(リロード)を防ぐ
    setIsLoading(true); // ローディング開始
    setErrorMessage(""); // エラーメッセージを初期化

    // await を付与することでこの処理が終わらない限り次の処理に進まないようになる(これがないとローディング処理などが先に呼ばれてしまう)
    await apiClient
      .get<Array<QiitaItemResponse>>("/items", {
        params: {
          query: formText, // フォーム入力を検索ワードとして設定
          per_page: 25, // 25件 の記事を取得するように設定
        },
      })
      .then((response) => {
        // レスポンスから利用したい要素を QiitaItem 型 の配列でセット
        setArticles(
          response.data.map<QiitaItem>((d) => {
            return {
              id: d.id,
              title: d.title,
              likes_count: d.likes_count,
              user: d.user,
            };
          })
        );

        // 検索キーワードをレスポンスから取得してセット
        setSearchWord(response.config.params.query);
      })
      .catch((error) => {
        // エラーメッセージをセット
        setErrorMessage(error.message);
      });

    setIsLoading(false); // ローディング終了
    setFormText(""); // 最終的にフォーム入力を空にする
  };

  return {
    articles,
    searchWord,
    errorMessage,
    isLoading,
    fetchArticles,
  };
};

axiosでAPIを叩いているところを解説すると
書き方としてはこんな感じになっていて

apiClient.httpメソッド指定<レスポンスの型>(baseUrlに続くurl, 設定)
  .then((成功レスポンス) => {
    // 成功
  })
  .catch((エラーレスポンス) => {
    // エラー
  })

今回は
HTTPメソッドがGET
この↓↓urlでリクエストを送信している
https://qiita.com/api/v2/items?per_page=25&query=react

※検索ワード(query: formText)が「react」の場合

ページ (pages/index.tsx)

"/"ページpages/index.tsx
フォームコンポーネント(SearchForm)の下に記事検索結果を表示するコンポーネント(ArticleList)を配置している。

pages/index.tsx
import React from "react";
import { useListQiitaArticles } from "../hooks/useListQiitaArticles";
import { ArticleList } from "../components/ArticleList";
import { SearchForm } from "../components/SearchForm";

const Home: React.VFC = () => {
  const { articles, searchWord, errorMessage, isLoading, fetchArticles } =
    useListQiitaArticles();

  return (
    <div className="max-w-5xl my-0 mx-auto px-12">
      <SearchForm fetchArticles={fetchArticles} />
      <ArticleList
        articles={articles}
        searchWord={searchWord}
        errorMessage={errorMessage}
        isLoading={isLoading}
      />
    </div>
  );
};

export default Home;

コンポーネント

検索フォームコンポーネント(components/SearchForm/index.tsx)

ポイント

  • Propsでカスタムフックで定義したAPIフェッチ関数を受け取って、Submitイベント(検索ボタンクリック)でAPIを叩く
  • フォーム入力有無でボタンカラーとボタンアクティブ・非アクティブを制御
components/SearchForm/index.tsx
import React, { Dispatch, FormEvent, SetStateAction, useState } from "react";

interface Props {
  fetchArticles: (
    e: FormEvent<HTMLFormElement>,
    formText: string,
    setFormText: Dispatch<SetStateAction<string>>
  ) => void;
}

export const SearchForm: React.VFC<Props> = (props) => {
  const [formText, setFormText] = useState<string>("");
  const buttonColor = formText
    ? "bg-blue-700 hover:bg-blue-500" // フォーム入力有:ブルー
    : "bg-gray-300"; // フォーム入力無:グレー
  return (
    <form
      className="mt-12 mb-6"
      onSubmit={(e) => props.fetchArticles(e, formText, setFormText)}
    >
      <label className="block text-gray-700 text-lg font-bold mb-2">
        Qiita 記事 検索キーワードを入力
      </label>
      <input
        className="shadow appearance-none border rounded w-full py-2 px-3 mb-4 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
        placeholder="例:React"
        value={formText}
        onChange={(e) => setFormText(e.target.value)}
      />
      <button
        className={`${buttonColor} text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline`}
        type="submit"
        disabled={!formText}
      >
        検索
      </button>
    </form>
  );
};

Qiita記事リストコンポーネント(components/Articlelist/index.tsx)

ポイント

  • Props以下を受け取り、4パターン(ローディング、エラー、検索結果なし、検索結果表示)でレンダリングする要素をハンドリング
    • articles: フェッチしたQiita記事リスト
    • searchWord: 検索ワード
    • errorMessage: エラーメッセージ
    • isLoading: ローディングフラグ
  • Qiita記事は下位のArticleItemコンポーネントに渡す
components/Articlelist/index.tsx
import React from "react";
import { QiitaItem } from "../../types/QiitaItem";
import { ArticleItem } from "./ArticleItem";

interface Props {
  articles: Array<QiitaItem>;
  searchWord: string;
  errorMessage: string;
  isLoading: boolean;
}

export const ArticleList: React.VFC<Props> = (props) => {
  // ローディング中
  if (props.isLoading) {
    return (
      <p className="mb-2 p-8 bg-yellow-100 rounded-lg">ローディング.......</p>
    );
  }
  // エラー(API失敗)
  if (props.errorMessage) {
    return (
      <p className="mb-2 p-8 bg-red-100 rounded-lg">{props.errorMessage}</p>
    );
  }
  // 成功したものの、検索結果0件
  if (props.searchWord && props.articles?.length === 0) {
    return (
      <p className="mb-2 p-8 bg-green-100 rounded-lg">
        検索ワード
        <span className="font-bold border-b-2 border-black">
          {props.searchWord}
        </span>
        に該当なーーーし!!
      </p>
    );
  }

  return (
    <>
      {props.searchWord && (
        <p className="mb-4 text-xl">
          検索キーワード
          <span className="ml-2 font-bold text-blue-700 border-b-2 border-blue-700">
            {props.searchWord}
          </span>
        </p>
      )}
      {props.articles?.map((article) => {
        return <ArticleItem key={article.id} article={article} />;
      })}
    </>
  );
};

Qiita記事コンポーネント(components/Articlelist/ArticleItem/index.tsx)

Propsで受け取ったデータを表示するだけ

components/Articlelist/ArticleItem/index.tsx
import React from "react";
import { QiitaItem } from "../../../types/QiitaItem";

interface Props {
  article: QiitaItem;
}

export const ArticleItem: React.VFC<Props> = ({ article }) => {
  return (
    <div className="mb-3 py-2 px-8 bg-blue-100 rounded-lg shadow">
      <p className="text-center font-bold  text-blue-800 mb-4 border-b-2 border-blue-800">
        {article.title}
      </p>
      <p className="text-blue-700">LGTM 👍:{article.likes_count}</p>
      <p className="text-blue-700">ユーザー{article.user.name}</p>
    </div>
  );
};

最後に

今実務で入っている案件がaxiosを利用しているので今回はaxiosを使ってみました。
SWRとかを使えばエラーやローディングのハンドリングがもっと簡単になってきそうなので、SWRでもAPI叩く何か作ろうと思ってます。

18
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
18