React Hooks Patterns

Reusable custom hook patterns for UI state, data fetching, forms, persistence, and interaction handling.

View
StandardDetailedCompact
Export
Copy the compact sheet, download it, or print it.
Download
`D` dense toggle · `C` copy all
## Custom Hook Patterns
Basic custom hook
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle] as const;
}

# Extract reusable stateful logic into a function.

Custom hook with options object
function useInterval({ callback, delay, enabled = true }: {
  callback: () => void;
  delay: number;
  enabled?: boolean;
}) {
  useEffect(() => {
    if (!enabled) return;
    const id = setInterval(callback, delay);
    return () => clearInterval(id);
  }, [callback, delay, enabled]);
}

# Prefer options objects for extensible APIs.

Return object from a custom hook
function useDisclosure(initial = false) {
  const [isOpen, setIsOpen] = useState(initial);
  return {
    isOpen,
    open: () => setIsOpen(true),
    close: () => setIsOpen(false),
    toggle: () => setIsOpen(v => !v),
  };
}

# Use named returns for richer APIs.

## Common UI Hooks
Track previous value
function usePrevious<T>(value: T) {
  const ref = useRef<T | undefined>(undefined);
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

# Store the previous render’s value.

Persist state to localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const raw = localStorage.getItem(key);
    return raw ? JSON.parse(raw) as T : initialValue;
  });
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  return [value, setValue] as const;
}

# Sync component state with localStorage.

Respond to media queries
function useMediaQuery(query: string) {
  const [matches, setMatches] = useState(false);
  useEffect(() => {
    const media = window.matchMedia(query);
    setMatches(media.matches);
    const onChange = () => setMatches(media.matches);
    media.addEventListener('change', onChange);
    return () => media.removeEventListener('change', onChange);
  }, [query]);
  return matches;
}

# Expose a boolean that tracks a CSS media query.

Handle clicks outside an element
function useOnClickOutside<T extends HTMLElement>(ref: React.RefObject<T>, onOutside: () => void) {
  useEffect(() => {
    function handle(event: MouseEvent) {
      if (!ref.current || ref.current.contains(event.target as Node)) return;
      onOutside();
    }
    document.addEventListener('mousedown', handle);
    return () => document.removeEventListener('mousedown', handle);
  }, [ref, onOutside]);
}

# Close menus or popovers when a click happens outside a ref.

## Data Fetching Patterns
Fetch with AbortController
useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setResults)
    .catch(err => {
      if (err.name !== 'AbortError') throw err;
    });
  return () => controller.abort();
}, [query]);

# Cancel stale requests on dependency change.

Track loading, error, and data state
const [data, setData] = useState<User[] | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);

# Model async UI state explicitly.

Expose derived async status
return {
  data,
  error,
  loading,
  isEmpty: !loading && !error && Array.isArray(data) && data.length === 0,
};

# Return a status object from custom data hooks.

## Forms and Event Patterns
Controlled form state
const [form, setForm] = useState({ email: '', password: '' });
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const { name, value } = e.target;
  setForm(prev => ({ ...prev, [name]: value }));
};

# Manage a form object with useState.

Complex form with useReducer
function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'change':
      return { ...state, [action.name]: action.value };
    case 'reset':
      return action.initial;
    default:
      return state;
  }
}

# Use a reducer when form behavior becomes more complex.

Debounced input value
function useDebouncedValue<T>(value: T, delay: number) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

# Delay reactions to rapidly changing values.

Recommended next

No recommendations yet.