【React 19.2対応】カスタムフックの作成方法・使い所・実例をわかりやすく解説

2023年1月4日React

Reactのカスタムフックは、useStateuseEffectuseContextなどのReact Hooksを組み合わせて、コンポーネント間で再利用しやすくするための仕組みです。React 19.2時点でも、カスタムフックの基本的な考え方は変わっていません。

この記事では、Reactのカスタムフックとは何か、どのような場面で使うべきか、関数名のルール、そしてReact 19以降の書き方に合わせたモーダル管理の実例を紹介します。

目次から読む

Reactのカスタムフックとは

Reactのカスタムフックとは、React Hooksを内部で使う独自の関数のことです。たとえば、useStateで状態を管理したり、useContextでContextの値を取得したりする処理を、ひとつの関数としてまとめることができます。

Reactには、useStateuseEffectuseContextuseMemouseCallbackなど、さまざまな組み込みHookがあります。しかし、アプリケーションを作っていると、「複数のコンポーネントで同じような処理を書いている」と感じることがあります。

そのようなときに、共通処理をカスタムフックとして切り出すことで、コードの重複を減らし、コンポーネントを読みやすくできます。

Reactのカスタムフックの使い所

カスタムフックは、React Hooksを使ったロジックを複数のコンポーネントで使い回したいときに便利です。見た目のUIを再利用したい場合はコンポーネント化しますが、状態管理や副作用、Contextの取得などの「処理」を再利用したい場合はカスタムフックが向いています。

たとえば、以下のような処理はカスタムフックに切り出しやすいです。

  • ログインユーザー情報を取得する処理
  • モーダルの開閉状態を管理する処理
  • フォーム入力の状態を管理する処理
  • Contextの値を安全に取得する処理
  • 画面サイズやオンライン状態など、ブラウザの状態を扱う処理
  • API通信やローディング状態をまとめる処理

ただし、何でもカスタムフックにすればよいわけではありません。1つのコンポーネントでしか使わない単純な処理まで無理に切り出すと、かえってコードが追いにくくなることがあります。まずはコンポーネント内で書き、同じ処理が複数の場所で必要になったタイミングでカスタムフック化するのがおすすめです。

カスタムフックの関数名のルール

カスタムフックの関数名は、必ずuseから始めます。たとえば、useModaluseAuthuseFetchuseWindowSizeのような名前にします。

Reactでは、useから始まる関数をHookとして扱います。これにより、ReactやESLintがHooksのルールに沿っているかをチェックしやすくなります。

たとえば、以下のような名前はカスタムフックとして適切です。

function useModal() {
  // モーダル用の処理を書く
}

function useAuth() {
  // 認証用の処理を書く
}

function useWindowSize() {
  // 画面サイズ用の処理を書く
}

一方で、以下のような名前はカスタムフックとしては適切ではありません。

function modal() {
  // useから始まっていないため、カスタムフックとして不適切
}

function getAuth() {
  // React Hookを内部で使うなら、useAuthのようにする
}

Hooksを使うときの基本ルール

React Hooksには重要なルールがあります。useStateuseContextなどのHookは、通常のJavaScript関数のどこででも呼べるわけではありません。

基本的には、以下の場所でのみ呼び出します。

  • Reactの関数コンポーネントのトップレベル
  • カスタムフックのトップレベル

条件分岐、ループ、ネストした関数、イベントハンドラ、try/catch/finallyの中では、通常のHookを呼び出さないようにします。

たとえば、以下は良い例です。

import { useState } from 'react';

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

  return (
    <button
      type="button"
      onClick={() => setCount(count + 1)}
    >
      {count}
    </button>
  );
}

以下は避けるべき例です。

import { useState } from 'react';

function Counter({ enabled }) {
  if (enabled) {
    const [count, setCount] = useState(0);
  }

  return null;
}

このように条件分岐の中でHookを呼び出すと、レンダーごとにHookが呼ばれる順番が変わる可能性があります。ReactはHookの呼び出し順をもとに状態を管理しているため、Hookは常に同じ順番で呼び出される必要があります。

React 19以降のContext Providerの書き方

React 19以降では、ContextをProviderとして使うときに、<SomeContext value={...}>という書き方ができます。

以前は、以下のように.Providerを使う書き方が一般的でした。

<ModalContext.Provider value={contextValue}>
  {children}
</ModalContext.Provider>

