Claude-initial-setup hooks-mastery

install
source · Clone the upstream repo
git clone https://github.com/VersoXBT/claude-initial-setup
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/VersoXBT/claude-initial-setup "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/react-nextjs/hooks-mastery" ~/.claude/skills/versoxbt-claude-initial-setup-hooks-mastery && rm -rf "$T"
manifest: skills/react-nextjs/hooks-mastery/SKILL.md
source content

React Hooks Mastery

Patterns for writing correct, performant, and composable React hooks.

When to Use

  • User is creating custom hooks
  • User asks about useCallback, useMemo, or performance optimization
  • User has complex state logic that needs useReducer
  • User is integrating an external store (Redux, Zustand, vanilla)
  • User encounters stale closure bugs or rules of hooks violations

Core Patterns

Custom Hooks -- Extracting Reusable Logic

Custom hooks encapsulate stateful logic for reuse across components. Name them with the

use
prefix.

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

function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null)
  const [error, setError] = useState<Error | null>(null)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    const controller = new AbortController()
    setIsLoading(true)
    setError(null)

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`)
        return res.json()
      })
      .then((json) => setData(json as T))
      .catch((err) => {
        if (err.name !== 'AbortError') setError(err)
      })
      .finally(() => setIsLoading(false))

    return () => controller.abort()
  }, [url])

  return { data, error, isLoading }
}

useCallback and useMemo -- When to Memoize

Memoize only when passing callbacks to memoized children or when computation is expensive. Do not memoize everything by default.

import { useCallback, useMemo } from 'react'

function ProductList({ products, onSelect }: Props) {
  // Memoize because this is passed to React.memo children
  const handleSelect = useCallback(
    (id: string) => {
      onSelect(id)
    },
    [onSelect]
  )

  // Memoize because sorting is O(n log n)
  const sorted = useMemo(
    () => [...products].sort((a, b) => a.price - b.price),
    [products]
  )

  return (
    <ul>
      {sorted.map((p) => (
        <ProductItem key={p.id} product={p} onSelect={handleSelect} />
      ))}
    </ul>
  )
}

const ProductItem = React.memo(({ product, onSelect }: ItemProps) => (
  <li onClick={() => onSelect(product.id)}>{product.name}</li>
))

useRef -- Mutable Values Without Re-renders

Use refs for values that should persist across renders without triggering re-renders: timers, previous values, DOM elements.

function useInterval(callback: () => void, delay: number | null) {
  const savedCallback = useRef(callback)

  // Update ref on every render so the interval always calls the latest callback
  useEffect(() => {
    savedCallback.current = callback
  }, [callback])

  useEffect(() => {
    if (delay === null) return
    const id = setInterval(() => savedCallback.current(), delay)
    return () => clearInterval(id)
  }, [delay])
}

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined)
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}

useReducer -- Complex State Transitions

Prefer useReducer when state transitions depend on previous state or when multiple sub-values change together.

import { useReducer } from 'react'

interface FormState {
  values: Record<string, string>
  errors: Record<string, string>
  isSubmitting: boolean
}

type FormAction =
  | { type: 'SET_FIELD'; field: string; value: string }
  | { type: 'SET_ERROR'; field: string; error: string }
  | { type: 'SUBMIT_START' }
  | { type: 'SUBMIT_SUCCESS' }
  | { type: 'SUBMIT_FAILURE'; errors: Record<string, string> }

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        values: { ...state.values, [action.field]: action.value },
        errors: { ...state.errors, [action.field]: '' },
      }
    case 'SET_ERROR':
      return { ...state, errors: { ...state.errors, [action.field]: action.error } }
    case 'SUBMIT_START':
      return { ...state, isSubmitting: true }
    case 'SUBMIT_SUCCESS':
      return { ...state, isSubmitting: false, errors: {} }
    case 'SUBMIT_FAILURE':
      return { ...state, isSubmitting: false, errors: action.errors }
  }
}

function useForm(initialValues: Record<string, string>) {
  const [state, dispatch] = useReducer(formReducer, {
    values: initialValues,
    errors: {},
    isSubmitting: false,
  })

  const setField = (field: string, value: string) =>
    dispatch({ type: 'SET_FIELD', field, value })

  const submit = async (handler: (values: Record<string, string>) => Promise<void>) => {
    dispatch({ type: 'SUBMIT_START' })
    try {
      await handler(state.values)
      dispatch({ type: 'SUBMIT_SUCCESS' })
    } catch (err) {
      dispatch({ type: 'SUBMIT_FAILURE', errors: { form: String(err) } })
    }
  }

  return { ...state, setField, submit }
}

useSyncExternalStore -- Subscribing to External State

The correct way to subscribe React to any external mutable store (vanilla stores, browser APIs, third-party state).

import { useSyncExternalStore } from 'react'

function createStore<T>(initialState: T) {
  let state = initialState
  const listeners = new Set<() => void>()

  return {
    getState: () => state,
    setState: (next: T | ((prev: T) => T)) => {
      state = typeof next === 'function' ? (next as (prev: T) => T)(state) : next
      listeners.forEach((listener) => listener())
    },
    subscribe: (listener: () => void) => {
      listeners.add(listener)
      return () => listeners.delete(listener)
    },
  }
}

const counterStore = createStore({ count: 0 })

function useCounter() {
  const state = useSyncExternalStore(
    counterStore.subscribe,
    counterStore.getState,
    counterStore.getState // server snapshot
  )
  return {
    count: state.count,
    increment: () => counterStore.setState((s) => ({ count: s.count + 1 })),
  }
}

Anti-Patterns

  • Calling hooks conditionally -- Hooks must be called in the same order every render. Never put hooks inside if/else, loops, or after early returns.
  • Missing dependencies in useEffect -- Causes stale closures and subtle bugs. Always include all referenced values in the dependency array. Use the exhaustive-deps ESLint rule.
  • Memoizing everything -- useCallback and useMemo have overhead. Only memoize when passing to React.memo children or for genuinely expensive computations.
  • Using useRef to avoid dependency arrays -- This hides bugs. Fix the dependency array instead of working around it with refs (except for the interval/timer pattern).
  • Giant useEffect -- An effect that does multiple unrelated things. Split into separate useEffect calls, each with its own dependencies.

Quick Reference

HookPurposeRe-renders?
useStateSimple stateYes
useReducerComplex state transitionsYes
useRefMutable value, DOM refsNo
useMemoCache expensive computationNo (returns cached)
useCallbackCache function referenceNo (returns cached)
useEffectSide effects after renderNo (runs after)
useSyncExternalStoreSubscribe to external storeYes (on store change)

Rules of Hooks: (1) Only call at the top level. (2) Only call from React functions or custom hooks.