👍

3ステップで実現するカスタムフック作成ガイド

2022/12/25に公開

カスタムHooksの使い方を早く知りたい方へ送る、初心者向け記事です。
keyword:React, customHooks, カスタムフック, 初心者

0. カスタムHooksはどんなときに使う?

以下のとおり、主にHooks周りでごちゃごちゃしているところを、分離したいときに使います。
※詳しくは、ReactのカスタムHooksをカジュアルに使ってコードの見通しを良くしようという良記事を参照願います

  • before:カスタムHookがないとき
    before
  • after:カスタムHookがあるとき
    after

1. 分離したいところを別コンポーネントにコピペする

さっそくカスタムHook化していきましょう。
上記サンプルのうち、Hooks周りでごちゃごちゃしているところを、新しく作ったファイルにコピペします。
このままだとエラーが出たりしますが、ひとまず気にしなくて大丈夫です。

カスタムHooksを利用する前のサンプルコード
App.js
import { useState } from "react";
import { API_URL } from "../src/consts/urls";
import { useToastMessage } from "./useToastMessage";

export const App = () => {
  const [keyword, setKeyword] = useState("");
  const [books, setBooks] = useState();
  const { showMessage } = useToastMessage();

  const searchBooks = async (keyword) => {
    try {
      //APIからデータをfetchする
      const r = await fetch(`${API_URL}?q=intitle:${keyword}&maxResults=15`);
      const data = await r.json();

      if (!r.ok) {
        throw new Error("エラーが発生しました");
      }
      if (data.totalItems === 0) {
        setBooks(null);
      } else {
        const filteredData = data.items.map((item, i) => {
          return {
            id: item.id,
            number: i,
            title: item.volumeInfo.title,
            thumbnail: item.volumeInfo.imageLinks
              ? item.volumeInfo.imageLinks.smallThumbnail
              : undefined,
          };
        });

        setBooks(filteredData);
      }

      //通信失敗時はエラーをtoastで表示する
    } catch (e) {
      console.error(e);
      return showMessage({ title: "エラーが発生しました", status: "error" });
    }
  };
  return (
    <>
      <input
        pr="4.5rm"
        id="input"
        bg="gray.100"
        name="input"
        placeholder="本のタイトルを入力"
        type="text"
        value={keyword}
        onChange={(e) => {
          setKeyword(e.target.value);
        }}
      />
      <button type="submit" onClick={() => {
                searchBooks(keyword);
              }} />// input
      {books && books[0].title}  //booksがあるときに1つめのタイトルを表示
    </>
  );
};

useSearchBooks.jsx
import { useState } from "react";
import { API_URL } from "../src/consts/urls";
import { useToastMessage } from "./useToastMessage";

export const App = () => {
  const [keyword, setKeyword] = useState("");
  const [books, setBooks] = useState();
  const { showMessage } = useToastMessage();

  const searchBooks = async (keyword) => {
    try {
      //APIからデータをfetchする
      const r = await fetch(`${API_URL}?q=intitle:${keyword}&maxResults=15`);
      const data = await r.json();

      if (!r.ok) {
        throw new Error("エラーが発生しました");
      }
      if (data.totalItems === 0) {
        setBooks(null);
      } else {
        const filteredData = data.items.map((item, i) => {
          return {
            id: item.id,
            number: i,
            title: item.volumeInfo.title,
            thumbnail: item.volumeInfo.imageLinks
              ? item.volumeInfo.imageLinks.smallThumbnail
              : undefined,
          };
        });

        setBooks(filteredData);
      }

      //通信失敗時はエラーをtoastで表示する
    } catch (e) {
      console.error(e);
      return showMessage({ title: "エラーが発生しました", status: "error" });
    }
  };

2. 上下にちょっと付け足す

カスタム Hookの名前と関数名をつけて、使いたい変数やステートをreturnさせる。

useSearchBooks.jsx
import { useCallback, useState } from "react";
import { API_URL } from "../src/consts/urls";
import { useToastMessage } from "./useToastMessage";

export const useSearchBooks = () => {   //1. useなんちゃらという名前をつけてnamed export
    const [keyword, setKeyword] = useState("");
    const [books, setBooks] = useState();
    const { showMessage } = useToastMessage();
    
    const searchBooks = useCallback(  //2. 関数名をつけてメモ化する
      async (keyword) => {
        try {
          const r = await fetch(`${API_URL}?q=intitle:${keyword}&maxResults=15`);
          const data = await r.json();

          if (!r.ok) {
            throw new Error("エラーが発生しました");}
          if (data.totalItems === 0) {
            setBooks(null);
          } else {
            const filteredData = data.items.map((item, i) => {
              return {
                id: item.id,
                number: i,
                title: item.volumeInfo.title,
                thumbnail: item.volumeInfo.imageLinks
                  ? item.volumeInfo.imageLinks.smallThumbnail
                  : undefined};
            });

            setBooks(filteredData);
          }
        } catch (e) {
          console.error(e);
          return showMessage({ title: "エラーが発生しました", status: "error" });
        }
      },
      [setBooks, showMessage]
    );
    return { searchBooks, books, keyword, setKeyword }; //3. 使いたいものをreturnする
  };

3. 使いたいところに1行足す

分離元のコンポーネントに戻りカスタムHookをインポートする。
カスタムHook化した箇所を削除し、1行足すと無事完成です。

App.js
import { useSearchBooks } from "../../../Hooks/useSearchBooks";  //カスタムHookをインポート

export const App = () => {
  const { searchBooks, books, keyword, setKeyword } = useSearchBooks(); //カスタムHookでreturnしたものを呼び出す。カスタムHook名に()をつけるのを忘れないように。

  return (
    <>
      <input
        placeholder="本のタイトルを入力"
        type="text"
        value={keyword}
        onChange={(e) => {
          setKeyword(e.target.value);
        }}
      />
      <button
        type="submit"
        onClick={() => {
          searchBooks(keyword);
        }}
      />
      // input
      {books && books[0].title} 
    </>
  );
};

<完>

サンプルコード集

記事の元ネタのコードや別のチュートリアルコードを公開しています
https://codesandbox.io/s/async-practice-styled-h35nyn?file=/Hooks/useSearchBooks.jsx
https://codesandbox.io/s/react-tutorial10-chakraui-d4k0t5?file=/src/Hooks/useLoginUser.ts

Discussion