React 19以降では、次のように書けます。

<ModalContext value={contextValue}>
  {children}
</ModalContext>

React 18以前のプロジェクトでは、従来どおり<ModalContext.Provider>を使います。この記事の実例では、React 19.2に合わせて新しいContext Providerの書き方を使います。

カスタムフックの実例:モーダルを開閉するuseModal

ここからは、実際にカスタムフックを使ってモーダルの開閉状態を管理する例を紹介します。

今回作成する構成は、以下のとおりです。

  • ModalProviderでモーダルの状態を管理する
  • useModalというカスタムフックでContextの値を取得する
  • Navbarからモーダルを開く
  • Modalからモーダルを閉じる
  • createPortalを使ってモーダルを別のDOM要素に描画する

1. モーダル用のContextとカスタムフックを作成する

まず、モーダルの状態を管理するContextを作成します。ここではmodal-context.jsxというファイルを作ります。

import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
} from 'react';

const ModalContext = createContext(null);

export function ModalProvider({ children }) {
  const [isModalOpen, setIsModalOpen] = useState(false);

  const openModal = useCallback(() => {
    setIsModalOpen(true);
  }, []);

  const closeModal = useCallback(() => {
    setIsModalOpen(false);
  }, []);

  const contextValue = useMemo(() => {
    return {
      isModalOpen,
      openModal,
      closeModal,
    };
  }, [isModalOpen, openModal, closeModal]);

  return (
    <ModalContext value={contextValue}>
      {children}
    </ModalContext>
  );
}

export function useModal() {
  const context = useContext(ModalContext);

  if (context === null) {
    throw new Error(
      'useModalはModalProviderの内側で使用してください。'
    );
  }

  return context;
}

このコードでは、ModalProviderがモーダルの開閉状態を管理しています。isModalOpenにはモーダルが開いているかどうかが入り、openModalでモーダルを開き、closeModalでモーダルを閉じます。

useModalがカスタムフックです。内部でuseContextを使い、ModalContextの値を取得しています。

また、useModalModalProviderの外側で使われた場合は、分かりやすいエラーを出すようにしています。これにより、Providerでラップし忘れたときに原因を見つけやすくなります。

2. アプリ全体をModalProviderでラップする

次に、アプリ全体でモーダルの状態を使えるように、ルートコンポーネントをModalProviderでラップします。

Viteを使っている場合は、main.jsxで以下のように書きます。

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

import App from './App';
import { ModalProvider } from './context/modal-context';
import './index.css';

createRoot(document.querySelector('#root')).render(
  <StrictMode>
    <ModalProvider>
      <App />
    </ModalProvider>
  </StrictMode>
);

ModalProviderAppを囲むことで、App配下のどのコンポーネントからでもuseModalを使えるようになります。

3. createPortal用のDOMを用意する

モーダルを通常のコンポーネントツリーとは別の場所に描画したい場合、React DOMのcreatePortalを使います。

そのために、index.htmlrootとは別のDOM要素を用意しておきます。

<div id="root"></div>
<div id="overlays"></div>

rootはReactアプリ本体を表示する場所です。overlaysは、モーダルやトースト通知など、画面の上に重ねて表示したいUIを描画する場所として使えます。

4. Navbarからモーダルを開く

次に、ナビゲーションのボタンをクリックしたときにモーダルを開くコンポーネントを作成します。

import { useModal } from '../context/modal-context';

export default function Navbar() {
  const { openModal } = useModal();

  return (
    <nav>
      <button
        type="button"
        onClick={openModal}
      >
        モーダルを開く
      </button>
    </nav>
  );
}

Navbarでは、useModalからopenModalを取り出しています。ボタンのonClickopenModalを指定することで、クリック時にモーダルを開けます。

このように、Contextを直接使う処理をuseModalにまとめておくと、各コンポーネント側のコードが短くなります。

5. Modalコンポーネントを作成する

次に、モーダル本体のコンポーネントを作成します。

import { createPortal } from 'react-dom';

import { useModal } from '../context/modal-context';

function ModalPortal({ children }) {
  if (typeof document === 'undefined') {
    return null;
  }

  const target = document.querySelector('#overlays');

  if (target === null) {
    return null;
  }

  return createPortal(children, target);
}

