55
37

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 1 year has passed since last update.

【React】カスタムフック(Custom Hook)を「完全に理解」してみる

Last updated at Posted at 2023-01-05

カスタムフックとは

カスタムフックとはその名の通り、自分がカスタムして作るフックです。
公式ドキュメントを見ると以下のように説明されています。

自分独自のフックを作成することで、コンポーネントからロジックを抽出して再利用可能な関数を作ることが可能です。

勉強中の時は普通のHooksですら覚えるのが大変で、カスタムフックにまで手を出せる余裕がありませんでした。
しかし…現場でカスタムフックを使わない現場はないと言えるほど、その存在感は大きいものだということが、現場に入って痛感しました。
現場レベルを目指すのであれば、カスタムフックはマストの技術という訳です。

なので今回は、「ふわっとしか理解していない」を「完全に理解した」となるまで本気で解説していこうと思います!
では行ってみよう!!!

カスタムフックのメリット

①コンポーネントの複雑化を防ぐ

まずはカスタムフックがどういうものかを見ていきましょう。
下記のコードはカスタムフックを利用する前のコードです。

import { useState } from "react";

export const App = () => {
  const [count, setCount] = useState(0);
  // カウントを1増やす
  const incrementCount = () => setCount((count) => count + 1);
  // カウントを1減らす
  const decrementCount = () => setCount((count) => count - 1);

  return (
    <div>
      <p>{count}</p>
      <button onClick={incrementCount}>+1</button>
      <button onClick={decrementCount}>-1</button>
    </div>
  );
};

これをカスタムフックにしてみます。

hooks.js
import { useState } from "react";

export const useCounter = () => {
  const [count, setCount] = useState(0);

  // カウントを1増やす
  const incrementCount = () => setCount((count) => count + 1);

  // カウントを1減らす
  const decrementCount = () => setCount((count) => count - 1);

  return [count, { setCount, incrementCount, decrementCount }];
};
App.js
import { useCounter } from "./hooks";

export const App = () => {
  const [count, { incrementCount, decrementCount }] = useCounter();

  return (
    <div>
      <p>{count}</p>
      <button onClick={incrementCount}>+1</button>
      <button onClick={decrementCount}>-1</button>
    </div>
  );
};

このように、Viewとロジックを分離することができます。
これによって、コンポーネント内の記述がかなり簡潔に書くことができます。

②機能を再利用できる

Viewとロジックを分離することができることがわかりました。
ということは、ロジックだけを再利用することも可能ということです。

同じ処理を別のコンポーネントで使いたい場合、コンポーネントにロジックを書いていた時は、そのコードをコピーして貼り付ける必要があります。
カスタムフックを作成しておけば、そのコンポーネントに作成したカスタムフックをimportするだけで使えるため、同じ処理を書く必要がなく、ロジックの再利用性が向上します。

③複数のhooksをまとめることができる

以下は『API経由で取得したTodoリストの内容を画面に表示する』例です。

hooks.js
import axios from "axios";
import { useEffect, useState } from "react";

const useTodos = () => {
  const [todos, setTodos] = useState([] as TodoItem[]);
  useEffect(() => {
    const getTodoRequest = async () => {
      const response = await axios.get("http://localhost:4000/todos");
      const todos = response.data;
      return todos;
    };
    getTodoRequest().then((todos) => setTodos(todos as TodoItem[]));
  }, []);
  return todos;
};
export default useTodos;
App.js
import useTodos from "./lib/hooks/useTodos";

const App = () => {
  const todos = useTodos();
  return (
    <>
      <h2>TodoList</h2>
      <ul>
        {todos.map(({ id, content }) => (
          <li key={id}>
            <div>{content}</div>
          </li>
        ))}
      </ul>
    </>
  );
};
export default App;

コンポーネントに直接フックを実装する場合、フックが増えた際にフックの関連性がわかりにくくなります。
しかし、hooks.jsをみてわかる通り、Todoリストに関するフック(useStateとuseEffect)はTodoリストのカスタムフック内にまとめられます。

カスタムフックを導入することにより、コンポーネント側はカスタムフックを呼ぶだけで関連するフックを意識することなくデータが扱えます。

④テストが容易

上記で説明したように、機能を再利用できるということはそれ単体で動かせる状態ということであり、単体で動かせるということはテストが簡単になるということです。
コンポーネントのテストとロジックのテストを分けることができるのは、カスタムフックの大きなメリットだと思います。

ルール

カスタムフックには公式が提唱しているルールが存在します。

①命名は必ず "use" で始まる

React はカスタムフックもルールを違反してるかどうかを自動でチェックしてくれます。しかしこの命名規則を守らなかったらカスタムフックかどうか判別できなくなり、自動チェックもできなくなります

