ReactでHooks APIが登場したのは2019年2月。現在では当たり前のように使われているHooksですが、みなさんは正しく使いこなせているでしょうか? 本記事ではHooks APIの基本的な使い方から、注意点まで説明します。
useStateとは
HooksはReactバージョン16.8で追加された新機能です。次のコードは、ボタンをクリックすると数値が増えるカウンターを作成するコンポーネントです。数値はReactのstate変数を使って管理されています。
import React, { useState } from "react";
export const CounterComponent = () => {
  // state を追加
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{count}</p>
      {/* クリックすると count が増える関数 */}
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        クリック
      </button>
    </div>
  );
};
上記コードのuseState()という関数がHooksです。この関数でReactのstate変数を宣言します。
useState()の返り値ではステートと、ステートを更新するための関数を配列にして得られます。変数の名前は任意ですが、更新するための関数は「set○○」といった形式にする場合が多いです。ステートには数値や文字列、オブジェクトなど、任意の値を初期値として渡せます。
key属性を使ったstateのリセット
Reactではコンポーネントにkey属性を付与できます。このkey属性を使うことで、コンポーネントの中のstateをリセットできます。
以下の例では親のコンポーネントがversionというstateを持っています。このversionを子どものコンポーネントのkey属性に渡すとversionが更新されたタイミングで子どものコンポーネント内のstateをリセットできます。
// 親のコンポーネント
export const ParentComponent = () => {
  // versionを保持する
  const [version, setVersion] = useState(0);
  return (
    <>
      {/* ChildComponentのkey属性にversionを渡す。versionが更新されるとChildComponentのstateがリセットされる */}
      <ChildComponent key={version} />
      <button onClick={() => setVersion(version + 1)}>Versionをリセット</button>
    </>
  );
};
// 子どものコンポーネント
const ChildComponent = () => {
  // このstateがリセットされる
  const [name, setName] = useState("Hana");
  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <p>Hi, {name}!</p>
    </div>
  );
};
[Versionをリセット]ボタンをクリックすると、inputに入力した値がリセットされるのがわかります。

この機能はReact公式ページで、stateが残ってしまうよくある問題を解決する手段として紹介されてます。『props が変更されたときにすべての state をリセットする』に解説があるので、ご確認ください。
useEffectとは
useEffectは、コンポーネントを外部システムと同期させるために使います。たとえばsetInterval()等のタイマー、window.addEventListener()等のイベントを指します。
useEffect には引数を2つ指定する必要があります。第1引数には実行したい関数を指定します。第2引数の配列に渡した値が変更されるたびに第1引数の関数を実行します。ここで指定した関数はコンポーネントのレンダリング時・更新時に実行されます。
実行タイミングを第2引数で制御する
useEffectの第2引数に空配列を渡すと、コンポーネントのマウント時およびアンマウント時にのみ実行されます。
▼ コンポーネントのマウント時およびアンマウント時にconsole.log()を実行する
import React, { useEffect } from "react";
export const AlwaysMountComponent = () => {
  // コンポーネントのマウント時およびアンマウント時にconsole.log() を呼び出します。
  useEffect(() => {
    console.log("useEffect: コンポーネントが更新されました");
  }, []);
  return <div>useEffectの第2引数は空配列を渡せます</div>;
};
※React 18以降のStrict Modeでは、マウント時とアンマウント時のuseEffectは二度実行されます。学びはじめで不安な方は、ReactのStrict Modeを無効にすると学びやすいでしょう。
useEffectのクリーンアップ
useEffectには戻り値に関数を指定できます。ここで指定した処理を「クリーンアップ関数」と呼び、コンポーネントのアンマウント時に実行されます。次のコードは、setInterval()を使った1秒おきに数値が増えるコンポーネントです。コンポーネントの破棄時にclearInterval()を実行し、タイマーを解除しています。
import React, { useState, useEffect } from "react";
export const UseEffectComponent = () => {
  const [isView, setIsView] = useState(true);
  const handleToggleView = () => {
    setIsView(!isView);
  };
  return (
    <div>
      <div className="box">{isView && <CountComponent />}</div>
      <button className="button" onClick={handleToggleView}>
        数値の表示を切り替える
      </button>
    </div>
  );
};
const CountComponent = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const countUp = setInterval(() => {
      setCount((count) => count + 1);
      console.log("カウントが1アップしました");
    }, 1000);
    // 戻り値の指定。
    // ここでは、コンポーネントがアンマウントされたときにタイマーを解除しています
    return () => {
      console.log("コンポーネントがアンマウントされました");
      clearInterval(countUp);
    };
  }, []);
  return <p className="num">{count}</p>;
};
「数値の表示を切り替える」ボタンを押してCountComponentを非表示にすると、タイマーが解除されていることがわかります。

