123
115

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【保存版】React Hooks 使用上の注意点が多すぎ問題 _(┐「ε:)_

Last updated at Posted at 2024-10-07

react_hooks_top.png

こんにちは、とまだです。

React を使っている方のほとんどが、React Hooksの存在を知っていると思います。

useStateuseEffectuseContext...などなど、Hooks はたくさんありますよね。

ただ、それぞれの Hooks をなんとなく使っている方も多いのではないでしょうか?

(少なくとも私は、最初はuseStateぐらいしか理解し切れていませんでした...)

React Hooks は強力なものの、種類が多くて使い分けが難しかったり、うっかりパフォーマンスが低下することもあるので注意が必要です。

今回は過去の自分(もしくは皆さん)のために、今回は主な Hooks のポイントを、日常生活での例え話を交えて解説していきます!

※以下の記事とあわせて読むことで、より理解が深まるかもしれません。

1. State Hooks:コンポーネントに記憶力を与える

react_hooks_state.png

まずは一番よく使われるであろう、State Hooks から見ていきましょう。

State Hooks は、コンポーネントが何かを覚えておくための道具です。例えば、ボタンを押した回数を覚えておいたり、テーマの設定を覚えておいたりすることができます。

1-1. useState: 単純な記憶装置

useState は、コンポーネントに「記憶力」を与える Hook です。

メモ帳を使い、ユーザーが入力した内容をメモ帳に書き留めておくようなものです。

使用例

以下のようなコンポーネントを考えてみましょう。

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

上記のコンポーネントは、ボタンをクリックするたびにカウントが増えるシンプルなカウンターです。

このように、ユーザーの操作に応じて値を覚える(保持する) ことができます。

後述しますが、useState の値(count)を更新する際は、setCount という関数を使うことに注意してください。

便利な使いどころ

他にも、以下のような使いどころがあります。
どれも、ユーザーの操作に応じて画面を更新するために使われるものですね。

  • フォームの入力値の管理
  • トグルの状態管理(オン/オフ)
  • 表示/非表示の切り替え
サンプルコード
例 1: フォームの入力値の管理

この例では、フォームの入力値をnameという状態に保持します。
さらに、入力するたびにsetName関数を使って値を更新しています。

この値を使って API にリクエストを送るなど、様々な処理に利用できますので、覚えておきましょう。

function Form() {
  const [name, setName] = useState('');
  return <input type="text" value={name} onChange={(e) => setName(e.target.value)} />;
}
例 2: トグルの状態管理(オン/オフ)

この例では、ボタンをクリックするたびにisOnの値を反転させています。

例えば、ダークモードの切り替えなどに使えますね。

function Toggle() {
  const [isOn, setIsOn] = useState(false);
  return <button onClick={() => setIsOn(!isOn)}>{isOn ? 'ON' : 'OFF'}</button>;
}
例 3: 表示/非表示の切り替え

この例では、ボタンをクリックするとテキストが表示されるか非表示になります。

例えば、ページ内のモーダルの表示/非表示などに使えますね。

function ToggleDisplay() {
  const [isVisible, setIsVisible] = useState(true);
  return (
    <>
      <button onClick={() => setIsVisible(!isVisible)}>{isVisible ? '非表示' : '表示'}</button>
      {isVisible && <p>表示されるテキスト</p>}
    </>
  );
}

注意点

値の更新は setXxxx 関数を使う

しれっと書いてしまいましたが、useState で管理している値を更新する際は、setXxxx という関数を使うことに注意してください。

例えば、count の値を更新する際は、setCount を使います。

const [count, setCount] = useState(0);

これは、React の仕様によるものですので、覚えておきましょう。

仮にこの関数を使わずに値を更新しようとすると、React が正しく動作しない可能性があります。

値の更新は非同期で行われる

setXxxx 関数を使って値を更新すると、その値は非同期で更新され、即座に setXxxx の処理が終わるわけではありません。

このため、以下のようなコードを書いた場合、count の値は常に 1 になってしまいます。

const [count, setCount] = useState(0);

setCount(count + 1); // 「現在の count に 1 を足してね」という依頼を出す
setCount(count + 1); // ここでも「現在の count に 1 を足してね」という依頼を出す

このようなコードだと、次のレンダリング時に setCount が一気に実行され、結果として count は 1 になってしまいます。

【もう少し正確に】
setXxxx はすぐに実行されるわけではなく、次のレンダリング(再描画)時にまとめて実行されます。

そのため、「初期状態の count に 1 を足す」という処理が 2 回行われることになり、最終的に count は 1 になります。
(むずすぎん?)

このような場合は、setCount の引数に関数を渡すことで解決できます。

setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);

上記の prevCount は、現在の count の値を表しています。

prevCount に現在の count の状態が反映されるため、各 setCount での計算が正しく行われるため、期待通りに count が増加します。