カスタムフックの命名は必ずuseで始まるようにしましょう。

②カスタムフックの戻り値

カスタムフックを作成する際は、何を返すべきか悩みます。
これは公式のルールにはなく、個人で自由に設定することができます。
基本的には、

・特定のuse〇〇の形式に合わせる
・全部返す
のどちらかが基本になります。

特定のuse〇〇の形式に合わせる

useStateやuseEffect、useRefなどの基本的なReact hooksには、戻り値にルールがあり、
カスタムフックもそのルールに合わせる方法があります。
一般的なマナーと言われるくらい、一番使われている方法です。

React hooks 戻り値
useEffect 戻り値なし
useRef 1個
useContext 1個
useState 配列([state, state更新関数])
useReducer 配列([state, state更新関数])

React hooksにスタイルを合わせるメリットとして下記2点が挙げられます

戻り値だけで、どのようなフックであるかある程度予測がつく
カスタムフックの処理自体にも統制をとることができる
戻り値が肥大化した際に、処理を分割すべきか見直すことができるため

先ほどの、カウント計測のカスタムフックもこちらを採用しております。

export const useCounter = () => {
  ・・・

  return [count, { setCount, incrementCount, decrementCount }];
};

オブジェクトですべて返す

返したいものを全てオブジェクトに詰め込んで返すパターンもあります。
とてもシンプルで、初心者、玄人問わず共通認識が合わせやすい方法です。

オブジェクトで返すのメリットとしては、下記3点が挙げられます。

・返却値を増やしたい時に変更がいらない
・コンポーネントで呼び出す際に、返却値の命名が変更されることがない
・テスト時に値を取りやすい (result.current.〇〇でとれる)
先ほどの処理をこのパターンに書き換えるとこのようになります。

export const useCounter = () => {
  ・・・

  return { count, setCount, incrementCount, decrementCount };
};

③同関数のメモ化

useCallbackなどを用いたメモ化はReactのパフォーマンスを上げる上で必須です。
下記のようなイメージです。

export const useCounter = (init) => {
    const [count, setCount] = useState(init);
    const increment = useCallback(() => {
        setCount(count + 1);
    }, [count]);
    const decrement = useCallback(() => {
        setCount(count - 1);
    }, [count]);
    return [count, {increment, decrement}];
};

また、カスタムフック全体を毎回実行するのはコストがかかるので、useMemoを用いて無駄な呼び出しを削減できる場合もあるなら、それも活用していくと良いです。

Hookの作成

上記に例として出しているのですが、実際の作り方を見ていきましょう。
JSONPlaceHolderからデータを取得し、取得したデータを表示するコードを作成していきます。

srcフォルダの下にhooksフォルダを作成して、useFetchData.jsファイルを作成します。Reactのドキュメント通り名前の先頭にはuseを使います。User.jsとPost.jsで共通化できる部分をuseFetchData関数として取り出すと下記のように記述することができます。User.jsとPost.jsではアクセスするURLが異なるのでURLは引数として外側から渡します。

src/hooks/useFetchData.js
import { useState, useEffect } from 'react';

const useFetchData = (url) => {
  const [data, setData] = useState([]);
  useEffect(() => {
    const fetchPost = async () => {
      const response = await fetch(url);
      const data = await response.json();
      setData(data);
    };
    fetchPost();
  }, [url]);
  return { data };
};

export default useFetchData;

Hookの使い方

カスタムフックのuseFetchDataが作成できたらUser.js, Post.jsでimportを行いuseFetchDataを利用します。
データを取得する部分についての処理はすべてuseFetchDataに記述されているのでどちらのコードもViewのみの記述で完結します。

src/user.js
import useFetchData from '../hooks/useFetchData';

const User = () => {
  const { data } = useFetchData('https://jsonplaceholder.typicode.com/users');

  return (
    <div>
      <h1>ユーザ一覧</h1>
      <ul>
        {data.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default User;
src/post.js
import useFetchData from '../hooks/useFetchData';

const Post = () => {
  const { data } = useFetchData('https://jsonplaceholder.typicode.com/posts');
  return (
    <div>
      <h1>記事一覧</h1>
      <ul>
        {data.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default Post;

終わりに

いかがだったでしょうか。
カスタムフックは一見難しそうにも見えますが、一つ一つの説明を見るとそんなに難しい話ではなかったですよね。
むしろカスタムフックを使いこなすことで得られるメリットはとても大きいものに感じました。
これを使わない手はないでしょう!

良きReactライフを!

参考

下記サイトをめちゃくちゃ参考にさせていただきました:pray_tone1:

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?