2020.05.10

React入門チュートリアル (7) カスタムフック


この連載記事は、これから React を学びたい JavaScript 開発者のための入門コンテンツです。対象とする React のバージョンは執筆時点で最新の v16.13 です。

1️⃣ Reactとは何か
2️⃣ JSX
3️⃣ 属性と状態
4️⃣ フォームとイベントハンドリング
5️⃣ ToDoアプリを作ってみよう
6️⃣ 副作用
7️⃣ カスタムフック
8️⃣ Reactプロジェクトを始める方法

⚠ 理解を助けることを意図としているため、網羅的、リファレンス的な解説はしていません。ドキュメントを併読すると、さらに理解が深まると思います。

本章では、カスタムフックを作成してロジックを共通化する方法について学びます。

Hook は作れる!

ここまでの章で、useStateuseEffect の二つのフック関数を紹介しましたが、カスタムフックを自作することもできます。

カスタムフックの作成には、以下のメリットがあります:

  • UI と切り離したロジックの抽出を可能にする。
  • コンポーネント関数の肥大化を抑える。
  • 複数のフック呼び出しを論理的な単位でまとめられる。

カスタムフックの作成

関数名が use で始まり、他のフック関数を含む関数は、カスタムフックとみなされます。

例を見てみましょう。

例 1

前章の練習問題 2 で、Tippy の組み込みを行いましたが、実際、解答例のコードには少し無理があります。ボタン以外を表現したい場合など考えると、融通の効かない抽象化です。

Before
function TooltipButton({ id, content, children }) {
  React.useEffect(() => {
    tippy(`#${id}`, { content });
  }, [id, content]);

  return <button id={id}>{children}</button>;
}

カスタムフックを使えば、useEffect の部分のみ抽出できます。

After
function useTooltip(id, content) {
  React.useEffect(() => {
    tippy(`#${id}`, { content });
  }, [id, content]);
}

function App() {
  useTooltip('myButton', 'Hello world!');
  useTooltip('myParagraph', 'This is another tooltip.');

  return (
    <>
      <button id="myButton">Hover me</button>
      <p>
        <span id="myParagraph">Hover me too!</span>
      </p>
    </>
  );
}

See the Pen React Tutorial Hook Sample 1 by Masahiro Harada (@MasahiroHarada) on CodePen.

特定の UI からロジックが切り離され、再利用性が向上しました。

例 2

次に、前章の練習問題 1 の解答例を、カスタムフックを使ってリライトします。

まずこちらがリライト前です。改善ポイントは、「ユーザー情報を取得する」ロジックが複数のフック関数実行に分かれている点です。

Before
function Users() {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    (async () => {
      const response = await fetch(API).then(res => res.json());
      setUsers(response.results);
    })();
  }, []);

  return (
    // ...
  );
}

上の例はまだ大丈夫ですが、コードが複雑になり、複数のロジックの塊を持つと、コンポーネント関数が肥大化して、どこからどこまでが何のロジックなのか判別が難しくなります。

そこで、カスタムフックを使って一連のロジックをまとめます。

After
function useUsers() {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    (async () => {
      const response = await fetch(API).then(res => res.json());
      setUsers(response.results);
    })();
  }, []);

  return users;
}

function Users() {
  const users = useUsers();

  return (
    // ...
  );
}

See the Pen React Tutorial Hook Sample 2 by Masahiro Harada (@MasahiroHarada) on CodePen.

コンポーネントはスッキリしましたね。

さらにこの例では、コンポーネント側が状態更新関数 setUsers を参照する必要がなかったので、カスタムフックに隠蔽された、知らなくていいことを管理しなくてよくなったという意味で、カプセル化が進んだとも言えます。

フック関数を含むロジックを抽出するときは、カスタムフックを作成する、と覚えましょう。

なぜカスタムフックを使うのか?

フックのルールを思い出しましょう。

  1. フックを呼び出すのはトップレベルのみ
  2. フックを呼び出すのは React の関数内のみ

2点目のルールは、以下の2パターンを含みます。

  • React の関数コンポーネント内
  • カスタムフック内

要するに、フック関数を含む一連の処理をコンポーネントから分離して抽象化したい場合は、カスタムフックを作成する必要がある、ということです。

練習問題

問題 1

以下のコードを、カスタムフックを用いて書き換えてください。

function mockApi() {
  return new Promise(ok => {
    setTimeout(() => ok([
      { id: '10dU7AN7xsi1I4' },
      { id: 'tBxyh2hbwMiqc' },
      { id: 'ICOgUNjpvO0PC' },
      { id: '33OrjzUFwkwEg' },
      { id: 'MCfhrrNN1goH6' },
      { id: 'rwCX06Y5XpbLG' }
    ]), 1000);
  });
}

function Gifs() {
  const [gifs, setGifs] = React.useState([]);
  const [slider, setSlider] = React.useState(null);

  React.useEffect(() => {
    (async () => {
      const response = await mockApi();
      setGifs(response);
    })();
  }, []);

  React.useEffect(() => {
    const instance = new Swiper('#slider', {
      spaceBetween: 10,
      slidesPerView: 2,
      navigation: {
        nextEl: '.swiper-button-next',
        prevEl: '.swiper-button-prev',
      }
    });

    setSlider(instance);
  }, []);

  React.useEffect(() => {
    if (gifs.length > 0) {
      slider.update();
    }
  }, [gifs]);

  return (
    <div id="slider" className="swiper-container">
      <div className="swiper-wrapper">
        {gifs.map(gif => (
          <img
            key={gif.id}
            className="swiper-slide"
            src={`https://media.giphy.com/media/${gif.id}/giphy.gif`}
            alt=""
          />
        ))}
      </div>
      <div class="swiper-button-prev"></div>
      <div class="swiper-button-next"></div>
    </div>
  );
}

const root = document.getElementById('root');
ReactDOM.render(<Gifs />, root);

See the Pen React Tutorial Quiz 7.1 by Masahiro Harada (@MasahiroHarada) on CodePen.

🤫 ヒント

  • GIF の取得を行う useFetchGifs と、Swiper の初期化および更新処理を行う useSlider の二つのカスタムフックを作成します。
  • mockApi は非同期通信を模した関数ですので、そのままにしてください。
  • Gif コンポーネントの冒頭は以下のようになります。
function Gifs() {
  const gifs = useFetchGifs();
  const slider = useSlider('#slider', gifs);

  // 以下略

🙌 解答例

こちらの CodePen を参照してください。

連載

  1. Reactとは何か
  2. JSX
  3. 属性と状態
  4. フォームとイベントハンドリング
  5. ToDoアプリを作ってみよう
  6. 副作用
  7. カスタムフック
  8. Reactプロジェクトを始める方法