445
298

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】カスタムフックと本気で向き合ってみた

Last updated at Posted at 2021-08-30

世はまさに、React hooks時代。
React hooks無しにして、Reactは書けない!!という人も多いのではないでしょうか。

その中でも特に「カスタムフック」は、React hooksの便利さをの根幹とも言える、最も重要な機能です。

カスタムフックは自由にカスタマイズできる一方で、設計や実装に悩むことが多くあります。

そんなカスタムフックに、本気で向き合ってみようと思います。

1章 - そもそもカスタムフックとは

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

公式ドキュメント: 独自フックの作成

簡単に言うと、React hooksの処理をコンポーネントに直接書くのではなく、
別ファイルに切り出して新しいhooksとして定義した関数のことをカスタムフックと言います。

2章 - カスタムフックを利用するメリット

カスタムフックを利用するメリットは沢山あると思いますが、
私が思うメリットは「複数のHooksをまとめることができる」「View(コンポーネント)とロジックが分離できる」の2点です。

1. 複数のReact hooksをまとめることができる

カスタムフックを利用すると、複数のReact hooksを1つにまとめて使うことができます。

下記コードは、useState, useEffectをまとめたuseCountTimeという新しいフックを作成している例です。
1つにまとめることで、同じ組み合わせの処理を他のコンポーネントで使うことができます。

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

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

  // 1秒ずつカウントを増やす
  useEffect(() => {
    const timer = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);

    // コンポーネントがアンマウントされる時にタイマー停止(クリーンアップ処理)
    return () => {
      clearTimeout(timer);
    };
  }, []);

  return count;
};
App.jsx
import { useCountTime } from "./hooks";

export const App = () => {
  const count = useCountTime();

  return <p>{count}</p>;
};

2. View(コンポーネント)とロジックが分離できる

カスタムフックを利用することで、コンポーネントとReact hooksの処理を分けることができます。
コンポーネントとロジックを分けることで、下記のようなメリットがあります。

  1. コンポーネントの関数の肥大化を抑えることができる
  2. ロジックの再利用性の向上
  3. テスタビリティの向上

以上3点のメリットにより、たとえ複数のhooksを含まなくともすべての処理をカスタムフックにする価値があると感じています。

下記コードをもとに詳しく解説します。

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.jsx
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>
  );
};

2-1. コンポーネント関数の肥大化を抑えることができる

上記コードをカスタムフックを利用せずに書いてみましょう。

App.jsx
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>
  );
};

このように、Appという一つの関数コンポーネントのコードが、カスタムフックを利用した時よりも長くなります。
そのため、処理が増えれば増えるほど、「あれ、JSX部分はどこだ?」「〇〇の処理を書いたのはどこだ?」と、可読性が低下する恐れがあります。

カスタムフックはそのようなコンポーネント関数の肥大化を抑えることができ、コードの可読性を向上させる効果があります。

2-2. ロジックの再利用性の向上

同じ処理を別のコンポーネントで使いたい場合、コンポーネントにロジックを書いていた時は、そのコードをコピーして貼り付ける必要があります。

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

2-3. テスタビリティの向上

React hooksはReact.FC内でしか呼ぶことができませんが、
@testing-library/react-hooksrenderHookを使えばコンポーネント外でもテストを書くことができます。

hooks.test.js
describe("useCounter", () => {
  const { result } = renderHook(() => useCounter());
  const [count] = result.current;

  test("初期値は0", () => {
    expect(count).toEqual(0);
  });

  ・・・
});

参考: React Hooks Testing Library

つまり、カスタムフック化すれば、ロジックのみの単体テストをすることができます
コンポーネントのテストとロジックのテストを分けることができるのは、カスタムフックの大きなメリットですね。

3章 - カスタムフックの公式ルール

カスタムフックには、公式が提唱する2つのルールがあります。

カスタムフックとは、名前が ”use” で始まり、ほかのフックを呼び出せる JavaScript の関数のことです。

参考: 独自フックの作成 - カスタムフックの抽出

1. 関数名はuseから始める

