404 Not Found

404 Not Found


nginx

カスタムフック

1. 学習内容



2. フォーム再利用要件の経緯

カスタムフック用の再利用パターン

100%
flowchart TD
    A[Repetitive Logic] --> B[Extract as useXxx]
    B --> C[ComponentsA useXxx]
    B --> D[ComponentsB useXxx]
    B --> E[ComponentsC useXxx]
    
    F[useXxxInside] --> G[useState]
    F --> H[useEffect]
    F --> I[useRef]
    F --> J[useContext]
    
    K[The Rule of Three] --> L{Number of logical repetitions?}
    L -->|1 time| M[No extraction required]
    L -->|2 times| N[Consider extracting]
    L -->|3+ times| O[Must be extracted ✅]
    
    style B fill:#e8f5e9,stroke:#2e7d32
    style O fill:#c8e6c9,stroke:#2e7d32

(1) 課題:5ページにわたって同じロジックが繰り返されている

ボブは、5つの異なるページで「ローカルストレージからユーザー設定を取得する」必要があります:

JSX
// Page A:Settings Page
function SettingsPage() {
  const [prefs, setPrefs] = useState(() => {
    const saved = localStorage.getItem('preferences')
    return saved ? JSON.parse(saved) : { theme: 'light', lang: 'zh' }
  })

  useEffect(() => {
    localStorage.setItem('preferences', JSON.stringify(prefs))
  }, [prefs])

  // ... Page A specifically UI
}

// Page B:Dashboard
function Dashboard() {
  const [prefs, setPrefs] = useState(() => {
    const saved = localStorage.getItem('preferences')
    return saved ? JSON.parse(saved) : { theme: 'light', lang: 'zh' }
  })

  useEffect(() => {
    localStorage.setItem('preferences', JSON.stringify(prefs))
  }, [prefs])

  // ... Page B specifically UI(Exactly the same logic!)
}

問題点localStorage 読み書きのロジックが5つのページにわたり5回もコピー&ペーストされています。「ストレージイベントを監視する」という機能を追加したい場合、5つのファイルを修正しなければなりません。

(2) カスタムフックの解決策

JSX
// ---- Custom Hook:Packaging localStorage Reading and Writing ----
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const saved = localStorage.getItem(key)
      return saved ? JSON.parse(saved) : initialValue
    } catch {
      return initialValue
    }
  })

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value))
    } catch (e) {
      console.error('Save to localStorage Failure:', e)
    }
  }, [key, value])

  return [value, setValue]
}

// ---- Use this consistently on all pages ----
function SettingsPage() {
  const [prefs, setPrefs] = useLocalStorage('preferences', {
    theme: 'light', lang: 'zh'
  })
  // ... Specifically UI
}

function Dashboard() {
  const [prefs, setPrefs] = useLocalStorage('preferences', {
    theme: 'light', lang: 'zh'
  })
  // ... Specifically UI
}

メリット:20行のロジック → 1行のコード。ロジックを変更する際は、useLocalStorageという1つのファイルのみを変更すれば済みます。



3. カスタムフックのルール

規則 説明 違反時の措置
use から始める React の依存関係名によるフックの特定 リンターはこのルールをチェックしません。条件付き呼び出しではエラーは発生しません
最上位レベルでのみ呼び出す ループ、条件分岐、またはネストされたブロック内では呼び出さない フックチェーンの不整合、状態の混乱
関数コンポーネントまたはカスタムフック内でのみ呼び出してください 通常の関数やクラスコンポーネント内では使用しないでください ランタイムエラー:無効なフック呼び出し
JSXではなくデータや関数を返す フックはロジックを返し、コンポーネントはUIを返す 関心の分離に反し、再利用が困難になる
副作用のクリーンアップ useEffect でクリーンアップ関数を返す メモリリーク、イベントリスナーの蓄積

(1) 命名規則

JSX
// ✅ It is essential to use Introduction
function useOnlineStatus() { }
function useWindowSize() { }
function useDebounce() { }

// ❌ Not based on use Introduction → React Will not check Hooks Rules
function onlineStatus() { }
function fetchData() { }

(2) 戻り値の設計

