Claude Code for Advanced React Hooks: Custom Hooks, Patterns, and Composition — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Advanced React Hooks: Custom Hooks, Patterns, and Composition
Frontend

Claude Code for Advanced React Hooks: Custom Hooks, Patterns, and Composition

Published: November 5, 2026
Read time: 8 min read
By: Claude Skills 360

React hooks transformed how state and side effects are encapsulated — but the real power is in custom hooks that extract domain logic from components. A well-designed custom hook is reusable across components, testable in isolation, and composable with other hooks. Claude Code writes custom hooks that follow the rules correctly: stable callback references, cleanup in useEffect, predictable rendering behavior, and the TypeScript generics that make hooks reusable without losing type safety.

CLAUDE.md for React Hooks Projects

## React Hooks Standards
- Custom hooks: prefix with use, single responsibility, documented with JSDoc
- useEffect: every dependency must be in the array — no eslint-disable comments
- Stable references: useCallback for callbacks passed to children; useMemo for expensive computations only
- State shape: useReducer when state has >2 related fields or complex transitions
- Context: never put frequently-changing data in context — it re-renders all consumers
- Testing: test custom hooks with @testing-library/react renderHook
- Concurrent mode safe: no observing external mutable variables directly — use useSyncExternalStore

useReducer for Complex State

// hooks/useOrderForm.ts — complex form state with useReducer
import { useReducer, useCallback } from 'react';

type OrderItem = { productId: string; quantity: number; priceCents: number };

type OrderFormState = {
  items: OrderItem[];
  customerId: string;
  notes: string;
  isSubmitting: boolean;
  submitError: string | null;
};

type OrderFormAction =
  | { type: 'ADD_ITEM'; item: OrderItem }
  | { type: 'REMOVE_ITEM'; productId: string }
  | { type: 'UPDATE_QUANTITY'; productId: string; quantity: number }
  | { type: 'SET_CUSTOMER'; customerId: string }
  | { type: 'SET_NOTES'; notes: string }
  | { type: 'SUBMIT_START' }
  | { type: 'SUBMIT_SUCCESS' }
  | { type: 'SUBMIT_ERROR'; error: string };

function orderFormReducer(state: OrderFormState, action: OrderFormAction): OrderFormState {
  switch (action.type) {
    case 'ADD_ITEM':
      const existing = state.items.find(i => i.productId === action.item.productId);
      if (existing) {
        return {
          ...state,
          items: state.items.map(i =>
            i.productId === action.item.productId
              ? { ...i, quantity: i.quantity + action.item.quantity }
              : i
          ),
        };
      }
      return { ...state, items: [...state.items, action.item] };
    
    case 'REMOVE_ITEM':
      return { ...state, items: state.items.filter(i => i.productId !== action.productId) };
    
    case 'UPDATE_QUANTITY':
      if (action.quantity <= 0) {
        return { ...state, items: state.items.filter(i => i.productId !== action.productId) };
      }
      return {
        ...state,
        items: state.items.map(i =>
          i.productId === action.productId ? { ...i, quantity: action.quantity } : i
        ),
      };
    
    case 'SUBMIT_START':
      return { ...state, isSubmitting: true, submitError: null };
    
    case 'SUBMIT_SUCCESS':
      return { ...state, isSubmitting: false, items: [], customerId: '', notes: '' };
    
    case 'SUBMIT_ERROR':
      return { ...state, isSubmitting: false, submitError: action.error };
    
    default:
      return state;
  }
}

export function useOrderForm() {
  const [state, dispatch] = useReducer(orderFormReducer, {
    items: [],
    customerId: '',
    notes: '',
    isSubmitting: false,
    submitError: null,
  });
  
  // Stable callbacks — memo'd so they don't re-render children
  const addItem = useCallback((item: OrderItem) =>
    dispatch({ type: 'ADD_ITEM', item }), []);
  
  const removeItem = useCallback((productId: string) =>
    dispatch({ type: 'REMOVE_ITEM', productId }), []);
  
  const submit = useCallback(async () => {
    dispatch({ type: 'SUBMIT_START' });
    try {
      await api.createOrder({ items: state.items, customerId: state.customerId });
      dispatch({ type: 'SUBMIT_SUCCESS' });
    } catch (err) {
      dispatch({ type: 'SUBMIT_ERROR', error: err.message });
    }
  }, [state.items, state.customerId]);
  
  const totalCents = state.items.reduce((sum, i) => sum + i.quantity * i.priceCents, 0);
  
  return { ...state, totalCents, addItem, removeItem, submit };
}