定番の書き方なので、難しければ「こういうもの」と覚えてしまっても OK です。

1-2. useReducer: 複雑な記憶装置

useReducer は、useState よりも複雑な状態を管理するための Hook です。

カウンターの例でいえば「1 を足す」以外にも、色々な操作を追加したい場合などに使えます。

複雑なアプリを作る際には、useReducer を使ってコードを整理することで、見通しを良くすることができます。

使用例

以下のようなことができるカウンターを考えてみましょう。

  • increment:カウントを 1 増やす
  • decrement:カウントを 1 減らす
  • double:カウントを 2 倍にする
  • reset:カウントをリセットする

これを useStateだけで書くと以下のようになります。

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>増やす</button>
      <button onClick={() => setCount(count - 1)}>減らす</button>
      <button onClick={() => setCount(count * 2)}>2倍にする</button>
      <button onClick={() => setCount(0)}>リセット</button>
    </>
  );
}

確かにこれで動きますが、操作が増えるにつれてコードが複雑になっていきます。

また、JSX (HTML のような記述) とロジック(処理)が混在してしまうため、見通しが悪くなりがちです。

そこで、useReducer を使ってリファクタリングしてみましょう。

function counterReducer(state, action) {
  switch (action.type) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'double': return state * 2;
    case 'reset': return 0;
    default: return state;
  }

function Counter() {
  // useReducerの第2引数に初期値を渡す
  // (dispatch については後述)
  const [count, dispatch] = useReducer(counterReducer, 0);
  return (
    <>
      <p>{count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>増やす</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>減らす</button>
      <button onClick={() => dispatch({ type: 'double' })}>2倍にする</button>
      <button onClick={() => dispatch({ type: 'reset' })}>リセット</button>
    </>
  );
}

いかがでしょうか?

コード全体としては行数が増えたものの、ロジックと JSX が分離され、見通しが良くなりました。

また、counterReducer という関数でロジックをまとめることで、処理の追加や変更がしやすくなります。

便利な使いどころ

useReducer は以下のような場合に使用を検討すると良いでしょう。

  1. 複数の値が絡む複雑な状態管理が必要な場合
  2. 状態更新のロジックが複雑な場合
サンプルコード
例 1: カウンター(複数の値が絡む場合)

この例では、カウントの値だけでなく、カウントの最大値も管理しています。

カウントが最大値に達したらリセットするような処理を追加するなど、複数の値が絡む場合に使えます。

function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return state.count + 1;
    case 'setMax':
      return { ...state, max: action.payload };
    // ...(略)
  }
}
例 2: カウンター(状態更新のロジックが複雑な場合)

この例では、カウントの値が偶数の場合は 2 倍にし、奇数の場合は 1 増やすという処理を追加しています。

このような複雑なロジックをまとめて管理するのに使えます。

function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return state % 2 === 0 ? state * 2 : state + 1;
    // ...(略)
  }
}

注意点

状態の更新は dispatch 関数を使う

useReducer で管理している状態を更新する際は、dispatch 関数を使うことに注意してください。
useReduce のお作法と思ってもらえれば OK です)

dispatch 関数には、type というプロパティを持つオブジェクトを渡すことで、状態の更新を行います。

const [count, dispatch] = useReducer(counterReducer, 0);

<button onClick={() => dispatch({ type: 'increment' })}>増やす</button>;

dispatch に渡すオブジェクトの type は、counterReducer 関数内の action.type と一致する必要があります。

2. Context Hooks:コンポーネント間で情報を共有する

react_hooks_context.png

次は、Context Hooks について見ていきましょう。

Context Hooks は、コンポーネント間で情報を共有するための道具です。

例えば、アプリ全体のテーマ設定や、ユーザーのログイン状態などを、複数のコンポーネントで共有したい場合に使います。

「ばかでかいメガホンで情報を共有する」というイメージで覚えておくといいかもしれません。

2-1. useContext: 広範囲で使える情報共有の仕組み

useContext は、コンポーネントツリーの中で情報を簡単に共有できるようにする Hook です。

会社の共有フォルダを想像してみてください。一度情報を置いておけば、どの部署からでもアクセスできますよね。

useContext はそのような仕組みを提供してくれます。

大規模なアプリほど、useContext の恩恵を受けやすいのではないでしょうか。

まずは useContext を使わない場合

以下のようなコンポーネントを考えてみましょう。

親側で取得したユーザー名を、子コンポーネントで表示したいとします。

// 親コンポーネント
function ParentComponent() {
  const user = { name: 'Alice' };

  return (
    <div>
      <h1>親やねん</h1>
      <ChildComponent user={user} />
    </div>
  );
}

// 子コンポーネント
function ChildComponent({ user }) {
  return (
    <div>
      <h2>子やねん</h2>
      <p>ユーザー名は{user.name}です。</p>
    </div>
  );
}

