🐣 React実践の教科書 P242〜253
作成日: 2022/01/18
1

今日やったこと

  • 「モダンJavaScriptの基本から始める React実践の教科書」 P242〜253
    • 9-1 カスタムフックとは
    • 9-2 カスタムフックの雛形を作成
    • 9-3 カスタムフックの実装

学習メモ

カスタムフックの概要

これまで以下のようなReactに標準で搭載されている Hooks を紹介してきた

- useState
- useEffect
- useCallback
- useMemo
- useContext
  • カスタムフックでこういった便利機能や特定のロジック(例: データの取得、ログイン処理等)を実行する Hooks をプロジェクト内で自作していく
  • 推奨ルールとして、標準の Hooks が全て use から始まっているので自作のカスタムフックもuse〜と命名する
    • 例えばユーザーデータ一覧を取得・設定するような Hooks の場合は、useFetchUsersという名前になる
  • カスタムフックファイルの中でも各種Hooksを利用することができる
    • useStateを使用してStateを定義したり、useEffectで副作用を制御することも可能

9-1 カスタムフックの必要性

カスタムフックの必要性を感じるために、まずはカスタムフックを使っていないシンプルなデータ取得・変換・表示のアプリケーションの例を考えてみる
サンプルのためTypeScriptは使わない例とする


APIを呼ぶと以下のような一覧データが取得できることとする

例:エンドポイント

https:// example.com/users

例:取得結果

[
	{
		"id": 1,
		"name": "主田",
		"age": 24
	},
	{
		"id": 2,
		"name": "先岡",
		"age": 28
	},
	{
		"id": 3,
		"name": "後藤",
		"age": 23
	},
]

仕様としては以下のようなアプリケーションとする

  • ボタン押下でユーザーデータを取得
  • 取得中は「データ取得中です」を表示
  • エラーが発生した場合は赤色で「エラーが発生しました」を表示
  • lastname(苗字)とfirstname(名前)は半角空白を空けて結合して表示

「ユーザーの一覧情報」「ローディング中かどうか」「エラーがあるかどうか」という3つのStateが必要となる
要件を満たすコードの一例は以下のようになる

App.jsx

import { useState } from "react";
import axios from "axios";

export const App = () => {
  const [userList, setUserList] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  // ユーザー取得ボタン押下アクション
  const onClickFetchUser = () => {
    // ボタン押下時にローディングフラグon、エラーフラグoff
    setIsLoading(true);
    setIsError(false);

    // APIの実行
    axios
      .get("https://example.com/users")
      .then(result => {
        // 苗字と名前を結合するように変換
        const users = result.data.map(user => ({
          id: user.id,
          name: `${user.lastname} ${user.firstname}`,
          age: user.age
        }));
        // ユーザー一覧 State を更新
        setUserList(users);
      })
      // エラーの場合はエラーフラグを on
      .catch(() => setIsError(true))
      // 処理完了後はローディングフラグを off
      .finally(() => setIsLoading(false));
  };

  return (
    <div>
      <button onClick={onClickFetchUser}>ユーザー取得</button>
      {/* エラーの場合はエラーメッセージを表示 */}
      {isError && <p style={{ color: "red" }}> エラーが発生しました</p>}
      {/* ローディング中は表示を切り替える */}
      {isLoading ? (
        <p> データ取得中です </p>
      ) : (
        userList.map(user => (
          <p key={user.id}>{`${user.id}:${user.name} (${user.age}歳)`}</p>
        ))
      )}
    </div>
  );
};
  • 機能の実現はできているが、onClickFetchUser関数の中でフラグの設定やデータの取得・変換を行っているため、コンポーネントのコード量が増えてしまっている
  • 本来コンポーネントの責務は与えられたデータに基づいて画面の見た目を構築することなのでこの複雑なロジック部分は分離してあげる方が良さそう
  • また、他のコンポーネントで同様のユーザー一覧取得を実装することになった場合、onClickFetchUser関数の中身を全てコピー&ペーストしていくことになるが、そうした場合、後に取得ロジックの変更があると複数のコンポーネントを修正していく羽目になってしまう

このようなコードを改善していくためにカスタムフックを学んでいく

9-2 カスタムフックの雛形を作成

実装したプロジェクトにカスタムフックを追加する
まずはカスタムフックを入れるためのフォルダを作成して、その中にuseFetchUsers.jsという名称でファイルを作成する

カスタムフックの実体はただの関数なので、以下のように関数を実装していく

useFetchUsers.js

// ユーザー一覧を取得するカスタムフック
export const useFetchUsers = () => {}

まずはコンポーネント側から接続できるかどうかを確認したいので、仮の State と関数を定義して return で返却しておく

useFetchUsers.js

import { useState } from "react";