export default function Modal({ className = '', children }) {
  const { isModalOpen, closeModal } = useModal();

  if (!isModalOpen) {
    return null;
  }

  return (
    <ModalPortal>
      <section
        id="backdrop"
        onClick={closeModal}
        aria-hidden="true"
      />

      <section
        role="dialog"
        aria-modal="true"
        className={className}
      >
        <button
          type="button"
          onClick={closeModal}
          aria-label="モーダルを閉じる"
        >
          ×
        </button>

        {children}
      </section>
    </ModalPortal>
  );
}

このコードでは、useModalからisModalOpencloseModalを取得しています。isModalOpenfalseの場合は、モーダルを表示せずにnullを返します。

isModalOpentrueになると、ModalPortalを使って#overlaysの中にモーダルを描画します。

また、閉じるボタンだけでなく、背景部分であるbackdropをクリックしたときにもcloseModalが実行されるようにしています。

6. AppコンポーネントでNavbarとModalを使う

最後に、AppコンポーネントでNavbarModalを読み込みます。

import Navbar from './components/Navbar';
import Modal from './components/Modal';

export default function App() {
  return (
    <>
      <Navbar />

      <main>
        <h1>カスタムフックのサンプル</h1>
        <p>
          ナビゲーションのボタンを押すとモーダルが開きます。
        </p>
      </main>

      <Modal className="sample-modal">
        <h2>モーダルの内容</h2>
        <p>
          Contextとカスタムフックを使って表示しています。
        </p>
      </Modal>
    </>
  );
}

これで、Navbarのボタンをクリックするとモーダルが開き、モーダル内の閉じるボタンや背景をクリックするとモーダルが閉じるようになります。

7. 最低限のCSS例

モーダルを画面中央に表示するために、最低限のCSSを追加しておきます。

#backdrop {
  position: fixed;
  inset: 0;
  background: rgb(0 0 0 / 50%);
  z-index: 10;
}

.sample-modal {
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 20;
  width: min(90%, 480px);
  padding: 24px;
  background: #fff;
  border-radius: 12px;
  transform: translate(-50%, -50%);
}

.sample-modal button {
  cursor: pointer;
}

このCSSはあくまで簡単な例です。実際のプロジェクトでは、デザインに合わせて背景色、余白、アニメーション、閉じるボタンの位置などを調整してください。

この実例でカスタムフックを使うメリット

今回の例では、モーダルの状態管理をModalProviderに集約し、各コンポーネントではuseModalを呼び出すだけにしています。

そのため、ナビゲーション、ヘッダー、サイドバー、カード、ボタンなど、どのコンポーネントからでも同じ方法でモーダルを開閉できます。

もし各コンポーネントで個別にuseStateを書いてモーダルを管理すると、状態がバラバラになり、どのモーダルがいつ開くのか分かりにくくなります。Contextとカスタムフックを組み合わせることで、モーダルの状態を一か所で管理できます。

カスタムフックはstateそのものを共有するわけではない

カスタムフックを使うときに注意したい点があります。それは、カスタムフック自体が自動的にstateを共有するわけではないということです。

たとえば、複数のコンポーネントで同じカスタムフックを呼び出した場合、それぞれのコンポーネントが独立したstateを持つことがあります。

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

  return {
    count,
    setCount,
  };
}

このuseCounterを複数のコンポーネントで使うと、それぞれのコンポーネントが別々のcountを持ちます。

一方で、今回のuseModalはContextを読み取っているため、ModalProviderが持っている同じモーダル状態にアクセスできます。つまり、共有したい状態がある場合は、Contextや外部ストアなどと組み合わせる必要があります。

カスタムフックを作るときの注意点

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

React Hooksを内部で使う関数は、useModalのようにuseから始めます。名前を正しく付けることで、React Hooksのルールを守りやすくなります。

2. Hookはトップレベルで呼び出す

カスタムフックの中でも、useStateuseContextなどはトップレベルで呼び出します。条件分岐やループの中で呼び出さないようにしましょう。

3. 返り値は分かりやすくする

カスタムフックの返り値は、使う側が理解しやすい形にします。値が複数ある場合は、オブジェクトで返すと名前付きで取り出せるため、可読性が上がります。

const {
  isModalOpen,
  openModal,
  closeModal,
} = useModal();

4. Provider外で使われた場合のエラーを用意する

Contextを使うカスタムフックでは、Providerの外側で使われたときに分かりやすいエラーを出すのがおすすめです。