Custom Async Hook

// hooks/useAsync.ts — generic async operation with loading/error/data states
import { useState, useEffect, useCallback, useRef } from 'react';

type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

export function useAsync<T>(
  asyncFn: () => Promise<T>,
  deps: React.DependencyList,
): AsyncState<T> & { refetch: () => void } {
  const [state, setState] = useState<AsyncState<T>>({ status: 'idle' });
  const mountedRef = useRef(true);
  
  useEffect(() => {
    mountedRef.current = true;
    return () => { mountedRef.current = false; };
  }, []);
  
  const execute = useCallback(async () => {
    setState({ status: 'loading' });
    try {
      const data = await asyncFn();
      if (mountedRef.current) {
        setState({ status: 'success', data });
      }
    } catch (err) {
      if (mountedRef.current) {
        setState({ status: 'error', error: err instanceof Error ? err : new Error(String(err)) });
      }
    }
  }, deps);
  
  useEffect(() => {
    execute();
  }, [execute]);
  
  return { ...state, refetch: execute };
}

// Usage:
// const { status, data, error, refetch } = useAsync(() => api.getOrders(userId), [userId]);

useSyncExternalStore for External State

// hooks/useThemeStore.ts — subscribe to external store safely (Concurrent Mode compatible)
import { useSyncExternalStore } from 'react';

// External store (could be Zustand, Redux, or any observable)
type ThemeStore = { theme: 'light' | 'dark'; accentColor: string };

class ThemeStoreImpl {
  private state: ThemeStore = { theme: 'light', accentColor: '#2563eb' };
  private listeners = new Set<() => void>();
  
  getSnapshot = (): ThemeStore => this.state;
  
  subscribe = (listener: () => void): (() => void) => {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  };
  
  setTheme(theme: ThemeStore['theme']) {
    this.state = { ...this.state, theme };
    this.listeners.forEach(l => l());
  }
}

export const themeStore = new ThemeStoreImpl();

// Hook: re-renders only when store changes
export function useTheme() {
  return useSyncExternalStore(
    themeStore.subscribe,
    themeStore.getSnapshot,
    // Server snapshot (for SSR):
    () => ({ theme: 'light' as const, accentColor: '#2563eb' }),
  );
}

Hook Composition Pattern

// hooks/useOrderManagement.ts — compose smaller hooks into domain hook
import { useCallback } from 'react';
import { useAsync } from './useAsync';
import { useLocalStorage } from './useLocalStorage';

// Small single-purpose hooks compose into domain hooks
function useLocalStorage<T>(key: string, initial: T) {
  const [value, setRaw] = useState<T>(() => {
    try {
      const stored = localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initial;
    } catch {
      return initial;
    }
  });
  
  const setValue = useCallback((newValue: T | ((prev: T) => T)) => {
    setRaw(prev => {
      const next = typeof newValue === 'function' ? (newValue as Function)(prev) : newValue;
      localStorage.setItem(key, JSON.stringify(next));
      return next;
    });
  }, [key]);
  
  return [value, setValue] as const;
}

export function useOrderManagement(userId: string) {
  const [draftOrderId, setDraftOrderId] = useLocalStorage<string | null>(
    `draft_order_${userId}`, null
  );
  
  const ordersState = useAsync(
    () => api.getOrders(userId),
    [userId]
  );
  
  const createOrder = useCallback(async (items: OrderItem[]) => {
    const order = await api.createOrder({ userId, items });
    setDraftOrderId(null);  // Clear draft after creation
    return order;
  }, [userId, setDraftOrderId]);
  
  return {
    orders: ordersState,
    draftOrderId,
    setDraftOrderId,
    createOrder,
  };
}

For the React state management libraries (Zustand, Jotai) that replace complex useReducer patterns in large apps, the React frontend guide covers library selection criteria. For the React Query data-fetching hooks that replace useAsync with caching and background refetch, the React Query guide covers the full TanStack Query API. The Claude Skills 360 bundle includes React hooks skill sets covering custom hook design, useReducer patterns, context composition, and concurrent mode compliance. Start with the free tier to try custom hook generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free