React hooksは、すべてuse〇〇から始まります。
そのため、自作のReact hooksであるカスタムフックも必ずuse〇〇という命名ルールに従う必要があります。
(カスタムフックがこの命名規則に従っていない場合、linterに怒られます。)

2. フックを呼び出している関数であること

フックは関数コンポーネントまたはカスタムフックの中でしか使うことができません。
つまり、フックは通常の関数に比べて使える範囲が狭いです。

そのため、わざわざフックを呼び出していない関数をフックとして扱う必要はないため、
カスタムフックは、フックを呼び出している関数である必要があります。

4章 - カスタムフックの設計や実装する上で気をつけること

冒頭でもお話しした通り、カスタムフックにはほとんど制約がありません。
3章で説明したこと以外は、自由に定義することができます。

一方で、設計に迷ったり、開発者個人の色がでてしまうことが頻繁に起きてしまう可能性もあります。

そのような時に気をつけることを2点ご紹介します。

1. 戻り値にルールを設ける

一番個人によってバラバラになりやすいのが、戻り値です。
そのため、戻り値に関してはプロジェクトでルールを設けることを推奨します。

プロジェクトで共通認識が取れていれば独自ルールで問題ないと思いますが、
一般的に使われている2パターンを紹介いたします。

パターン1: React hooksのスタイルに合わせる

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

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

参考: ReactのカスタムHooksをカジュアルに使ってコードの見通しを良くしよう

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

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

先ほどの例も、このパターンに添った戻り値になっています。

hooks.js
export const useCounter = () => {
  ・・・

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

パターン2: オブジェクトですべて返す

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

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

  • 返却値を増やしたい時に変更がいらない
  • コンポーネントで呼び出す際に、返却値の命名が変更されることがない
  • テスト時に値を取りやすい (result.current.〇〇でとれる)

先ほどの処理をこのパターンに書き換えるとこのようになります。

hooks.js
export const useCounter = () => {
  ・・・

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

2. メモ化を意識する

カスタムフックを作る時には、レンダリングの最適化のためにメモ化を意識することを推奨します。

Reactでは、コンポーネントの再レンダリングと共に、値や関数も新しく作られてしまいます。
そのため、値や関数をキャッシュさせ、再レンダリングのときにキャッシュされた値や関数を返すようにすることで、レンダリングの最適化ができます。

関数のメモ化(useCallback)

useCallbackは関数をメモ化するためのフックです。
下記ブログの通り、関数を返すカスタムフックに関してはすべてuseCallbackで囲ってあげるのがよいでしょう。

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

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

  // メモ化
  const incrementCount = useCallback(() => setCount(() => count + 1), []);

  // メモ化
  const decrementCount = useCallback(() => setCount(() => count - 1), []);

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

値のメモ化(useMemo)

useMemoは実行結果や値をメモ化することができるフックです。
カスタムフック内で配列操作などの重い処理を実行した結果を返す場合に使うことができます。

hooks.js
import { useContext } from "react";
import { usersContext } from "./Users";

/* usersContextで配布されるユーザーデータは下記である
  const users = [
    {
      id: 1,
      name: "リアクト太郎"
    },
    {
      id: 2,
      name: "リアクト二郎"
    },
    ・・・
  ]
*/

export const useUserNames = () => {
  const users = useContext(usersContext)

  // 配列操作の実行結果をメモ化
  const userNames = useMemo(() => users.map(({name}) => name), [users])

  return { userNames }  
};

結論

  • ほとんど全てのフックをカスタムフックとして処理を切り出すべき
    • 複数のReact hooksをまとめることができるため
    • View(コンポーネント)とロジックが分離できるため
      • コンポーネントの関数の肥大化の防止
      • ロジックの再利用性の向上
      • テスタビリティの向上
  • カスタムフックの公式ルールはたったの2点
    • フックを含む関数であること
    • 命名はuse〇〇
  • カスタムフックの戻り値のルールはプロジェクト共通認識をとるべき
  • メモ化意識 忘れずに
445
298
3

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
445
298

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?