107
106

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をこれから学びたい人向け」の記事も書いてます。
もし興味があれば覗いていただけると感謝感激です。

107
106
1

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
107
106

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?