はじめに
検索キーワードから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記事を取得して一覧表示するアプリ。
ローディング中
エラー時
画像はオフラインの時のエラーメッセージ
検索結果0件
ディレクトリ構成
各ファイルのコード
_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
を付与できる)
他にも設定がたくさんある。こちらに詳しくのってます。
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
// アプリ内で利用するための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を叩く関数
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
)を配置している。
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を叩く - フォーム入力有無でボタンカラーとボタンアクティブ・非アクティブを制御
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
コンポーネントに渡す
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
で受け取ったデータを表示するだけ
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叩く何か作ろうと思ってます。