この場合、親コンポーネントで取得したユーザー名を、props を使って子コンポーネントに渡しています。
一応、情報の共有はできています。

しかし、この方法だと、親コンポーネントと子コンポーネントに加え、さらにその子コンポーネントにも情報を渡す必要がある場合、コードが複雑になりますよね。

また、中間のコンポーネントが不必要な props を扱う必要が生じることもあります。

こういう情報の受け渡しを「バケツリレー」と呼ぶことがあります。

useContext を使った場合

こんなときに、useContext を使うと便利です。

基本的には以下のような手順で使います。

  1. Context を作成する
  2. 親コンポーネントにて、情報を渡したい子コンポーネントを Context.Provider で囲む
  3. 子コンポーネントで useContext を使って情報を取得する

実際に例を見てみましょう。

// Contextの作成
const UserContext = React.createContext();

// 親コンポーネント
function ParentComponent() {
  const user = { name: 'Alice' };

  return (
    <UserContext.Provider value={user}>
      {' '}
      {/* Context.Providerで囲む */}
      <div>
        <h1>親やねん</h1>
        <ChildComponent /> {/* 子コンポーネントに情報を直接渡さなくてもOK */}
      </div>
    </UserContext.Provider>
  );
}

// 子コンポーネント
function ChildComponent() {
  // useContextを使ってContextの値を取得
  const user = React.useContext(UserContext);

  return (
    <div>
      <h2>子やねん</h2>
      <p>ユーザー名は{user.name}です。</p>
    </div>
  );
}

このように、useContext を使うことで、親コンポーネントを経由せずに、直接情報を取得することができます。

大規模なアプリほど、useContext の恩恵を受けやすいということがわかりますね。

【Context とは】
React のコンポーネントツリー全体で共有される情報を管理するための仕組みです。

【Context.Provider】
情報を提供するためのコンポーネントです。Provider で囲まれた範囲内でのみ、その Context の値を参照できます。

言うなれば、Context.Provider は「情報の共有フォルダ」みたいなものです。
「ばかでかい声が届く範囲」とイメージするといいでしょう。

便利な使いどころ

他にも、以下のような使いどころがあります。
どれも、アプリケーション全体で頻繁に参照される情報であり、多くのコンポーネントで利用する可能性が高いものですね。

  • テーマ設定(ダークモードなど)
  • ユーザーのログイン状態
  • 言語設定

注意点

過剰な使用に注意

あまりにも多くの情報を Context で管理すると、コンポーネントの再利用性が下がる可能性があります。

たとえば、個々のコンポーネントでのみ利用する情報は、props を使って渡す方が適していますね。
(先の例でいうと、仮に isLoggedInがごく一部のコンポーネントでしか使われない場合、Context で管理する必要はないかもしれません)

本当に広範囲で共有する必要がある情報だけを Context で管理するようにしましょう。

Provider の範囲を意識する

Context の Provider コンポーネントで囲まれた範囲内でのみ、その Context の値を参照できます。

たとえば、以下のようなコードでは、ChildComponentUserContext.Providerの外側にあるため、useContext(UserContext)で値を取得できません。

サンプルコード(悪い例)
const UserContext = React.createContext();

// 親コンポーネント
function ParentComponent() {
  const user = { name: 'Alice' };

  return (
    <>
      <UserContext.Provider value={user}>
        <div>
          <h1>親やねん</h1>
        </div>
      </UserContext.Provider>
      <ChildComponent /> {/* ここではContextの値を参照できない */}
    </>
  );
}
値が変わると再レンダリングされる

Context の値が更新されると、それを参照しているすべてのコンポーネントが再レンダリングされます。

そのため、Context の値が頻繁に変わるような情報は、Context で管理するのは避けた方がいいかもしれません。

また同じ理由で、再レンダリングの影響を受けたくないコンポーネントは、Context を使わずに props で情報を渡すようにしましょう。

3. Ref Hooks:レンダリングの外側で何かをする

react_hooks_ref.png

次は、Ref Hooks について見ていきましょう。

Ref Hooks は、コンポーネントのレンダリングに直接関係のない値を保持するための道具です。

例えば、DOM 要素への直接アクセスや、タイマー ID の保持など、React の通常のデータフローの外側で何かを行いたい場合に使います。

ここでいう「通常のデータフロー」とは、props や state を使ってコンポーネント間で情報を受け渡すなど、React の基本的なデータの流れのことを指します。

Ref Hooks には、useRefuseImperativeHandleの 2 つがあります。

3-1. useRef: 変更してもレンダリングが発生しない特別な記憶装置

useRef は、値の変更がレンダリングを引き起こさない特別な記憶装置です。

日記帳をイメージしてみてください。

何か情報を書き留めておくという意味ではメモ帳と同じですが、日記を書いても周りの人に内容を見せるわけではありませんよね。