JSX
// Method 1:Return an array(like useState)
function useToggle(initial = false) {
  const [value, setValue] = useState(initial)
  const toggle = useCallback(() => setValue(v => !v), [])
  return [value, toggle]  // Array,Flexible Deconstruction of Names
}

// Method 2:Return Object(Greater clarity when there are multiple return values)
function useUser() {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  return { user, loading, error }  // Object,Property Names Are Clear
}

// Method 3:Returns a single value
function useDocumentTitle(title) {
  useEffect(() => { document.title = title }, [title])
  // No return value required
}

(3) 内部では任意のフックを使用できる

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

  useEffect(() => {
    let cancelled = false
    setLoading(true)
    
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error('Request Failed')
        return res.json()
      })
      .then(data => { if (!cancelled) { setData(data); setLoading(false) }})
      .catch(err => { if (!cancelled) { setError(err.message); setLoading(false) }})

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

  return { data, loading, error }
}
// Used internally useState + useEffect + useCallback(If necessary)
// Custom Hook You can combine any of the built-in features Hooks


4. 6つの実用的なカスタムフック

フック 関数 内部で使用されるフック 戻り値
useToggle ブールスイッチ(トグル) useState, useCallback { value, toggle, setTrue, setFalse }
useDebounce 手ぶれ補正入力 useState, useEffect debouncedValue
useWindowSize ウィンドウサイズの監視 useState, useEffect { width, height }
useFetch データ要求(トライステート) useState, useEffect, useCallback { data, loading, error, refetch }
useIntersectionObserver 可視性検出 useState, useEffect isIntersecting
useMediaQuery レスポンシブ・ブレークポイント useState, useEffect matches

(1) フック 1: useToggle(トグルスイッチ)

JSX
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue)
  const toggle = useCallback(() => setValue(v => !v), [])
  const setTrue = useCallback(() => setValue(true), [])
  const setFalse = useCallback(() => setValue(false), [])

  return { value, toggle, setTrue, setFalse }
}

// Usage
function ModalExample() {
  const { value: isOpen, setTrue: open, setFalse: close } = useToggle(false)
  return (
    <div>
      <button onClick={open}>Open the pop-up window</button>
      {isOpen && (
        <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <div style={{ backgroundColor: 'white', padding: '24px', borderRadius: '8px' }}>
            <h2>Pop-up Title</h2>
            <p>Pop-up Content</p>
            <button onClick={close}>Close</button>
          </div>
        </div>
      )}
    </div>
  )
}

(2) フック 2: useDebounce(ジッター防止)

JSX
function useDebounce(value, delay = 500) {
  const [debouncedValue, setDebouncedValue] = useState(value)

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)

    return () => clearTimeout(timer)
  }, [value, delay])

  return debouncedValue
}

// Usage:Search Input Stabilization
function SearchBox() {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebounce(query, 500)

  // Only at debouncedQuery Request only when there is a change(The user has stopped typing 500ms after )
  useEffect(() => {
    if (debouncedQuery) {
      fetch('/api/search?q=' + debouncedQuery).then(/* ... */)
    }
  }, [debouncedQuery])

  return <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." />
}

(3) フック 3: useWindowSize (ウィンドウサイズ)

JSX
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  })

  useEffect(() => {
    let timeoutId

    function handleResize() {
      // Image Stabilization:50ms several times inside resize Execute only once
      clearTimeout(timeoutId)
      timeoutId = setTimeout(() => {
        setSize({ width: window.innerWidth, height: window.innerHeight })
      }, 50)
    }

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

  return size
}

// Usage
function ResponsiveComponent() {
  const { width } = useWindowSize()
  return (
    <p>
      Current window width:{width}px
      {width < 768 ? '(Mobile View)' : width < 1024 ? '(Flat View)' : '(Desktop View)'}
    </p>
  )
}

(4) フック 4: useFetch(データリクエスト)

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

  useEffect(() => {
    let cancelled = false
    setLoading(true)
    setError(null)

    fetch(url, options)
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
        return res.json()
      })
      .then(data => { if (!cancelled) { setData(data); setLoading(false) }})
      .catch(err => { if (!cancelled) { setError(err.message); setLoading(false) }})

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

  // Resubmit Request
  const refetch = useCallback(() => {
    setLoading(true)
    fetch(url, options)
      .then(res => res.json())
      .then(data => { setData(data); setLoading(false) })
      .catch(err => { setError(err.message); setLoading(false) })
  }, [url])

  return { data, loading, error, refetch }
}