クリーンアップ関数でclearInterval()を実行しないと、要素が非表示になっても内部的にsetInterval()の処理が実行されてしまうため、パフォーマンスに影響を与えてしまいます。
useEffect内でstateを更新しないようにする
以下は一見問題ないように見えますが、useEffect内でstateを更新するのはアンチパターンです。
import React, { useEffect, useState } from 'react';
export const Counter => {
  const [count, setCount] = useState(0);
  // countが変更されるたびにuseEffectが呼び出される
  useEffect(() => {
    setCount(count + 1);
  }, [count]);
  return <div>{count}</div>;
}
このとき、コンソールには以下のような警告が発生しています。
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn’t have a dependency array, or one of the dependencies changes on every render.
useEffectの依存配列には、コード内で参照されるすべてのリアクティブな値を指定します。この場合countはリアクティブでuseEffect()内で参照されているので、依存配列に入れる必要があります。
依存関係にある値(count)が更新されるとuseEffectが呼び出されます。そしてcountはuseEffectの関数が実行されるたびに更新されます。そのため、
- useEffectの処理を実行
- countの数値を更新
- countが更新されたので- useEffectが再度実行
…という無限ループに陥ってしまうのです。
今回のようにuseEffect内で以前のstateの値を使用したい場合は、更新用関数を使います。setCount((count) => count + 1)のように、更新用関数で以前の値を受け取り、それを元に更新すればcount自体を依存配列から削除できます。
import React, { useEffect, useState } from 'react';
export const Counter => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    // 更新用関数をsetCountに渡す
    setCount((count) => count + 1);
  }, []); // 依存配列からcountが削除できるので無限ループがなくなる
  return <div>{count}</div>;
}
useCallbackとは
Reactには、メモ化という仕組みが存在します。これは関数の実行時に前回実行された関数と入力された引数を比較し、変更がなければ前回の実行によって得られた値をそのまま返すというものです。Reactはこれにより、関数の実行の効率化を行っています。
useCallbackはメモ化したコールバック関数を返すHooksです。Reactでコールバック関数を利用する際に、通常通り書いてしまうとコンポーネントが更新されるたびに新しい関数インスタンスが発行されてしまい、パフォーマンスに悪影響となります。正しい用途で使用すれば、コンポーネントの不要な再レンダリングを防げます。
また、再レンダリングを避けるには、子コンポーネントもメモ化してあげる必要があります。Reactのメモ化は、コールバック関数だけでなくコンポーネントにも適用可能です。コンポーネント全体をmemo()関数で囲ってあげると、そのコンポーネントをメモ化できます。次のコードは、ChildComponent.jsをmemo()で囲ったものです。
import React, { memo, useState, useCallback } from "react";
// memoでラップ
const ChildComponent = memo(({ name, handleClick }) => {
  console.log(`子コンポーネント「${name}」が再レンダリングされました`);
  return <button onClick={handleClick}>{name}を+1</button>;
});
/**
 * 数値を表示するコンポーネントです。
 * 子コンポーネントに、クリックすると
 * 数値が増えるボタンを持っています。
 */
// Counterコンポーネント(親)
export const UseCallbackComponent = () => {
  // ふたつの state を用意
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  // count1 のセット用関数
  // useCallbackで関数をラップし、依存配列には関数内で利用している count1 を入れます。
  const handleClick1 = useCallback(() => {
    setCount1(count1 + 1);
  }, [count1]);
  // count2 のセット用関数
  // useCallbackで関数をラップし、依存配列には関数内で利用している count2 を入れます。
  const handleClick2 = useCallback(() => {
    setCount2(count2 + 1);
  }, [count2]);
  //子コンポーネントを呼び出す
  return (
    <div>
      <div>
        {count1} , {count2}
      </div>
      <div className="buttonGroup">
        <ChildComponent name="カウンタ1" handleClick={handleClick1} />
        <ChildComponent name="カウンタ2" handleClick={handleClick2} />
      </div>
    </div>
  );
};
上記のコードを実行すると、それぞれのボタンを押すと、対応したコンポーネントのconsole.log()のみが実行されることがわかります。