if (context === null) {
  throw new Error(
    'useModalはModalProviderの内側で使用してください。'
  );
}

このようにしておくと、開発中に原因をすぐに見つけられます。

5. 不要なカスタムフック化は避ける

カスタムフックは便利ですが、使いすぎるとコードの流れが見えにくくなることがあります。単純な処理や、1つのコンポーネントでしか使わない処理は、そのままコンポーネント内に書いても問題ありません。

useMemoとuseCallbackを使っている理由

今回の例では、openModalcloseModaluseCallbackを使い、contextValueuseMemoを使っています。

これは、Contextに渡す値の参照をできるだけ安定させるためです。

Contextのvalueに毎回新しいオブジェクトを直接渡すと、Providerが再レンダーされるたびにContextの値も新しくなります。その結果、Contextを読んでいるコンポーネントも再レンダーされやすくなります。

小さなアプリでは大きな問題にならないことも多いですが、アプリが大きくなると再レンダーの影響が気になる場合があります。そのため、Contextでオブジェクトや関数を渡すときは、必要に応じてuseMemouseCallbackを使うとよいでしょう。

カスタムフックと通常の関数の違い

カスタムフックと通常の関数の大きな違いは、React Hooksを内部で使えるかどうかです。

比較項目カスタムフック通常の関数
関数名useから始める自由に付けられる
React Hooksの使用使用できる基本的に使用しない
主な用途状態管理や副作用などのロジック共有計算、変換、整形などの通常処理
呼び出す場所コンポーネントまたは他のカスタムフック内必要な場所で呼び出せる

たとえば、日付の文字列を整形するだけなら通常の関数で十分です。一方で、useStateuseEffectを使って状態や副作用を扱うなら、カスタムフックとして作成します。

よくある質問

カスタムフックはReact 19.2でも使えますか?

はい。React 19.2でもカスタムフックは引き続き使えます。Reactの公式ドキュメントでも、コンポーネント間でロジックを共有する方法としてカスタムフックが紹介されています。

カスタムフックの名前は必ずuseから始める必要がありますか?

はい。React Hooksを内部で使う関数は、useModaluseAuthのようにuseから始めます。これにより、React Hooksのルールに沿った関数であることが分かりやすくなります。

カスタムフックを使えばstateは自動的に共有されますか?

いいえ。カスタムフックを複数のコンポーネントで呼び出しても、stateが自動的に共有されるわけではありません。stateを共有したい場合は、Contextや外部ストアなどと組み合わせます。

Contextを使うだけならカスタムフックは不要ですか?

必須ではありません。ただし、useContextを直接いろいろなコンポーネントで書くよりも、useModalのようなカスタムフックにまとめると、Provider外で使ったときのエラー処理も書けるため便利です。

React 18以前でもこの記事のコードは使えますか?

一部修正すれば使えます。React 18以前では、Context Providerの部分を<ModalContext value={contextValue}>ではなく、<ModalContext.Provider value={contextValue}>に変更してください。

useMemoやuseCallbackは必ず使う必要がありますか?

必須ではありません。小さなアプリでは使わなくても問題ない場合があります。ただし、Contextのvalueにオブジェクトや関数を渡す場合、再レンダーを抑えたいときに役立ちます。

カスタムフックはどのファイルに置くべきですか?

決まりはありませんが、hooksフォルダやcontextフォルダに分けることが多いです。今回のようにContextと強く関係するカスタムフックは、Contextファイル内にまとめても分かりやすいです。

参考リンク

カスタムフックやHooksのルールについて詳しく知りたい場合は、React公式ドキュメントも参考になります。

まとめ

Reactのカスタムフックは、コンポーネント間で状態管理や副作用などのロジックを再利用するための便利な仕組みです。

関数名は必ずuseから始め、内部ではReact Hooksのルールを守って実装します。特に、Hookはコンポーネントまたはカスタムフックのトップレベルで呼び出すことが大切です。

今回の例では、ModalProvideruseModalを使って、アプリ全体でモーダルの開閉状態を扱えるようにしました。React 19以降ではContext Providerを<ModalContext value={...}>のように書けるため、以前よりも少しシンプルに記述できます。

カスタムフックを上手に使うと、コンポーネントの見通しが良くなり、同じ処理の重複も減らせます。処理が複数のコンポーネントで必要になったときは、カスタムフックとして切り出せないかを考えてみましょう。

React

Posted by devsakaso