// Usage
function UserProfile({ userId }) {
  const { data: user, loading, error, refetch } = useFetch(`/api/users/${userId}`)

  if (loading) return <p>Loading......</p>
  if (error) return <p>Error:{error} <button onClick={refetch}>Retry</button></p>
  return <h1>{user.name}</h1>
}

(5) フック 5: useIntersectionObserver(表示状態の検出)

JSX
function useIntersectionObserver(ref, options = {}) {
  const [isIntersecting, setIsIntersecting] = useState(false)

  useEffect(() => {
    if (!ref.current) return

    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting)
    }, { threshold: options.threshold || 0.1, ...options })

    observer.observe(ref.current)

    return () => observer.disconnect()
  }, [ref, options.threshold])

  return isIntersecting
}

// Usage:Lazy Loading of Images
function LazyImage({ src, alt }) {
  const imgRef = useRef(null)
  const isVisible = useIntersectionObserver(imgRef)
  const [loaded, setLoaded] = useState(false)

  return (
    <div ref={imgRef} style={{ minHeight: '200px', backgroundColor: '#f5f5f5' }}>
      {isVisible && (
        <img
          src={src}
          alt={alt}
          onLoad={() => setLoaded(true)}
          style={{ width: '100%', opacity: loaded ? 1 : 0, transition: 'opacity 0.3s' }}
        />
      )}
    </div>
  )
}

(6) フック 6: useMediaQuery(レスポンシブ・ブレークポイント)

JSX
function useMediaQuery(query) {
  const [matches, setMatches] = useState(() => window.matchMedia(query).matches)

  useEffect(() => {
    const mql = window.matchMedia(query)
    function handler(e) { setMatches(e.matches) }
    mql.addEventListener('change', handler)
    return () => mql.removeEventListener('change', handler)
  }, [query])

  return matches
}

// Usage
function ResponsiveLayout() {
  const isMobile = useMediaQuery('(max-width: 768px)')
  const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)')
  const isDesktop = useMediaQuery('(min-width: 1025px)')

  return (
    <div>
      {isMobile && <MobileMenu />}
      {isDesktop && <FullSidebar />}
      <p>
        {isMobile ? '📱 Cell Phone' : isTablet ? '📟 Tablet' : '💻 Computer'}
      </p>
    </div>
  )
}


5. カスタムフックの再利用に関する原則

原則 説明 悪い例 良い例
単一責任の原則 フックは1つのことだけを行う useUserAndTheme() useUser() + useTheme()
柔軟なパラメータ 妥当なデフォルト値を設定する useFetch('/api/data') ハードコーディングされたURL useFetch(url) パラメータ化されたURL
返される値 「読み込み中」、「エラー」、「データ」の3つの状態を返す useFetch() データのみを返す useFetch() { data, loading, error } を返す
リソースのクリーンアップ コンポーネントがアンマウントされた際の副作用をクリーンアップする タイマーやイベントリスナーはクリーンアップしない useEffect でクリーンアップを返す
組み合わせ可能 小さなフックを組み合わせて、より大きなフックにできる useComplexData() 100行 useFetch()useDebounce() の組み合わせ

(1) ▶ サンプル:複数の小さなフックを組み合わせる

JSX
// ============================================
// Example:Combine multiple small Hook Building Complex Search Features
// ============================================

// 1. Small hook:Input Stabilization
function useDebounce(value, delay) { /* ... */ }

// 2. Small hook:Data Request
function useFetch(url) { /* ... */ }

// 3. Small hook:Local Storage
function useLocalStorage(key, initial) { /* ... */ }