useRefでDOMを参照する
useRef はReactコンポーネントの要素に対して参照(reference)を作成できます。JavaScriptのDOM操作でいうところのdocument.querySelectorのような使い方ができます。おもにReact側でDOMに対する参照を持つために使われることが多いです。useRefをDOM要素への参照として使う場合、マウントされた時に参照が割り当てられ、マウントが解除されるとnullとなります。そのため、useRefの初期値にはマウントされていない時の値としてnullを定義します。
import React, { useRef } from "react";
export const StateComponent = () => {
  // 🔽 h1要素への参照を作成。
  // 初期値として、マウントされていない時の値としてnullを定義
  const refContainer = useRef(null);
  return <h1 ref={refContainer}>参照を確認</h1>;
};
参照したいReactコンポーネントの要素には、ref属性を付与する必要があります。参照した値はcurrentプロパティーで確認できます。以下は、ボタンをクリックするとインプット要素にフォーカスが当たるコードです。
▼インプット要素にフォーカスさせるコード
import React, { useRef } from "react";
export const UseRefComponent = () => {
  const inputEl = useRef(null);
  const handleClick = () => {
    // current の要素を参照
    inputEl.current.focus();
  };
  return (
    <div>
      <input className="input" ref={inputEl} type="text" />
      <button className="button" onClick={handleClick}>
        input要素にフォーカスする
      </button>
    </div>
  );
};

useRefで任意の値を保持する
useRefにはJSX要素の参照だけでなく、任意の値を保持できます。 useRefで作成されるcurrentプロパティは汎用的なコンテナーとして用意されているもので、書き換え可能かつどのような値でも保持できます。
useStateとの違いとして、useRefで保持している値は、変更されてもコンポーネントが再レンダリングされないという特徴があります。「コンポーネントの値を変えたいけど、再レンダリングは行いたくない」というケースで活用すると良いでしょう。次のコードを実行すると、内部の値は変更されていても<p>要素の表示は変わっていないことがわかります。
import React, { useEffect, useRef } from "react";
const UseRefValueComponent = () => {
  // 🔽 useRefに任意の値を保持させる
  const count = useRef(0);
  useEffect(() => {
    // 🔽 レンダリングされたときにuseEffectでconsole.log()を実行
    console.log("レンダリングが実行されました");
  });
  // 🔽 useRefで定義した数値を増やすイベント
  const handleAddCount = () => {
    count.current += 1;
    console.log(`count.currentの値は現在${count.current}です。`);
  };
  return (
    <>
      <button onClick={handleAddCount}>数値を1増やす</button>
      {/* 🔽 useRefの現在値を表示する */}
      <p>{count.current}</p>
    </>
  );
};

useStateで更新したのにレンダリングされない?
ここからはHooksの注意点を説明します。
Reactコンポーネントの状態管理を行うのにほぼ必須といえるState。useStateはそんなステートを関数型で使える便利なHooksです。実は、useStateは注意しなくてはいけない点があります。たとえば以下のようなTODOリストを実装したとします。
import React, { useCallback, useState } from "react";
/**
 * TODOリストを作成するコンポーネントです。
 */
export const TodoList = () => {
  const [lists, setLists] = useState([]);
  const [value, setValue] = useState("");
  // ボタンをクリックしたとき、TODOリストの配列をひとつ増やすイベントハンドラー
  const handleClick = useCallback(() => {
    const id = lists.length;
    // pushメソッドで破壊的に配列を変更
    lists.push({ id, text: value });
    setValue("");
  }, [lists, value]);
  // inputの値をvalueにセットするイベントハンドラー
  const handleChange = useCallback(
    (e) => {
      setValue(e.target.value);
    },
    [setValue]
  );
  return (
    <div>
      <h1>TODO LIST</h1>
      <button onClick={handleClick}>タスクを追加</button>
      <input value={value} onChange={handleChange} />
      {lists.map((list) => (
        <div key={list.id}>
          list.text
        </div>
      ))}
    </div>
  );
};
useStateは既存のStateと状態更新関数で渡ってきた新しい値を比較し、違いがあれば更新を行います。ですが、現在のstateの配列にsplice()やpush()等の破壊的なメソッドで変更を加えてから渡し直しても画面の再描画が起こりません。
現在の状態を表す配列を直接変更するのではなく、別の配列を新たに生成してuseStateの更新関数に渡すようにすれば正しく更新されます。
  // ボタンをクリックしたとき、TODOリストの配列をひとつ増やすイベントハンドラー
  const handleClick = useCallback(() => {
    const id = lists.length;
    // 新しく配列を作成して、更新関数に渡す
    const newList = [...lists, { id, text: value, isComplete: false }];
    setLists(newList);
    setValue("");
  }, [lists, value]);