// ユーザー一覧を取得するカスタムフック
export const useFetchUsers = () => {
    const [userList, setUserList] = useState([{ id: 1 }]);
    const onClickFetchUser = () => alert('関数実行')

    // まとめて返却したいのでオブジェクトに設定する
    return { userList, onClickFetchUser }
}
  • カスタムフックではStateや関数等、複数の値を return していくことが多いので上記のようにオブジェクト(または配列)でまとめて返却することが多い

  • 上記ではuserListというプロパティにuserListという変数の値を割り当てているが、これはオブジェクトの省略記法を使っている

このカスタムフックをコンポーネントから実行する場合、以下のように記述する

App.jsx

import { useState } from "react";
import { useFetchUsers } from "./hooks/useFetchUsers";

export const App = () => {
  // カスタムフックの使用
  // 関数を実行し返却値を分割代入で受け取る
  const { userList, onClickFetchUser } = useFetchUsers();
  console.log(userList); // [{ id: 1 }]
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  // ユーザー取得ボタン押下アクション
  return (
    <div>
      <button onClick={onClickFetchUser}>ユーザー取得</button>
      {/* エラーの場合はエラーメッセージを表示 */}
      {isError && <p style={{ color: "red" }}> エラーが発生しました</p>}
      {/* ローディング中は表示を切り替える */}
      {isLoading ? (
        <p> データ取得中です </p>
      ) : (
        userList.map(user => (
          <p key={user.id}>{`${user.id}:${user.name} (${user.age}歳)`}</p>
        ))
      )}
    </div>
  );
};

実行すると、userListにカスタムフック側で設定した値が入っていることが確認できる
また、この状態でボタンを押下するとアラートが表示されカスタムフック側で定義したonClickFetchUser関数が正しく実行されていることが確認できる

9-3 カスタムフックの実装

雛形の作成と接続の確認ができたので、処理を実装していく
といっても、もともと書いていた処理を移動させるだけ
userListだけでなくisLoadingisErrorもユーザー一覧取得に関係するStateなのでカスタムフックに持っていく

useFetchUsers.js

import { useState } from "react";
import axios from "axios";

// ユーザー一覧を取得するカスタムフック
export const useFetchUsers = () => {
    const [userList, setUserList] = useState([]);
    const [isLoading, setIsLoading] = useState(false);
    const [isError, setIsError] = useState(false);

    // ユーザー取得ボタン押下アクション
    const onClickFetchUser = () => {
        // ボタン押下時にローディングフラグon、エラーフラグoff
        setIsLoading(true);
        setIsError(false);

        // APIの実行
        axios
            .get("https://example.com/users")
            .then(result => {
                // 苗字と名前を結合するように変換
                const users = result.data.map(user => ({
                id: user.id,
                name: `${user.lastname} ${user.firstname}`,
                age: user.age
                }));
                // ユーザー一覧 State を更新
                setUserList(users);
            })
            // エラーの場合はエラーフラグを on
            .catch(() => setIsError(true))
            // 処理完了後はローディングフラグを off
            .finally(() => setIsLoading(false));
    }

    // まとめて返却したいのでオブジェクトに設定する
    return { userList, isLoading, isError, onClickFetchUser };
}

App.jsx

import { useState } from "react";
import { useFetchUsers } from "./hooks/useFetchUsers";

export const App = () => {
  // カスタムフックの使用
  // 関数を実行し返却値を分割代入で受け取る
  const { userList, isLoading, isError, onClickFetchUser } = useFetchUsers();

  // ユーザー取得ボタン押下アクション
  return (
    <div>
      <button onClick={onClickFetchUser}>ユーザー取得</button>
      {/* エラーの場合はエラーメッセージを表示 */}
      {isError && <p style={{ color: "red" }}> エラーが発生しました</p>}
      {/* ローディング中は表示を切り替える */}
      {isLoading ? (
        <p> データ取得中です </p>
      ) : (
        userList.map(user => (
          <p key={user.id}>{`${user.id}:${user.name} (${user.age}歳)`}</p>
        ))
      )}
    </div>
  );
};

これで完全にロジックの分離をすることができ、カスタムフック化する前と比べてApp.jsxが非常にすっきりした
また、他のコンポーネントでユーザー一覧取得をしたい場合も、以下のように2行追加するだけで実装できるようになった

import { useFetchUsers } from "./hooks/useFetchUsers";
// …省略
  const { userList, isLoading, isError, onClickFetchUser } = useFetchUsers();

また、ロジックに変更があった場合もuseFetchUsersを修正するだけなので、修正漏れがなくなる
このようにカスタムフックを適切に使用していくことで可読性・保守性の高いReact開発を行うことが可能となる

所感

自作のHooksを作る方法を学びました。
明日以降は、これまで学んだことの総復習として、React×TypeScriptでのメモアプリを作成します。
一旦サンプルコードを見ずに実装していきます。