Agent skill

react-hooks-patterns

Master React Hooks including useState, useEffect, useContext, useReducer, and custom hooks with production-grade patterns

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/product/react-hooks-patterns

SKILL.md

React Hooks Patterns Skill

Overview

Master React Hooks patterns including built-in hooks (useState, useEffect, useContext, useReducer, useCallback, useMemo) and custom hook development for reusable logic.

Learning Objectives

  • Understand all built-in React Hooks
  • Create custom hooks for code reuse
  • Implement advanced hook patterns
  • Optimize performance with hooks
  • Handle side effects effectively

Core Hooks

useState

jsx
const [state, setState] = useState(initialValue);

// Lazy initialization for expensive computations
const [state, setState] = useState(() => expensiveComputation());

// Functional updates when state depends on previous value
setState(prev => prev + 1);

useEffect

jsx
useEffect(() => {
  // Side effect code
  const subscription = api.subscribe();

  // Cleanup function
  return () => {
    subscription.unsubscribe();
  };
}, [dependencies]); // Dependency array

useContext

jsx
const value = useContext(MyContext);

// Best practice: Create custom hook
function useMyContext() {
  const context = useContext(MyContext);
  if (!context) {
    throw new Error('useMyContext must be within Provider');
  }
  return context;
}

useReducer

jsx
const [state, dispatch] = useReducer(reducer, initialState);

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

useCallback

jsx
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b], // Dependencies
);

useMemo

jsx
const memoizedValue = useMemo(
  () => computeExpensiveValue(a, b),
  [a, b]
);

useRef

jsx
const ref = useRef(initialValue);

// DOM access
const inputRef = useRef(null);
<input ref={inputRef} />
inputRef.current.focus();

// Mutable value that doesn't trigger re-render
const countRef = useRef(0);

Custom Hooks Library

useFetch

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

  useEffect(() => {
    let cancelled = false;

    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setData(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });

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

  return { data, loading, error };
}

useLocalStorage

jsx
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

useDebounce

jsx
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

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

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

usePrevious

jsx
function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

useToggle

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

  const toggle = useCallback(() => {
    setValue(v => !v);
  }, []);

  return [value, toggle];
}

useOnClickOutside

jsx
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };

    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);

    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

Practice Exercises

  1. Counter with useReducer: Build a counter using useReducer instead of useState
  2. Theme Context: Create a theme switcher using useContext
  3. Data Fetching: Implement useFetch with caching
  4. Form Handler: Create useForm custom hook for form state management
  5. Window Size Hook: Build useWindowSize hook for responsive layouts
  6. Intersection Observer: Create useInView hook for lazy loading
  7. Timer Hook: Build useInterval and useTimeout hooks

Common Patterns

Fetching with Abort

jsx
function useFetchWithAbort(url) {
  const [state, setState] = useState({ data: null, loading: true, error: null });

  useEffect(() => {
    const abortController = new AbortController();

    fetch(url, { signal: abortController.signal })
      .then(res => res.json())
      .then(data => setState({ data, loading: false, error: null }))
      .catch(err => {
        if (err.name !== 'AbortError') {
          setState({ data: null, loading: false, error: err });
        }
      });

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

  return state;
}

Combining Multiple Hooks

jsx
function useAuthenticatedApi(url) {
  const { token } = useAuth();
  const { data, loading, error } = useFetch(url, {
    headers: {
      Authorization: `Bearer ${token}`
    }
  });

  return { data, loading, error };
}

Best Practices

  1. Name custom hooks with "use" prefix
  2. Extract reusable logic into custom hooks
  3. Keep hooks at component top level (no conditionals)
  4. Use ESLint plugin for hooks rules
  5. Document custom hook dependencies
  6. Test custom hooks with @testing-library/react-hooks

Resources

Assessment

Can you:

  • Explain when to use each built-in hook?
  • Create custom hooks for reusable logic?
  • Handle cleanup in useEffect properly?
  • Optimize with useCallback and useMemo?
  • Manage complex state with useReducer?
  • Build a custom hook library for your project?

Unit Test Template

jsx
// __tests__/useCustomHook.test.js
import { renderHook, act, waitFor } from '@testing-library/react';
import { useCustomHook } from '../useCustomHook';

describe('useCustomHook', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should initialize with default state', () => {
    const { result } = renderHook(() => useCustomHook());
    expect(result.current.state).toBe(initialValue);
  });

  it('should handle async operations', async () => {
    const { result } = renderHook(() => useCustomHook());

    await act(async () => {
      await result.current.asyncAction();
    });

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });
  });

  it('should cleanup on unmount', () => {
    const cleanup = jest.fn();
    const { unmount } = renderHook(() => useCustomHook({ onCleanup: cleanup }));

    unmount();
    expect(cleanup).toHaveBeenCalled();
  });
});

Retry Logic Pattern

jsx
function useRetryAsync(asyncFn, options = {}) {
  const { maxRetries = 3, backoff = 1000 } = options;
  const [state, setState] = useState({ data: null, error: null, loading: false });

  const execute = useCallback(async (...args) => {
    setState(s => ({ ...s, loading: true, error: null }));
    let lastError;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        const data = await asyncFn(...args);
        setState({ data, error: null, loading: false });
        return data;
      } catch (error) {
        lastError = error;
        if (attempt < maxRetries) {
          await new Promise(r => setTimeout(r, backoff * Math.pow(2, attempt)));
        }
      }
    }

    setState({ data: null, error: lastError, loading: false });
    throw lastError;
  }, [asyncFn, maxRetries, backoff]);

  return { ...state, execute };
}

Version: 2.0.0 Last Updated: 2025-12-30 SASMP Version: 2.0.0 Difficulty: Intermediate Estimated Time: 2-3 weeks Prerequisites: React Fundamentals Changelog: Added retry patterns, test templates, and observability hooks

Didn't find tool you were looking for?

Be as detailed as possible for better results