同じように、useRef も「こっそり」値を保持するための仕組みです。

使用例

以下のようなコンポーネントを考えてみましょう。

function Timer() {
  const intervalRef = useRef(null);
  const [seconds, setSeconds] = useState(0);

  // タイマーを開始(useEffectについては後述)
  useEffect(() => {
    // useRefで作成したrefオブジェクトの情報を1秒ごとに更新
    intervalRef.current = setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);

    // 最後にタイマーをお掃除するクリーンアップ関数を返す
    return () => clearInterval(intervalRef.current);
  }, []);

  return <div>経過時間: {seconds}</div>;
}

このコンポーネントは、1 秒ごとにカウントアップするシンプルなタイマーです。

上記の例では、useRefを使ってintervalRefという ref オブジェクトを作成し、タイマー ID を保持しています。

intervalRef.current は、画面に表示(レンダリング)したい「値」ではなく、タイマー ID のような「裏方の情報」である点に注意しましょう。

ここで intervalRef.current という形で ref オブジェクトの値を更新していますが、この値の変更はレンダリングを引き起こしません。

そのため、タイマー ID のように、画面に直接表示するわけではない情報を保持するのに便利なのです。

サンプルコード(仮にぜんぶuseStateで書いた場合)

一応、useStateだけで書いた場合も考えてみましょう。

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);

    return () => clearInterval(id);
  }, []);

  return <div>経過時間: {seconds}</div>;
}

この場合、setSecondsで値を更新するたびにコンポーネントが再レンダリングされます。

今回、値を更新するたびに変えたいのは seconds の値だけですので、コンポーネント全体を再レンダリングする必要はありません。

そのため、useRefを使ってintervalRefという ref オブジェクトを作成し、タイマー ID を保持することで、不要な再レンダリングを防ぐことができます。

便利な使いどころ

useRef は以下のような場合にも便利です。

  1. DOM 要素への直接アクセス
  2. 以前の値の保持(記憶)
  3. イベントハンドラの保持
サンプルコード
例 1: 入力フィールドにフォーカスを当てる

この例では、ボタンをクリックすると入力フィールドにフォーカスが当たります。

function FocusInput() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>入力欄にフォーカスを当てる</button>
    </>
  );
}
例 2: 以前の値の保持

この例では、ボタンをクリックするたびにカウントアップしますが、以前の値も保持しています。

2 つの state をわざわざ使う必要がなくなり、コードがスッキリします。
(+再レンダリングを引き起こさないので、パフォーマンスも向上します)

function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  useEffect(() => {
    prevCountRef.current = count;
  }, [count]);

  return (
    <>
      <p>前回の値: {prevCountRef.current}</p>
      <p>現在の値: {count}</p>
      <button onClick={() => setCount(count + 1)}>カウントアップ</button>
    </>
  );
}
例 3: イベントハンドラの保持

「イベントハンドラ」とは、たとえばボタンをクリックしたときに実行される関数のことです。

この例では、ボタンをクリックするとアラートが表示されます。

function AlertButton() {
  const handleClick = () => {
    alert('ボタンがクリックされました!');
  };

  const buttonRef = useRef();

  useEffect(() => {
    buttonRef.current.addEventListener('click', handleClick);
    return () => {
      buttonRef.current.removeEventListener('click', handleClick);
    };
  }, []);

  return <button ref={buttonRef}>クリックしてね</button>;
}

上記の例では、useRefを使ってbuttonRefという ref オブジェクトを作成し、ボタン要素にアクセスしています。

useEffectを使って、コンポーネントがマウントされたときにイベントリスナーを追加し、アンマウントされたときに削除しています。

少し高度な使い方ですが、イベントハンドラの保持にも使えることがわかりますね。

注意点

ref の値の変更はレンダリングを引き起こさない

先述の通り、useRef で作成した ref オブジェクトの値を変更しても、コンポーネントは再レンダリングされません。

逆にいうと、ref の値を変更したときに画面を更新したい場合は、別途 state を使う必要がありますので注意しましょう。

  • useState:値の変更がレンダリングを引き起こす
  • useRef:値の変更がレンダリングを引き起こさない

この違いを理解しておくと、コンポーネントの設計がスムーズに進むかもしれません。

3-2. useImperativeHandle: 親コンポーネントに公開するメソッドを制御する

useImperativeHandle は、少し変わった使い方の Hook です。

子コンポーネントが持つメソッドや値を、親コンポーネントに制限付きで公開したい場合に使います。

言うなれば、子ども部屋にある鍵付き机のうち、一部の引き出しを親に公開するようなイメージです。

少し高度な使い方ですので、初心者の方は一旦スルーしても大丈夫です。

使用例

以下のような親子コンポーネントを考えてみましょう。

// 子コンポーネント
const ChildInput = React.forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
  }));

  return <input ref={inputRef} />;
});