// 4. Combined into a large Hook:Search
function useSearch(defaultQuery = '') {
  const [query, setQuery] = useState(defaultQuery)
  const [history, setHistory] = useLocalStorage('search_history', [])
  const debouncedQuery = useDebounce(query, 500)
  const { data, loading, error } = useFetch(
    debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
  )

  function search(newQuery) {
    setQuery(newQuery)
    // Save Search History
    if (newQuery.trim()) {
      setHistory(prev => [newQuery, ...prev.filter(h => h !== newQuery)].slice(0, 10))
    }
  }

  return { query, setQuery: search, results: data, loading, error, history }
}
// useSearch Combined internally useState + useDebounce + useFetch + useLocalStorage
▶ 試してみよう

6. 完全な例:画像ギャラリー(複数のカスタムフックの組み合わせ)

JSX
// ============================================
// Complete Example:Photo Gallery(Combination 3 Custom Hook)
// ============================================

import { useState, useEffect, useCallback, useRef } from 'react'

// ---- Custom Hook 1:Infinite Scrolling ----
function useInfiniteScroll(callback) {
  const sentinelRef = useRef(null)

  useEffect(() => {
    const sentinel = sentinelRef.current
    if (!sentinel) return

    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) callback()
    }, { threshold: 0.1 })

    observer.observe(sentinel)
    return () => observer.disconnect()
  }, [callback])

  return sentinelRef
}

// ---- Custom Hook 2:Keyboard Navigation ----
function useKeyPress(targetKey, callback) {
  useEffect(() => {
    function handler(e) {
      if (e.key === targetKey) callback(e)
    }
    window.addEventListener('keydown', handler)
    return () => window.removeEventListener('keydown', handler)
  }, [targetKey, callback])
}

// ---- Custom Hook 3:Click here ----
function useClickOutside(callback) {
  const ref = useRef(null)

  useEffect(() => {
    function handler(e) {
      if (ref.current && !ref.current.contains(e.target)) {
        callback()
      }
    }
    document.addEventListener('mousedown', handler)
    return () => document.removeEventListener('mousedown', handler)
  }, [callback])

  return ref
}

