37
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【React / TypeScript】Recoil での状態管理サンプルアプリ

Last updated at Posted at 2021-09-23

はじめに

以前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動画

qiita-recoil-sample.gif

画質悪くてすみません。

ディレクトリ構成

srcフォルダで管理。
スクリーンショット 2021-09-23 7.39.27.png

ディレクトリの中身をもう少し細かく見てみるとこんな感じ。
スクリーンショット 2021-09-23 7.40.15.png

各ファイルのコード

型定義(types/QiitaItem.ts)

(正直「レスポンスの型」と「アプリ内で利用するための型」分ける必要なかったかもしれない、)

src/types/QiitaItem.tsx
// アプリ内で利用するための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 を付与できる)
src/lib/apiClient.tsx
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のキーを一元管理。

src/grobalStates/recoilKeys.ts
// 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>

今回は利用するとき(selectorgetorsetするとき)に「検索キーワード」を引数に渡すことで 「検索キーワード」ごとのstate管理を可能にする。

src/grobalStates/atoms/articleListAtom.ts
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管理を可能にしている。

src/grobalStates/selectors/articleListSelector.ts
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[]>
src/grobalStates/atoms/searchWordsAtom.ts
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にセット(更新する)
src/grobalStates/selectors/searchWordsSelector.ts
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にて値を加工する必要がないため)

src/grobalStates/atoms/minLikesCountAtom.ts
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

src/hooks/useListQiitaArticles.ts
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>で囲む

src/pages/_app.tsx
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

ページ要素のルートを囲うコンポーネント

src/components/BaseContainer/index.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記事リスト」を表示するコンポーネント

src/components/common/ArticleList/index.tsx
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} />;
      })}
    </>
  );
};

「検索キーワード と 記事数」表示のコンポーネント

検索キーワード と 記事数を表示している

src/components/common/ArticleList/SearchWordWithCountLabel/index.tsx
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数
  • ユーザー名
src/components/common/ArticleList/ArticleItem/index.tsx
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.jsLinkコンポーネントの機能で、「記事絞り込みページ」に遷移させる。

参考:next/link

src/components/common/ArticleList/SearchHistoryWords/index.tsx
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記事リストをRecoilselectorでセット
src/pages/search-articles/index.tsx
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フェッチ処理を実施するコンポーネント

src/components/SearchArticlesPage/SearchForm/index.tsx
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で受け取った検索結果に応じた内容を表示するコンポーネント

src/components/SearchArticlesPage/SearchResult/index.tsx
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.jsuseRouterでパスから検索ワードを取得する
  • 検索ワードとLGTM数に応じた記事リストをRecoilselectorで取得してレンダリング

参考:next/router

src/pages/filter-articles/[word]/index.tsx
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数をグローバルステート(Recoilatomに直接)に更新することで取得してレンダリングする記事を制御している。

src/components/FilterArticlesPage/LikesCountFilter/index.tsx
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>
  );
};

最後に

何はともあれRecoilReactNext.jsも使っていて、書いていてホントに楽しいです!

比較的新しい技術にもかかわらず日本語の記事なんかも非常に多くていつも助けられています!
今回もいろいろな記事の知見に助けていただきました。
ありがとうございました。

だいぶわかりずらい部分もあると思います。誤りなどあれば気軽にコメントお願いしたいです!

参考にさせていただいた記事

37
14
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
37
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?