// 親コンポーネント
function ParentComponent() {
  const childRef = useRef();

  const focusChild = () => {
    childRef.current.focus();
  };

  return (
    <>
      <ChildInput ref={childRef} />
      <button onClick={focusChild}>子コンポーネントにフォーカス</button>
    </>
  );
}

この例では、親コンポーネントから子コンポーネントの入力フィールドにフォーカスを当てることができます。

ただし、親コンポーネントは子コンポーネントのfocusメソッドにしかアクセスできません。
これにより、子コンポーネントの内部実装を隠蔽しつつ、必要な機能だけを公開することができます。

「別に全部公開してもいいんじゃない?」と思うかもしれません。

しかし、親が子コンポーネントの内部実装に依存すると、コンポーネント間の結合度が高くなり、コードの保守性が下がる可能性があります。
そこで、useImperativeHandle を使って、必要な機能だけを公開することで、メンテナンス性を高めることができます。

何か具体的な画面・処理のために使いというよりは、コードの保守性を高めるための手段として使われることが多いです。

便利な使いどころ

少しイメージしにくいかもしれませんので、具体的な使いどころを見てみましょう。

サンプルコード(親から子を操作する必要がある場合)
例: モーダルの表示・非表示

この例では、親側でボタンをクリックするとモーダルを開いたり閉じたりできるようにしています。

一方で、モーダルの内部実装は子コンポーネントに隠蔽されています。
また、モーダルの内部実装が変わっても、親コンポーネントは影響を受けません。

言うなれば、親は「モーダルを開いてね」とだけ伝えれば、子は「モーダルを開く方法」を自由に決められるというわけです。
(目的を達成する手段は問わない)

// 子コンポーネント
const Modal = React.forwardRef((props, ref) => {
  const [isOpen, setIsOpen] = useState(false);

  useImperativeHandle(ref, () => ({
    open: () => setIsOpen(true),
    close: () => setIsOpen(false),
  }));

  return (
    <div style={{ display: isOpen ? 'block' : 'none' }}>
      <p>モーダルの中身</p>
    </div>
  );
});

// 親コンポーネント
function ParentComponent() {
  const modalRef = useRef();

  const openModal = () => {
    modalRef.current.open();
  };

  const closeModal = () => {
    modalRef.current.close();
  };

  return (
    <>
      <Modal ref={modalRef} />
      <button onClick={openModal}>モーダルを開く</button>
      <button onClick={closeModal}>モーダルを閉じる</button>
    </>
  );
}

注意点

過度の使用は避ける

useImperativeHandle は強力ですが、多用するとコードの可読性が下がり、コンポーネント間の結合度が高くなる可能性があります。

慣れないうちは、props や Context を使ってコンポーネント間の情報共有を行う方が安全かもしれません。

forwardRef と組み合わせて使用する

useImperativeHandle は通常、React.forwardRefと組み合わせて使用します。これにより、親コンポーネントから子コンポーネントに ref を渡すことができます。

4. Effect Hooks:コンポーネントと外部とのやり取り

react_hooks_effect.png

さあ、Hooks も後半戦です!

続いて、Effect Hooks について見ていきましょう。

Effect Hooks は、コンポーネントが外部とのやり取りするための道具です。

例えば、API からデータを取得したり、DOM を直接操作したり、タイマーをセットしたりする場合に使います。

4-1. useEffect: コンポーネントの副作用を管理する

useEffect は、コンポーネントの副作用(side effects)を扱うための Hook です。

「副作用」と聞くと難しそうですが、要は「レンダリング以外のこと」と考えてください。

例えば、お風呂に入るときを想像してみてください。
お風呂に入ること自体が主目的ですが、その前後にタオルを用意したり、髪を乾かしたりしますよね。
これらの「ついでにやること」が副作用です。

使用例

以下のようなコンポーネントを考えてみましょう。

function DataFetcher() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('https://api.example.com/data')
      .then((response) => response.json())
      .then((data) => setData(data));
  }, []);

  if (!data) return <div>Loading...</div>;
  return <div>{data.title}</div>;
}

このコンポーネントは、マウント時に API からデータを取得し、それを表示します。

useEffectを使うことで、コンポーネントのレンダリング後にデータ取得を行うことができます。

useEffectは、コンポーネントのレンダリング後に実行されるため、API からデータを取得するなどの非同期処理に適しています。

逆にいうと、useEffectを使わない場合、コンポーネントがレンダリングされるたびにデータを取得してしまうことになります。

サンプルコード(useEffectを使わない場合)
function DataFetcher() {
  const [data, setData] = useState(null);

  fetch('https://api.example.com/data')
    .then((response) => response.json())
    .then((data) => setData(data));

  if (!data) return <div>Loading...</div>;
  return <div>{data.title}</div>;
}

