はじめに
以前QiitaAPI
を使ったこんなアプリ(↓↓下記記事)を作成しました。そのアプリをRecoil
を使って少し拡張してみました。
【React / TypeScript】axios と QiitaAPI を使ったサンプルアプリ
ディレクトリ構成とか諸々、前の記事の状態から原型とどめてない状態になった。笑
だいぶこの記事も冗長になってしまいましたが、お手柔らかにお願いします。
何かあれば気軽にコメントいただければ非常に嬉しいです!
Recoil
Recoilの導入方法などはこちらから。
https://recoiljs.org/docs/introduction/getting-started
またRecoil
の使い方について簡単にまとめた記事でも書けたらいいなー、と思っております。
使用技術
ざっくりとこのような感じ
- Next.js
- TypeScript
- Recoil
- axios
- Tailwind CSS
一応このプロジェクトのソースコード↓↓
https://github.com/TomoyukiMatsuda/React-pra/tree/next-sample/axios-recoil/next-sample
作ったアプリ
アプリ概要
できること
- Qiita記事を検索キーワードで検索して表示する
- 検索履歴キーワードが残り、表示される
- 検索履歴キーワードごとのQiita記事検索結果を確認できる
- 検索結果のQiita記事をLGTM数で絞り込みができる
2ページの構成
- Qiita記事検索ページ
- Qiita記事絞り込みページ(LGTM数に応じて)
Gif動画
画質悪くてすみません。
ディレクトリ構成
各ファイルのコード
型定義(types/QiitaItem.ts)
(正直「レスポンスの型」と「アプリ内で利用するための型」分ける必要なかったかもしれない、)
// アプリ内で利用するためのQiita記事型定義
export type QiitaItem = Pick<
QiitaItemResponse,
"id" | "title" | "likes_count" | "user"
>;
// 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;
};
}
ApiClient(lib/apiClient.tsx)
apiClient
にはaxios
を利用。ここで基本的な設定をしておく。
axios:こちらに詳しく載っている
-
baseURL:
APIを叩くときの基本となる 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",
headers: {
"Content-Type": "application/json",
},
responseType: "json",
});
グローバルなステート管理(grobalStates/...)
ここでRecoil
を使う。
今回は3つのAtom
と2つのSelector
を用意。
Atom
-
articleListAtom.ts:
検索キーワードごとのQiita記事リストの状態を保持する -
searchWordsAtom.ts:
検索キーワード履歴の状態を保持する -
minLikesCountAtom.ts:
Qiita記事絞り込みに利用するLGTM数の状態を保持する
Selector
-
articleListSelector.ts:
Qiita記事リストを加工して返す(get
)、更新する(set
)セレクタ--
get:
検索キーワードごとのQiita記事リストをLGTM数に応じて動的に返す -
set:
検索キーワードごとのQiita記事リストをAtom
にセットする(更新する)
-
-
searchWordsSelector.ts:
検索キーワード履歴を加工して返す(get
)、更新する(set
)セレクタ--
get:
ここでは検索キーワードのリストをAtom
からそのまま返す(特に加工処理せず) -
set:
「検索キーワードが重複しない」かつ「新しいキーワードが配列の先頭に来るように」Atom
にセット(更新する)
-
Atom / Selector を管理するキーファイル(grobalStates/recoilKeys.ts)
Atom
/Selector
のキーを一元管理。
// Atomを管理するキー
export enum AtomKeys {
ARTICLE_LIST_STATE = "articleListState",
SEARCH_WORDS_STATE = "searchWordsState",
MIN_LIKES_COUNT_STATE = "minLikesCountState",
}
// Selectorを管理するキー
export enum SelectorKeys {
ARTICLE_LIST_SELECTOR = "articleListSelector",
SEARCH_WORDS_SELECTOR = "searchWordsSelector",
}
Qiita記事リストのステート管理
articleListAtom.ts
articleListAtom.ts:
検索キーワードごとのQiita記事リストの状態を保持する
atomFamily
を使うことで、state
を利用するときに引数を受け取ることができる。
そして、その引数からユニークなキーを自動的に生成してくれる。
同じatomFamily
を用いて同じ型を持つ別の値の取得・更新ができる。
// atomFamily<stateの型, 引数の型>
atomFamily<QiitaItem[], string>
今回は利用するとき(selector
でget
orset
するとき)に「検索キーワード」を引数に渡すことで 「検索キーワード」ごとのstate
管理を可能にする。
import { atomFamily } from "recoil";
import { QiitaItem } from "../../types/QiitaItem";
import { AtomKeys } from "../recoilKeys";
// searchArticleList(Qiita記事リスト)のAtom(データストア)
export const articleListState = atomFamily<QiitaItem[], string>({
key: AtomKeys.ARTICLE_LIST_STATE,
default: [],
});
articleListSelector.ts
-
articleListSelector.ts:
Qiita記事リストを加工して返す(get
)、更新する(set
)セレクタ--
get:
検索キーワードごとのQiita記事リストをLGTM数に応じて動的に返す -
set:
検索キーワードごとのQiita記事リストをAtom
にセットする(更新する)
-
// selectorFamily<stateの型, 引数の型>
selectorFamily<QiitaItem[], string>
今回はselector
を利用するときに
「検索キーワード」を引数に渡す→それをatomFamily
に渡す(articleListState(id)
)ことで「検索キーワード」ごとのstate
管理を可能にしている。
import { selectorFamily } from "recoil";
import { QiitaItem } from "../../types/QiitaItem";
import { articleListState } from "../atoms/articleListAtom";
import { minLikesCountState } from "../atoms/minLikesCountAtom";
import { SelectorKeys } from "../recoilKeys";
// searchArticleList(Qiita記事リスト)のSelector(データ加工所)だよという宣言
export const articleListSelector = selectorFamily<QiitaItem[], string>({
key: SelectorKeys.ARTICLE_LIST_SELECTOR,
get:
// 引数 id には検索キーワードを渡す
(id) =>
({ get }) => {
// get(articleListState(id)) で検索キーワードに紐づくstate(Qiita記事リスト)を取得
return get(articleListState(id)).filter((item) => {
// LGTM数でフィルタリングした値を返す
return item.likes_count >= get(minLikesCountState);
});
},
set:
// 引数 id には検索キーワードを渡す
(id) =>
({ set }, newValue) => {
// articleListState(id)を第一引数に指定することで、idをユニークなキーとしたstateをセットする
set(articleListState(id), newValue);
},
});
検索キーワードのステート管理
searchWordsAtom.ts
searchWordsAtom.ts:
検索キーワード履歴の状態を保持する
こちらにはatomFamily
ではなくatom
を使う。
// atom<stateの型>
atom<string[]>
import { atom } from "recoil";
import { AtomKeys } from "../recoilKeys";
// 「検索キーワード」のAtom(データストア)
export const searchWordsState = atom<string[]>({
key: AtomKeys.SEARCH_WORDS_STATE,
default: [],
});
searchWordsSelector.ts
-
searchWordsSelector.ts:
検索キーワード履歴を加工して返す(get
)、更新する(set
)セレクタ--
get:
ここでは検索キーワードのリストをAtom
からそのまま返す(特に加工処理せず) -
set:
「検索キーワードが重複しない」かつ「新しいキーワードが配列の先頭に来るように」Atom
にセット(更新する)
-
import { DefaultValue, selector } from "recoil";
import { searchWordsState } from "../atoms/searchWordsAtom";
import { SelectorKeys } from "../recoilKeys";
export const searchWordsSelector = selector<string[]>({
key: SelectorKeys.SEARCH_WORDS_SELECTOR,
get: ({ get }) => {
return get(searchWordsState);
},
set: ({ set }, newValue) => {
if (newValue instanceof DefaultValue) return; // 型がDefaultValueであれば return
// 重複を防いだ値をセットする
set(searchWordsState, (currVal) => {
return Array.from(new Set<string>([...newValue, ...currVal]));
});
},
});
LGTM数のステート管理
minLikesCountAtom.ts:
Qiita記事絞り込みに利用するLGTM数の状態を保持する
LGTM数のステート管理にはselector
は用意せず、atom
の値を直接参照する。
(※特にselector
にて値を加工する必要がないため)
import { atom } from "recoil";
import { AtomKeys } from "../recoilKeys";
// 絞り込みに利用する「最低LGTM数」のAtom(データストア)
export const minLikesCountState = atom<number>({
key: AtomKeys.MIN_LIKES_COUNT_STATE,
default: 0,
});
API処理のhooks(hooks/useListQiitaArticles.ts)
APIを叩くフェッチ処理のカスタムフック。
以下をの5つを返す。
-
articles:
取得したQiita記事リスト -
searchWord:
検索キーワード -
errorMessage:
エラーレスポンスメッセージ -
isLoading:
APIを叩いているときにローディング中かどうかを判断するフラグ -
fetchArticles:
API(/api/v2/items)を叩く関数
API叩く処理
今回は
HTTPメソッドがGETで
この↓↓urlでリクエストを送信している
https://qiita.com/api/v2/items?per_page=25&query=検索キーワード
例) 検索ワード(query: formText)が「react」の場合
https://qiita.com/api/v2/items?per_page=25&query=react
import {
Dispatch,
FormEvent,
SetStateAction,
useCallback,
useState,
} from "react";
import { apiClient } from "../lib/apiClient";
import { QiitaItem, QiitaItemResponse } from "../types/QiitaItem";
import { useSetRecoilState } from "recoil";
import { searchWordsSelector } from "../grobalStates/selectors/searchWordsSelector";
export const useListQiitaArticles = () => {
const setSearchWords = useSetRecoilState(searchWordsSelector);
const [articles, setArticles] = useState<QiitaItem[]>([]);
const [searchWord, setSearchWord] = useState("");
const [errorMessage, setErrorMessage] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const fetchArticles = useCallback(
async (
e: FormEvent<HTMLFormElement>,
formText: string,
setFormText: Dispatch<SetStateAction<string>>
) => {
e.preventDefault(); // フォームのデフォルトの動作(リロード)を防ぐ
setIsLoading(true); // ローディング開始
setErrorMessage(""); // エラーメッセージを初期化
await apiClient
.get<QiitaItemResponse[]>("/items", {
params: {
query: formText, // フォーム入力を検索ワードとして設定
per_page: 100, // 100件 の記事を取得するように設定
},
})
.then((response) => {
// データを利用したい値だけの形に整形
const searchArticleResponse = response.data.map<QiitaItem>((d) => {
return {
id: d.id,
title: d.title,
likes_count: d.likes_count,
user: d.user,
};
});
setArticles(searchArticleResponse);
setSearchWord(response.config.params.query); // 検索キーワードをレスポンスから取得してセット
// 検索結果が1件以上ある場合だけ検索履歴ワードをセットする(グローバルステート)
if (searchArticleResponse.length !== 0) {
// セットする値は配列なので、検索キーワード1件だけの配列をセット
setSearchWords([response.config.params.query]);
}
})
.catch((error) => {
setErrorMessage(error.message); // エラーメッセージをセット
});
setIsLoading(false); // ローディング終了
setFormText(""); // 最終的にフォーム入力を空にする
},
[setIsLoading, setErrorMessage, setArticles, setSearchWord, setSearchWords]
);
return {
articles,
searchWord,
errorMessage,
isLoading,
fetchArticles,
};
};
API成功時には、検索キーワードのステートをselector
で更新している。
// 一部抜粋
const setSearchWords = useSetRecoilState(searchWordsSelector);
// 検索結果が1件以上ある場合だけ検索履歴ワードをセットする(グローバルステート)
if (searchArticleResponse.length !== 0) {
// セットする値は配列なので、検索キーワード1件だけの配列をセット
setSearchWords([response.config.params.query]);
}
ページ(pages/...)とコンポーネント(components/...)
_app.tsx
Recoil
での状態管理を利用したいスコープを<RecoilRoot>
で囲む
import "tailwindcss/tailwind.css";
import type { AppProps } from "next/app";
import { RecoilRoot } from "recoil";
function MyApp({ Component, pageProps }: AppProps) {
return (
// <RecoilRoot>で囲うことで、その中でRecoilが利用できるようになる
<RecoilRoot>
<Component {...pageProps} />
</RecoilRoot>
);
}
export default MyApp;
囲わずにRecoil
を利用しようとするとエラーになる。
Error: This component must be used inside a <RecoilRoot> componen.
ページで共通利用のコンポーネント
BaseContainer.tsx
ページ要素のルートを囲うコンポーネント
import React, { ReactNode } from "react";
interface Props {
children: ReactNode;
}
// ベースとなるレイアウトコンテナー
export const BaseContainer: React.VFC<Props> = (props) => {
return <div className="max-w-5xl mx-auto px-12">{props.children}</div>;
};
Qiita記事リストのコンポーネント(3つ)
「検索キーワードと記事数ラベル」と「Qiita記事リスト」を表示するコンポーネント
import React from "react";
import { QiitaItem } from "../../../types/QiitaItem";
import { ArticleItem } from "./ArticleItem";
import { SearchWordWithCountLabel } from "./SearchWordWithCountLabel";
interface Props {
articles: QiitaItem[];
searchWord: string;
}
// Qiita記事リスト+検索キーワードと記事数表示コンポーネント
export const ArticleList: React.VFC<Props> = (props) => {
return (
<>
<SearchWordWithCountLabel
searchWord={props.searchWord}
articles={props.articles}
/>
{props.articles?.map((article) => {
return <ArticleItem key={article.id} article={article} />;
})}
</>
);
};
「検索キーワード と 記事数」表示のコンポーネント
検索キーワード と 記事数を表示している
import React from "react";
import { QiitaItem } from "../../../../types/QiitaItem";
interface Props {
searchWord: string;
articles: QiitaItem[];
}
// 「検索キーワード と 記事数」ラベルコンポーネント
export const SearchWordWithCountLabel: React.VFC<Props> = (props) => {
return (
<>
{props.searchWord && (
<p className="mb-4 text-xl">
検索キーワード と 記事数:
<span className="ml-2 mr-3 font-bold text-blue-700 border-b-2 border-blue-700">
{props.searchWord}
</span>
<span className="font-bold text-blue-700 border-b-2 border-blue-700">
{props.articles.length} 件
</span>
</p>
)}
</>
);
};
Qiita記事アイテムのコンポーネント
記事の下記要素を表示している
- タイトル
- LGTM数
- ユーザー名
import React from "react";
import { QiitaItem } from "../../../../types/QiitaItem";
interface Props {
article: QiitaItem;
}
// Qiita記事アイテムコンポーネント
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>
);
};
検索履歴(リンク)を表示するコンポーネント
検索履歴のキーワードを横並びで一覧表示するコンポーネント。
検索履歴をクリックするとNext.js
のLink
コンポーネントの機能で、「記事絞り込みページ」に遷移させる。
参考:next/link
import React from "react";
import Link from "next/link";
import { useRecoilValue } from "recoil";
import { searchWordsSelector } from "../../../grobalStates/selectors/searchWordsSelector";
// 検索履歴リンクコンポーネント
export const SearchHistoryWords: React.VFC = () => {
// グローバルステートから検索履歴を取得
const searchWords = useRecoilValue(searchWordsSelector);
return (
<>
{searchWords.length !== 0 &&
searchWords?.map((searchWord, index) => {
return (
<Link
key={`${searchWord}_${index}`}
href={`/filter-articles/${searchWord}`}
>
<span className="p-2 mx-1 bg-green-100 text-green-800 rounded-lg shadow cursor-pointer">
{searchWord}
</span>
</Link>
);
})}
</>
);
};
記事検索ページ
pages/search-articles/index.tsx
レンダリング要素
- 記事検索フォーム
- 記事検索結果
ポイント
- API処理のカスタムフックを呼ぶ
- 検索結果のQiita記事リストを
Recoil
のselector
でセット
import React, { useEffect } from "react";
import { useListQiitaArticles } from "../../hooks/useListQiitaArticles";
import { SearchForm } from "../../components/SearchArticlesPage/SearchForm";
import { useSetRecoilState } from "recoil";
import { articleListSelector } from "../../grobalStates/selectors/articleListSelector";
import { SearchResult } from "../../components/SearchArticlesPage/SearchResult";
import { BaseContainer } from "../../components/BaseContainer";
// Qiita記事検索ページ
const SearchArticles: React.VFC = () => {
const { articles, searchWord, errorMessage, isLoading, fetchArticles } =
useListQiitaArticles();
// 検索キーワードを引数に渡した articleListSelector のセッター
const setSearchArticleList = useSetRecoilState(
articleListSelector(searchWord)
);
useEffect(() => {
// グローバルステートをセット
setSearchArticleList(articles);
}, [articles, setSearchArticleList]);
return (
<BaseContainer>
<SearchForm fetchArticles={fetchArticles} isLoading={isLoading} />
<SearchResult
articles={articles}
searchWord={searchWord}
errorMessage={errorMessage}
isLoading={isLoading}
/>
</BaseContainer>
);
};
export default SearchArticles;
components/SearchArticlesPage/SearchForm/index.tsx
フォーム入力を検索キーワードとしてAPIフェッチ処理を実施するコンポーネント
import React, { Dispatch, FormEvent, SetStateAction, useState } from "react";
import { SearchHistoryWords } from "../../common/SearchHistoryWords";
interface Props {
fetchArticles: (
e: FormEvent<HTMLFormElement>,
formText: string,
setFormText: Dispatch<SetStateAction<string>>
) => void;
isLoading: boolean;
}
// Qiita記事検索フォームコンポーネント
export const SearchForm: React.VFC<Props> = (props) => {
const [formText, setFormText] = useState<string>("");
const buttonColor =
formText && !props.isLoading
? "bg-blue-500 hover:bg-blue-400"
: "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}
disabled={props.isLoading}
onChange={(e) => setFormText(e.target.value)}
/>
<button
className={`${buttonColor} text-white font-bold mr-3 py-2 px-4 rounded focus:outline-none focus:shadow-outline`}
type="submit"
disabled={!formText || props.isLoading}
>
検索
</button>
<SearchHistoryWords />
</form>
);
};
components/SearchArticlesPage/SearchResult/index.tsx
props
で受け取った検索結果に応じた内容を表示するコンポーネント
import React from "react";
import { QiitaItem } from "../../../types/QiitaItem";
import { ArticleList } from "../../common/ArticleList";
interface Props {
articles: QiitaItem[];
searchWord: string;
errorMessage: string;
isLoading: boolean;
}
// 検索結果表示コンポーネント
export const SearchResult: 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 (
<ArticleList articles={props.articles} searchWord={props.searchWord} />
);
};
記事絞り込みページ
pages/filter-articles/[word]/index.tsx
レンダリング要素
- 「< もどる」ボタン
- LGTM数での記事絞り込みフォーム
- 絞り込み後の記事一覧
ポイント
-
Next.js
のuseRouter
でパスから検索ワードを取得する - 検索ワードとLGTM数に応じた記事リストを
Recoil
のselector
で取得してレンダリング
参考:next/router
import React from "react";
import { useRouter } from "next/router";
import { useRecoilValue } from "recoil";
import { articleListSelector } from "../../../grobalStates/selectors/articleListSelector";
import { LikesCountFilter } from "../../../components/FilterArticlesPage/LikesCountFilter";
import { ArticleList } from "../../../components/common/ArticleList";
import { BaseContainer } from "../../../components/BaseContainer";
// LGTM数でQiita記事絞り込みページ
const FilterArticlesByWord: React.VFC = () => {
const router = useRouter();
const searchWord = (router.query.word as string) || ""; // router.query.word の word はディレクトリ名[word]に対応している
const articleList = useRecoilValue(articleListSelector(searchWord));
return (
<BaseContainer>
<button
className={`bg-blue-500 hover:bg-blue-400 text-white font-bold mt-2 py-1 px-3 rounded focus:outline-none focus:shadow-outline`}
onClick={() => router.push("/search-articles")}
>
< もどる
</button>
<LikesCountFilter />
<ArticleList articles={articleList} searchWord={searchWord} />
</BaseContainer>
);
};
export default FilterArticlesByWord;
components/FilterArticlesPage/LikesCountFilter/index.tsx
LGTM数を変更して絞り込みをするフィルターのコンポーネント。
LGTM数をグローバルステート(Recoil
のatom
に直接)に更新することで取得してレンダリングする記事を制御している。
import React, { useCallback, useEffect } from "react";
import { SearchHistoryWords } from "../../common/SearchHistoryWords";
import { useRecoilState, useResetRecoilState } from "recoil";
import { minLikesCountState } from "../../../grobalStates/atoms/minLikesCountAtom";
// LGTM数でQiita記事絞り込みフォームコンポーネント
import React, { useCallback, useEffect } from "react";
import { SearchHistoryWords } from "../../common/SearchHistoryWords";
import { useRecoilState, useResetRecoilState } from "recoil";
import { minLikesCountState } from "../../../grobalStates/atoms/minLikesCountAtom";
// LGTM数でQiita記事絞り込みフォームコンポーネント
export const LikesCountFilter: React.VFC = () => {
// [取得した値, セッター] LGTM数のグローバルステートより取得
const [minLikesCount, setMinLikeCount] = useRecoilState(minLikesCountState);
// Recoilのリセット関数:実行でデフォルト値に戻す
const resetMinLikesCount = useResetRecoilState(minLikesCountState);
const buttonColor =
minLikesCount !== 0 ? "bg-red-400 hover:bg-red-300" : "bg-gray-300";
const onClickReset = useCallback(
(e) => {
e.preventDefault(); // リロードを防ぐ
resetMinLikesCount();
},
[resetMinLikesCount]
);
useEffect(() => {
// アンマウント時にLGTM数リセット
return () => resetMinLikesCount();
}, []);
return (
<form className="mt-3 mb-2">
<label className="block text-gray-700 text-lg font-bold mb-2">
LGTM👍 数で絞り込み(下限を指定)
</label>
<input
className="w-24 shadow appearance-none border rounded py-2 px-3 mr-2 mb-4 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
type="number"
min="0"
value={minLikesCount}
onChange={(e) => setMinLikeCount(parseInt(e.target.value) || 0)}
/>
<button
className={`${buttonColor} text-white font-bold py-2 px-4 mr-1 rounded focus:outline-none focus:shadow-outline`}
onClick={onClickReset}
disabled={minLikesCount === 0}
>
リセット
</button>
<SearchHistoryWords />
</form>
);
};
最後に
何はともあれRecoil
もReact
もNext.js
も使っていて、書いていてホントに楽しいです!
比較的新しい技術にもかかわらず日本語の記事なんかも非常に多くていつも助けられています!
今回もいろいろな記事の知見に助けていただきました。
ありがとうございました。
だいぶわかりずらい部分もあると思います。誤りなどあれば気軽にコメントお願いしたいです!