[React講座] カスタムフックとロジックの再利用

当ページのリンクには広告が含まれています。
目次

カスタムフックとロジックの再利用

カスタムフックとは

一言でいうと、「自分で作る useState / useEffect 的な関数」です。

条件はこれだけ:

  • 関数名が use から始まる
  • 中で useStateuseEffect など 他のフックを使う

例:

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue)
  const toggle = () => setValue((v) => !v)
  return [value, toggle]
}

これも立派なフックです。
使う側は、React標準フックと同じノリで使えます。

const [isOpen, toggleOpen] = useToggle(false)

カスタムフックの目的はざっくり2つ:

  1. ロジックをコンポーネントから切り離して見通しを良くする
  2. 同じパターンのロジックを複数コンポーネントで再利用する

シンプルな例:useToggle

まずは感覚をつかむために、さっきの useToggle をちゃんと書いてみます。

// useToggle.js
import { useState } from 'react'

export function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue)

  const toggle = () => {
    setValue((prev) => !prev)
  }

  return [value, toggle]
}

使い方:

// App.jsx
import { useToggle } from './useToggle'

export default function App() {
  const [isOpen, toggleOpen] = useToggle(false)
  const [isOn, toggleOn] = useToggle(true)

  return (
    <div>
      <button onClick={toggleOpen}>
        詳細を {isOpen ? '閉じる' : '開く'}
      </button>
      {isOpen && <p>ここに詳細テキスト</p>}

      <hr />

      <button onClick={toggleOn}>
        電源: {isOn ? 'ON' : 'OFF'}
      </button>
    </div>
  )
}

ここでやっていること:

  • useStatesetState((prev)=>!prev) というよくあるパターンを1個にまとめた
  • その結果、App 側は 「useToggle を2回呼ぶ」だけで同じロジックが使える

「重複したロジック」を見つけて切り出す

カスタムフックの作り方の実務的な流れは:

  1. まずは普通にコンポーネントを書く
  2. 「あれ、このロジック、別コンポーネントでもほぼ同じもの書いてるな…」 と気づく
  3. その部分をカスタムフックに抽出する

という順番です。

Before:2つのカウンターコンポーネント

// CounterA.jsx
import { useState } from 'react'

export function CounterA() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>Counter A: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
      <button onClick={() => setCount(0)}>リセット</button>
    </div>
  )
}

// CounterB.jsx
import { useState } from 'react'

export function CounterB() {
  const [count, setCount] = useState(10)

  return (
    <div>
      <p>Counter B: {count}</p>
      <button onClick={() => setCount((c) => c - 1)}>-1</button>
      <button onClick={() => setCount(10)}>リセット</button>
    </div>
  )
}

両方とも、「count を持って、ボタンで増減・リセット」しているだけで、ロジックがほぼ同じです。

After:useCounter に抽出

// useCounter.js
import { useState } from 'react'

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

  const increment = () => setCount((c) => c + 1)
  const decrement = () => setCount((c) => c - 1)
  const reset = () => setCount(initialValue)

  return { count, increment, decrement, reset }
}

使う側:

// CounterA.jsx
import { useCounter } from './useCounter'

export function CounterA() {
  const { count, increment, reset } = useCounter(0)

  return (
    <div>
      <p>Counter A: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={reset}>リセット</button>
    </div>
  )
}

// CounterB.jsx
import { useCounter } from './useCounter'

export function CounterB() {
  const { count, decrement, reset } = useCounter(10)

  return (
    <div>
      <p>Counter B: {count}</p>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>リセット</button>
    </div>
  )
}

useCounter に「カウンターの挙動」を閉じ込めたので、コンポーネント側は「表示」と「どの操作をUIに紐づけるか」だけ書けばよくなります。

こういう「ロジック = フック / UI = コンポーネント」の分離が、カスタムフックの一番良いところです。

データ取得用フック:useFetch

前回、解説した「API通信+loading+error」は、いろんな画面で頻出するパターンです。
これもカスタムフック化するとかなりスッキリします。

useFetch(url) の例

// useFetch.js
import { useState, useEffect } from 'react'

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

  useEffect(() => {
    if (!url) return

    let isCancelled = false

    async function fetchData() {
      try {
        setLoading(true)
        setError(null)

        const res = await fetch(url)
        if (!res.ok) throw new Error('ネットワークエラー')
        const json = await res.json()

        if (!isCancelled) {
          setData(json)
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err.message || 'エラーが発生しました')
        }
      } finally {
        if (!isCancelled) {
          setLoading(false)
        }
      }
    }

    fetchData()

    return () => {
      isCancelled = true
    }
  }, [url])

  return { data, loading, error }
}

使う側:

// PostList.jsx
import { useFetch } from './useFetch'

export default function PostList() {
  const { data: posts, loading, error } = useFetch(
    'https://jsonplaceholder.typicode.com/posts?_limit=5',
  )

  if (loading) return <p>読み込み中...</p>
  if (error) return <p style={{ color: 'red' }}>エラー: {error}</p>
  if (!posts) return null

  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  )
}