これでもデータを取得できますが、コンポーネントがレンダリングされるたびにデータを取得してしまうため、パフォーマンスが悪くなります。

便利な使いどころ

useEffect は以下のような場合に特に便利です。

  1. データの取得
  2. DOM の直接操作
  3. タイマーやイベントリスナーの設定

これらをコンポーネントのレンダリング後に行いたい場合に、useEffect を使うと便利です。

サンプルコード
例: ウィンドウサイズの監視

この例では、ウィンドウサイズの変更を監視し、現在のサイズを表示します。

実際のアプリでも「ウィンドウサイズに応じてレイアウトを変える」ということはよくあるかと思います。

function WindowSizeTracker() {
  // useStateでウィンドウサイズを管理
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    // ウィンドウサイズが変更されたときにstateを更新
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    // イベントリスナーを追加
    window.addEventListener('resize', handleResize);

    // クリーンアップ関数
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空の依存配列で、マウント時にのみ効果を適用(後述)

  return (
    <div>
      Window size: {windowSize.width} x {windowSize.height}
    </div>
  );
}

注意点

依存配列を適切に設定する

useEffect の第二引数である依存配列は、effect が再実行されるタイミングを制御します。

useEffect(() => {
  // エフェクトのロジック
}, [依存する値]); // この値が変更されたときのみ再実行

たとえば、入力フォームに入力された値に応じてデータを取得する場合、以下のように書くことができます。

useEffect(() => {
  fetch(`https://api.example.com/data?query=${inputValue}`)
    .then((response) => response.json())
    .then((data) => setData(data));
}, [inputValue]);

空の配列[]を渡すと、コンポーネントのマウント時にのみ effect が実行されます。
初回のレンダリング時にのみ実行したい処理には、空の配列を渡すと便利です。

また、依存配列を省略した場合には、毎回のレンダリング後に effect が実行されます。

適切な依存配列を設定することで、不要な effect の再実行を防ぎ、パフォーマンスを向上させることができます。

ここをミスると、急激にパフォーマンスが低下する可能性があるので、注意しましょう!!!!!

クリーンアップ関数を適切に実装する

useEffect で設定したタイマーやイベントリスナーは、コンポーネントのアンマウント時にクリーンアップする必要があります。

クリーンアップとは、リソースの解放や後始末をすることです。

たとえば、タイマーをセットした場合、コンポーネントがアンマウントされたときにタイマーを解除しないと、メモリリーク(メモリの過剰消費)が発生する可能性があります。

以下は、クリーンアップ関数を実装する例です。

useEffect(() => {
  const timerId = setInterval(() => {
    // 何か処理
  }, 1000);

  return () => {
    clearInterval(timerId); // クリーンアップ関数
  };
}, []);

5. Performance Hooks:パフォーマンスを向上させる

react_hooks_performance.png

次は、Performance Hooks について見ていきましょう。

Performance Hooks は、アプリケーションのパフォーマンスを向上させたり、確実に同じ値を返す関数を最適化したりするための道具です。

例えば、複雑な計算の結果をキャッシュしたり、コールバック関数を最適化したりする場合に使います。

5-1. useMemo: 計算結果をキャッシュする

useMemo は、計算コストの高い処理の結果をキャッシュ(記憶)するための Hook です。

例えば、九九を覚えるときを想像してみてください。
「さんご (3×5)」の答えは 15 ですが、毎回「3+3+3+3+3」を計算するのは面倒ですよね。
そこで、「さんご => 15」という計算結果を覚えておけば、次回からは計算せずに使い回すことができます。

useMemo も同じように、「計算結果を覚えておく」ための仕組みです。

使用例

以下のようなコンポーネントを考えてみましょう。

function ExpensiveComponent({ data }) {
  const expensiveResult = useMemo(() => {
    // 時間がかかる計算(dataに2をかけ続ける1,000,000,000回のループ)
    let result = 0;
    for (let i = 0; i < 1000000000; i++) {
      result = data * 2;
    }
    return result;
  }, [data]); // dataが変更されたときのみ再計算

  return <div>Result: {expensiveResult}</div>;
}

このコンポーネントは、入力データをもとに時間がかかる計算を行っています。

こんな計算を毎回レンダリングのたびに行うと、パフォーマンスが低下してしまいますよね。

そこでuseMemoを使うことで、dataが変更されない限り、以前の計算結果を再利用できます。

便利な使いどころ

useMemo は以下のような場合に特に便利です。

  1. 計算コストの高い処理の結果をキャッシュする
  2. 入力データが変わらない限り、同じ値を返す関数を最適化する

サンプルコードを見てみましょう。

サンプルコード
例: リストのフィルタリング

たとえば、ニュース記事のリストから特定のカテゴリーの記事だけを表示するコンポーネントを考えてみましょう。