// ---- Image Gallery Component ----
function ImageGallery() {
  const [images, setImages] = useState([])
  const [page, setPage] = useState(1)
  const [selected, setSelected] = useState(null)
  const [loading, setLoading] = useState(false)

  // Loading image
  const loadMore = useCallback(() => {
    if (loading) return
    setLoading(true)
    const newImages = Array.from({ length: 8 }, (_, i) => ({
      id: images.length + i + 1,
      url: `https://picsum.photos/seed/${images.length + i + 1}/300/200`,
      title: `Image ${images.length + i + 1}`
    }))
    setTimeout(() => {
      setImages(prev => [...prev, ...newImages])
      setPage(p => p + 1)
      setLoading(false)
    }, 800)
  }, [loading, images.length])

  // First Load
  useEffect(() => { loadMore() }, [])

  // Infinite Scroll: Load More
  const sentinelRef = useInfiniteScroll(loadMore)

  // Click here to close the preview
  const previewRef = useClickOutside(() => setSelected(null))

  // Escape Key to close the preview
  useKeyPress('Escape', () => setSelected(null))
  // Use the left and right arrow keys to switch between images
  useKeyPress('ArrowLeft', () => {
    if (selected) {
      const idx = images.findIndex(i => i.id === selected.id)
      if (idx > 0) setSelected(images[idx - 1])
    }
  })
  useKeyPress('ArrowRight', () => {
    if (selected) {
      const idx = images.findIndex(i => i.id === selected.id)
      if (idx < images.length - 1) setSelected(images[idx + 1])
    }
  })

  return (
    <div style={{ maxWidth: '900px', margin: '0 auto' }}>
      <h2>🖼️ Photo Gallery</h2>
      <p style={{ color: '#666', fontSize: '13px' }}>Loaded {images.length} images | Keyboard:← Previous → Next Escape Close</p>

      {/* Image Grid */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '12px' }}>
        {images.map(img => (
          <div key={img.id} onClick={() => setSelected(img)}
            style={{ cursor: 'pointer', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
            <img src={img.url} alt={img.title}
              style={{ width: '100%', height: '150px', objectFit: 'cover', transition: 'transform 0.3s' }}
              onMouseEnter={e => e.currentTarget.style.transform = 'scale(1.05)'}
              onMouseLeave={e => e.currentTarget.style.transform = 'scale(1)'} />
            <p style={{ padding: '8px', margin: 0, fontSize: '13px', textAlign: 'center' }}>{img.title}</p>
          </div>
        ))}
      </div>

      {/* Load More Triggers */}
      <div ref={sentinelRef} style={{ textAlign: 'center', padding: '20px' }}>
        {loading ? <p>⏳ Loading......</p> : <p style={{ color: '#999' }}>↓ Scroll to load more</p>}
      </div>

      {/* Image Preview(Click here to close) */}
      {selected && (
        <div style={{
          position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
          backgroundColor: 'rgba(0,0,0,0.8)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          zIndex: 1000
        }}>
          <div ref={previewRef} style={{ maxWidth: '80vw', maxHeight: '80vh' }}>
            <img src={selected.url.replace('300/200', '600/400')} alt={selected.title}
              style={{ maxWidth: '100%', maxHeight: '80vh', borderRadius: '8px' }} />
            <p style={{ color: 'white', textAlign: 'center', marginTop: '12px' }}>
              {selected.title}(Click here to close)
            </p>
          </div>
        </div>
      )}
    </div>
  )
}

期待される出力:初回読み込み時に8枚の画像が表示される画像ギャラリー。画面の一番下までスクロールすると自動的にさらに画像が読み込まれ、画像をクリックするとプレビューが開きます(キーボード操作に対応)。プレビュー領域の外側をクリックすると、プレビューが閉じます。


(1) ▶ サンプル:useClipboard フック — クリップボードへのコピー

JSX
function useClipboard() {
  const [copied, setCopied] = useState(false)

  async function copy(text) {
    try {
      await navigator.clipboard.writeText(text)
      setCopied(true)
      setTimeout(() => setCopied(false), 2000)
    } catch {
      setCopied(false)
    }
  }

  return { copied, copy }
}

function CopyButton({ text }) {
  const { copied, copy } = useClipboard()
  return (
    <button onClick={() => copy(text)}
      style={{ padding: '4px 12px', cursor: 'pointer', background: copied ? '#52c41a' : '#f0f0f0', color: copied ? 'white' : '#333', border: 'none', borderRadius: 4 }}>
      {copied ? 'Copied!' : 'Copy'}
    </button>
  )
}
▶ 試してみよう

(2) ▶ サンプル:useOnlineStatus フック — ネットワーク状態の検出

JSX
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine)

  useEffect(() => {
    function handleOnline() { setIsOnline(true) }
    function handleOffline() { setIsOnline(false) }
    window.addEventListener('online', handleOnline)
    window.addEventListener('offline', handleOffline)
    return () => {
      window.removeEventListener('online', handleOnline)
      window.removeEventListener('offline', handleOffline)
    }
  }, [])

  return isOnline
}

function StatusBar() {
  const isOnline = useOnlineStatus()
  return (
    <div style={{ padding: '8px 16px', background: isOnline ? '#f6ffed' : '#fff2f0', borderBottom: `1px solid ${isOnline ? '#b7eb8f' : '#ffccc7'}` }}>
      {isOnline ? '🟢 Online' : '🔴 Offline — changes will sync when reconnected'}
    </div>
  )
}
▶ 試してみよう

(3) ▶ サンプル:useLocalStorage フック — 状態の永続化

JSX
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const saved = localStorage.getItem(key)
      return saved !== null ? JSON.parse(saved) : initialValue
    } catch { return initialValue }
  })

  useEffect(() => {
    try { localStorage.setItem(key, JSON.stringify(value)) }
    catch (e) { console.error('localStorage save failed:', e) }
  }, [key, value])

  return [value, setValue]
}

function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light')
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}
      style={{
        padding: '8px 16px', cursor: 'pointer', borderRadius: 4, border: 'none',
        background: theme === 'dark' ? '#333' : '#f0f0f0',
        color: theme === 'dark' ? 'white' : '#333',
      }}>
      {theme === 'dark' ? '🌙 Dark' : '☀️ Light'} (saved)
    </button>
  )
}
▶ 試してみよう