同じフックを別コンポーネントでも使える:

// UserList.jsx
import { useFetch } from './useFetch'

export default function UserList() {
  const { data: users, loading, error } = useFetch(
    'https://jsonplaceholder.typicode.com/users',
  )

  // 以下同様
}

こうしておくと、API通信・エラーハンドリングまわりのロジックは useFetch に閉じ込め、各コンポーネントは「データの表示」に集中できるようになります。

※ 実務では更に高度なことが必要になるので、React Query や SWR などのライブラリを使うことが多いですが、
カスタムフックでこのくらい書けるようになっておくと、それらのライブラリも理解しやすいです。

フォーム用フック:useForm の例

共通処理:

  • 値をstateで持つ
  • onChangee.target.name / e.target.value を拾う
  • 送信時に preventDefault する
  • リセットする

これもある程度まとめられます。

シンプルな useForm

// useForm.js
import { useState } from 'react'

export function useForm(initialValues) {
  const [values, setValues] = useState(initialValues)

  function handleChange(e) {
    const { name, type, value, checked } = e.target
    setValues((prev) => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }))
  }

  function reset() {
    setValues(initialValues)
  }

  return { values, handleChange, reset, setValues }
}

使う側:

// ContactForm.jsx
import { useForm } from './useForm'

export default function ContactForm() {
  const { values, handleChange, reset } = useForm({
    name: '',
    email: '',
    message: '',
    subscribe: false,
  })

  function handleSubmit(e) {
    e.preventDefault()
    console.log('送信:', values)
    reset()
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>
          名前:
          <input
            name="name"
            value={values.name}
            onChange={handleChange}
          />
        </label>
      </div>

      <div>
        <label>
          メール:
          <input
            name="email"
            value={values.email}
            onChange={handleChange}
          />
        </label>
      </div>

      <div>
        <label>
          メッセージ:
          <textarea
            name="message"
            value={values.message}
            onChange={handleChange}
          />
        </label>
      </div>

      <div>
        <label>
          <input
            name="subscribe"
            type="checkbox"
            checked={values.subscribe}
            onChange={handleChange}
          />
          メルマガ購読
        </label>
      </div>

      <button type="submit">送信</button>
      <button type="button" onClick={reset}>リセット</button>
    </form>
  )
}

こうしておけば、別のフォームでも同じ useForm を使い回せます。

フックは「組み合わせて」使える

カスタムフックの強みは、「フック同士をネストできる」ことです。

例:

function useExample() {
  const [count, setCount] = useState(0)
  const [isOpen, toggleOpen] = useToggle(false)
  const { data, loading } = useFetch('/api/items?count=' + count)

  // ここにさらに独自ロジック

  return { count, setCount, isOpen, toggleOpen, data, loading }
}
  • 自作フックの中で他の自作フックを呼んでOK
  • それぞれの責務を分けておけば、「ロジックのレゴブロック」みたいに組める

最終的には、以下の分業を目指す形がよいです。

  • コンポーネント:UI(JSX)だけに近くなる
  • カスタムフック:状態管理・API呼び出し・バリデーションなどのロジックを担当

カスタムフックにも「フックのルール」がそのまま適用される

標準フックと同じく、カスタムフックも以下のルールを守る必要があります:

  1. use から始まる名前にする
    • Reactが「これはフックだ」と判定するのに必要
    • ESLint の react-hooks ルールもこの前提
  2. トップレベルでしか呼ばない
    • 条件分岐の中で呼ばない
    • ループの中で呼ばない
    • 早期returnの前後で呼ぶ数を変えない
    // NG: if (flag) { const [x, setX] = useState(0) } // OK:const [x, setX] = useState(0) if (!flag) return null
  3. フックはコンポーネント or カスタムフックの中でだけ使う
    • 普通の関数の中では useState などを使えない
    • カスタムフック自体は「フックを呼ぶためのラッパ」なのでOK

実質的には、「フックは常に同じ順番・同じ個数で呼ばれるように書く」ということです。

まとめ

  • カスタムフックは「use から始まり、中で他のフックを使う関数」
  • 目的は
    • ロジックをコンポーネントから切り離して見通しを良くする
    • 複数コンポーネントでロジックを再利用する
  • 典型例:
    • useToggle(真偽値トグル)
    • useCounter(汎用カウンター)
    • useFetch(API通信+loading+error)
    • useForm(フォーム値とハンドラ)
  • フックのルール(トップレベルで呼ぶ、useから始める)はカスタムフックにもそのまま適用される
  • 最終的には
    • UI = コンポーネント
    • ロジック = カスタムフック
      という分離を意識すると、大きめのアプリでも整理しやすくなる

<<前へ(useReducerとロジックの整理)

>>次へ(Reactのスタイリング手法)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次