function NewsList({ articles, category }) {
  const filteredArticles = useMemo(() => {
    return articles.filter((article) => article.category === category);
  }, [articles, category]);

  return (
    <ul>
      {filteredArticles.map((article) => (
        <li key={article.id}>{article.title}</li>
      ))}
    </ul>
  );
}

このコンポーネントは、articlescategoryを受け取り、特定のカテゴリーの記事だけを表示します。

いまは単純な例ですが、articlesが大量のデータを持っていたり、filterの条件が複雑だったりすると、計算コストが高くなりますよね。

そこで、useMemoを使ってfilteredArticlesをキャッシュすることで、不要な再計算を防ぐことができます。

注意点

過度の最適化に注意

useMemoは計算コストの高い処理の結果をキャッシュするための Hook ですが、すべての計算にuseMemoを使用すると、かえってパフォーマンスが低下する場合があります。

嬉しくなってuseMemoを使いたくなるかもしれませんが、本当に必要な場合にのみ使用しましょう。

依存配列を適切に設定する

useMemoの第二引数である依存配列は、値を再計算するタイミングを制御します。

useMemo(() => {
  // 計算ロジック
}, [依存する値]); // この値が変更されたときのみ再計算

この依存配列を適切に設定しないと、古い計算結果を使い続ける可能性があるので、注意しましょう。

たとえば、以下の例では、useMemoの依存配列を空にしてしまうと、filteredArticlesが常に同じ値を返すため、カテゴリーが変わっても表示が更新されません。

const filteredArticles = useMemo(() => {
  return articles.filter((article) => article.category === category);
}, []); // 依存配列が空なので、常に同じ値を返す

5-2. useCallback: コールバック関数を最適化する

useCallback は、コールバック関数を最適化するための Hook です。

「コールバック関数」とは、引数として渡される関数のことです。

例えば、子コンポーネントにコールバック関数を渡す場合、その関数が毎回新しく作成されると、不要な再レンダリングが発生します。

そこで、useCallbackを使うことで、毎回関数を再作成するのを防ぎ、不要な再レンダリングを防ぐことができます。

駅まで歩くとき、毎回新しい道を選ぶのは面倒ですよね。
useCallbackは、いつもの道を覚えておくようなものです。
(ちょっと無理やりかもしれませんが)

使用例

以下のようなコンポーネントを考えてみましょう。

function ParentComponent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // 依存配列が空なので、常に同じ関数参照を返す

  return (
    <>
      <ChildComponent onClick={handleClick} />
      <div>Count: {count}</div>
    </>
  );
}

このコンポーネントは、子コンポーネントにクリックハンドラ handleClick を渡しています。

useCallbackを使うことで、レンダリングのたびに新しい関数を作成することを避け、子コンポーネントの不要な再レンダリングを防ぐことができます。

以下のサンプルコードを見ていただくとイメージしやすいかもしれません。

サンプルコード(useCallbackを使わない場合)

あえてuseCallbackを使わない場合を見てみましょう。

function ParentComponent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <>
      <ChildComponent onClick={handleClick} />
      <div>Count: {count}</div>
    </>
  );
}

これでも一応動きますが、handleClickを使うたびに新しい関数を作成するため、不要な再レンダリングが発生する可能性があります。

useCallback の便利な使いどころ

useCallback は主に以下のような場面でよく使われます。

  1. 子コンポーネントにコールバック関数を渡す場合
  2. useEffect の依存配列に関数を含める場合

後者がしっくりこないかもしれないので、具体的な例を用いて説明しましょう。

サンプルコード

では、useEffect と組み合わせる例を見てみましょう。

まずは、問題のあるコードを見ていきます。

問題のあるコード:
function Timer() {
  const [count, setCount] = useState(0);

  // この関数は毎回のレンダリングで新しく作成される
  const increment = () => {
    setCount((prevCount) => prevCount + 1);
  };

  useEffect(() => {
    const timerId = setInterval(increment, 1000);
    return () => clearInterval(timerId);
  }, [increment]); // incrementが毎回変わるので、Effectが毎回再実行される

  return <div>Count: {count}</div>;
}

この例では、increment関数が毎回のレンダリングで新しく作成されるため、useEffect の依存配列に含まれているincrementも毎回変更されます。

結果として、Effect が毎回クリーンアップされて再実行され、タイマーが正しく機能しない可能性があります。

increment関数が毎回新しく作成されると、Effect は毎回新しい関数を受け取るため、前回のタイマーがクリアされずに新しいタイマーが追加されてしまいます。

解決策:useCallback を使用したコード

では、useCallbackを使ってこの問題を解決してみましょう。

function Timer() {
  const [count, setCount] = useState(0);

  // useCallbackを使用して関数を最適化
  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // 依存配列が空なので、この関数は再レンダリング時に再作成されない

  useEffect(() => {
    const timerId = setInterval(increment, 1000);
    return () => clearInterval(timerId);
  }, [increment]); // incrementは変更されないので、Effectは最初の1回だけ実行される

  return <div>Count: {count}</div>;
}