useLayoutEffectは使った方がいい?
現在提供されているHooksには、useEffectとuseLayoutEffectという似たようなものがあります。どのようなときに使い分ければよいのでしょうか?
useLayoutEffectはuseEffectとほぼ同じ動作を行いますが、useLayoutEffectはコールバック関数内の処理とstateの更新がブラウザが画面を再描画する前に処理されることを保証するところにおおきな違いがあります。useEffectはDOMの変更を待たずに非同期で呼び出されるために、コールバック関数の処理が完了する前のDOMがユーザーに一瞬見えてしまうことがあります。それに対しuseLayoutEffectはDOMの変更を待ってから同期的に処理が行われるため、画面に不整合が生じません。
しかし残念ながらデメリットもあります。同期的に実行されるということは、画面描画完了までの時間が長くなってしまうということです。結果的にどうしてもユーザーへのページ表示が遅くなってしまい、ユーザーの体験を損なってしまいます。どうしても都合が悪い時など以外は useEffectの方を使ったほうが無難でしょう。
公式ドキュメントでも、基本的にuseEffectの方を使うことが推奨されています。
また、プロジェクトでサーバーサイドレンダリングを使用している場合は、useEffectおよびuseLayoutEffectのどちらもJavaScriptのダウンロードが完了するまで動作しないため、注意しましょう。
useLayoutEffectはJavaScriptの処理にユーザーによるクリック操作が実行に必要なケースなどに役立ちます。
- 例:javascript - useEffect triggered by recoil state doesn’t work as expected in safari - Stack Overflow
useMemoとuseCallbackの違い
useMemoとuseCallbackは両方とも実行する処理を第一引数に、監視対象となる値の配列を第二引数に持ちます。
useMemoは一度計算した結果を変数に保持してくれる「メモ化」を行います。最初のレンダリングで一度作業を行い、第二引数に渡した依存する値が変化しない限りはキャッシュされたものを返します。それに対してuseCallbackは、第一引数に渡しているコールバック関数をメモ化し、不要な再レンダリングを防いでくれます。
useMemoは大きなデータ処理に、useCallbackは関数に依存関係を追加して不要なレンダリングを抑制したいときに使用するとよいでしょう。また、useCallbackでメモ化しているコールバック関数は、メモ化しているコンポーネントに渡して利用することで正しく機能します。作成したコンポーネント自身で利用しても意味がないので気をつけましょう。
// NG: メモ化したコンポーネントに渡さず、そのまま利用
const Component = () => {
  const handleClick = useCallback(() => { ... }, []);
  return (
    <button onClick={handleClick}>Click!</button>
  );
};
// OK: メモ化したコンポーネントに渡して利用
const Component = () => {
  const handleClick = useCallback(() => { ... }, []);
  return (
    <ChildButtonComponent handleClick={handleClick} />
  );
};
const ChildButtonComponent = React.memo(({ handleClick }) => {
  return (
    <button onClick={handleClick}>Click!</button>
  );
});
Hooksのお約束
Reactは、コンポーネントで呼び出されるHooksは必ず同じ順番・同じ回数で呼び出されることをルールとしており、場合によってHooksの順番や実行回数が変わることを禁止しています。そのため、「このケースのときはHooksを実行したくない」といった書き方はできません。
▼ 以下のような書き方はできない
import React, { useState } from "react";
export const StateComponent = () => {
  const [value, setValue] = useState("");
  setValue("hoge");
  if (value === "hoge") {
    const [value2, setValue2] = useState("huga");
  } else {
    const [value2, setValue2] = useState("foo");
  }
  // ...省略...
};
状況によって処理を分岐させたいときは、Hooks関数の内部で行うようにしましょう。
意外と便利、useDebugValue
その名の通り、useDebugValueはデバッグに使えるフックです。Reactの開発を進めていくと、基本のHooksを組み合わせた独自のカスタムフックを作成することも多いです。useDebugValueは、カスタムフックのデバッグ情報をReactの開発者ツール用拡張機能(React Dev Tools)に表示させることができます。useDebugValueはどのカスタムフックから呼び出されたのか検知し、対応するカスタムフックの横に指定したデバッグ情報を表示します。
React用拡張機能で見ると下の画像のように表示されています。
▼開発者ツールで確認した図

useDebugValueフックで指定した値がカスタムフックのデバッグ情報として表示されていることがわかります。
まとめ
HooksはReactでのアプリケーション開発において、頻繁に使用するようになりました。ですが、正しく使用しないとパフォーマンスが落ちてしまったり無限ループバグの温床となってしまう可能性もあります。
公式でeslint-plugin-react-hooksという、Hooksをルールどおり正しく使えているかどうかをチェックするプラグインを提供してくれていますので、これを活用するとよいでしょう。
※この記事が公開されたのは4年前ですが、半年前の4月に内容をメンテナンスしています。