(4) ▶ サンプル:useCountdown フック — カウントダウン

JSX
function useCountdown(seconds, autostart = false) {
  const [timeLeft, setTimeLeft] = useState(seconds)
  const [isRunning, setIsRunning] = useState(autostart)

  useEffect(() => {
    if (!isRunning || timeLeft <= 0) return
    const timer = setInterval(() => {
      setTimeLeft(prev => {
        if (prev <= 1) { setIsRunning(false); return 0 }
        return prev - 1
      })
    }, 1000)
    return () => clearInterval(timer)
  }, [isRunning, timeLeft])

  function start() { setTimeLeft(seconds); setIsRunning(true) }
  function pause() { setIsRunning(false) }
  function resume() { setIsRunning(true) }

  const minutes = Math.floor(timeLeft / 60)
  const secs = timeLeft % 60
  const formatted = `${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`

  return { timeLeft, formatted, isRunning, start, pause, resume }
}

function CountdownTimer() {
  const { formatted, isRunning, start, pause, resume } = useCountdown(300)
  return (
    <div style={{ textAlign: 'center', padding: 20 }}>
      <p style={{ fontSize: 48, fontFamily: 'monospace', margin: '0 0 16px' }}>{formatted}</p>
      <div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
        {!isRunning ? <button onClick={start} style={{ padding: '8px 16px', cursor: 'pointer' }}>Start</button>
          : <button onClick={pause} style={{ padding: '8px 16px', cursor: 'pointer' }}>Pause</button>}
        {isRunning || <button onClick={resume} style={{ padding: '8px 16px', cursor: 'pointer' }}>Resume</button>}
      </div>
    </div>
  )
}
▶ 試してみよう

❓ よくある質問

Q カスタムフックと通常の関数の違いは何ですか?
A カスタムフック内では React Hooks(useStateuseEffect など)を使用できますが、通常の関数内では使用できません。したがって、「フックを必要とする再利用可能なロジック」にはカスタムフックを使用し、「純粋な計算ロジック」には通常の関数で十分です。例えば、formatDate()は通常の関数を使用していますが、useWindowSize()はカスタムフックを使用しています。
Q カスタムフックのテストはどのように行いますか?
A @testing-library/react-hooks ライブラリを使用します。カスタムフックのテストは、コンポーネントのテストとまったく同じです。フックをレンダリングし、戻り値を確認し、更新をトリガーして、変更内容を確認します。純粋に論理的なフックの場合、テストは非常に簡単です。
Q いつロジックをカスタムフックに抽出すればよいですか?
A 2つ以上のコンポーネントで同じロジックが見られる場合です。(そのロジックが実際に再利用されることが確実になる前など)早すぎる段階で抽出するのは避けましょう。かといって、(5回もコピー&ペーストしてしまうまで)あまり長く待つのもよくありません。役立つ目安として、「3回ルール」があります。同じロジックが3回以上現れる場合は、必ずカスタムフックに抽出する必要があります。
Q カスタムフックで useEffect を使用できますか?
A はい、可能です。これは非常に一般的な使い方です。カスタムフックでは、あらゆる組み込みフック(useStateuseEffectuseRefuseContext など)を使用できます。例えば、useFetchフックは、データ、読み込み、およびエラー状態の管理に内部でuseStateを使用し、マウント時のデータ取得にuseEffectを、AbortControllerの保存にuseRefを使用しています。これこそがカスタムフックの価値であり、複数のフックを1つの再利用可能なロジック単位に統合できる点にあります。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐):設定可能な初期値を持つ、useCounter を引数として受け取り、{ count, increment, decrement, reset } を返すフックを作成してください。
  2. 上級問題(難易度 ⭐⭐)navigator.clipboard API をラップし、{ copied, copy(text) } を返す useClipboard フックを作成してください。また、テキストをクリップボードにコピーする機能もサポートしてください。
  3. 課題(難易度:⭐⭐⭐):WebSocket接続をラップし、{ data, isConnected, send(message), reconnect() }を返すuseWebSocketフックを作成してください。自動再接続とハートビート検出に対応している必要があります。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%