今度は useCallbackを使用してincrement関数を最適化しています。

これでincrement関数は再レンダリング時に再作成されなくなり、useEffect も最初の 1 回だけ実行されるようになります。

このように、パフォーマンス目的だけでなく、関数の再作成によりバグが発生する可能性がある場合にもuseCallbackを使用することができます。

注意点

過度の最適化に注意

useMemo と同様に、すべての関数に useCallback を使用すると、かえってパフォーマンスが低下する可能性があります。

必要な場合にのみ使用しましょう。

依存配列を適切に設定する

useCallback の第二引数である依存配列は、関数を再作成するタイミングを制御します。

依存配列に含めるべき値を忘れると、古い状態や古い props を参照し続ける可能性があります。

悪い例:

const handleClick = useCallback(() => {
  // 何か処理
}, []); // 依存配列が空なので、常に同じ関数参照を返す

6. Custom Hooks:自分だけの Hook を作る

react_hooks_custom.png

最後に、Custom Hooks について見ていきましょう。

Custom Hooks は、自分で作る特別な Hook です。
既存の Hooks や JavaScript の技を組み合わせて、再利用可能なロジックを作ることができます。

例えば、料理のレシピを想像してみてください。
基本的な調理法を組み合わせて、自分だけの特別なレシピを作るようなものです。

useXXX: 自分だけの Hook を作る

Custom Hook は、useで始まる関数として定義します。

これにより、React のルールに従った再利用可能なロジックを作ることができます。

使用例

React Hooks を使いこなせるようになってくると、Hooks を組み合わせた複雑なロジックを作ることが増えてきます。

そんなとき、同じロジックを複数のコンポーネントで使い回したいときがあります。

以下のような Custom Hook を考えてみましょう。

function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return windowSize;
}

// 使用例
function ResponsiveComponent() {
  const { width, height } = useWindowSize();

  return (
    <div>
      Window size: {width} x {height}
    </div>
  );
}

この Custom Hook は、ウィンドウサイズを監視し、現在のサイズを返します。

これにより、ウィンドウサイズの監視ロジックを複数のコンポーネントで再利用できます。

便利な使いどころ

Custom Hooks は以下のような場合に特に便利です。

  1. 複数のコンポーネントで共通して使用するロジックがある場合
  2. 複雑なステート管理や副作用のロジックをカプセル化したい場合
  3. テストしやすいロジックを作りたい場合
サンプルコード
例: API からデータを取得する Custom Hook

この例では、API からデータを取得するための Custom Hook を作成しています。

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch(url);
        const json = await response.json();
        setData(json);
        setLoading(false);
      } catch (error) {
        setError(error);
        setLoading(false);
      }
    }

    fetchData();
  }, [url]);

  return { data, loading, error };
}

// 使用例
function DataComponent() {
  const { data, loading, error } = useFetch('https://api.example.com/data');

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{data.title}</div>;
}

この Custom Hook を使用することで、データ取得のロジックを簡単に再利用できます。

注意点

Hooks のルールを守る

Custom Hook を作成する際も、通常の Hooks と同じルールを守る必要があります。

  • Hook はトップレベルでのみ呼び出す
  • Hook は関数コンポーネントまたは他の Custom Hook の中でのみ呼び出す

これにより、React が Hook のルールを適用できるようになります。

Hook の呼び出し方については別な記事でも触れているので、参考にしてみてください。

再利用性を意識する

Custom Hook を作成する際は、できるだけ汎用的で再利用可能なものにすることを心がけましょう。

最初は特定のコンポーネントに依存したロジックを作成することもありますが、できるだけ汎用的な Hook にすることで、他のコンポーネントでも使い回すことができます。

例:

  • 特定の API エンドポイントに依存しない
  • 特定のコンポーネントに依存しない
  • パラメータを受け取ることで柔軟に使える

まとめ

ここまで、React Hooks の主要な機能について見てきました。

State Hooks、Context Hooks、Ref Hooks、Effect Hooks、Performance Hooks、そして Custom Hooks と、それぞれの Hook には特徴があり、使用場面もあることがわかりました。

※ちなみに他にもまだ Hooks はありますが、一旦今回紹介したものを優先的に覚えれば OK です。

Hooks の基本的な使い方を理解した上で、実際のプロジェクトで少しずつ活用していくことをおすすめします!

長いのに、最後まで読んでいただいてありがとうございました。

ちょっと宣伝

Qiita では主に「React をちょっと書ける人向け」記事を書いていますが、個人ブログでは「React をこれから学びたい人向け」の記事も書いてます。
もし興味があれば覗いていただけると感謝感激です。

123
115
2

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
123
115

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?