Custom Hooks

This section documents React 19.x for the web, using function components and Hooks throughout (class components appear only where React still requires them, such as error boundaries). React Native is out of scope and gets only a pointer. The React 19 additions follow the current official React documentation. This content was generated with the assistance of AI and should be verified against react.dev before being relied on in production, since React APIs continue to evolve between releases.

This section’s bibliography lists the reference material consulted while preparing these pages.

A custom Hook is a function whose name starts with use and that calls other Hooks. It packages stateful logic so several components can reuse it. Each component that calls the Hook gets its own independent state — a custom Hook shares logic, not state.

Extracting a custom Hook

Move the useState / useEffect / other Hook calls into a useSomething function; return whatever the component needs (a value, a tuple, an object).

import { useState, useCallback } from 'react';

function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn((v) => !v), []);
  return [on, toggle];
}

// usage
function Panel() {
  const [open, toggleOpen] = useToggle();
  return <button onClick={toggleOpen}>{open ? 'Hide' : 'Show'}</button>;
}

Name it for the concept (useFormInput, useChatRoom), not the mechanism (useEffectAndState). Only extract when it removes real duplication or clarifies intent. See Reusing Logic with Custom Hooks.

Worked examples

// useLocalStorage — state mirrored to localStorage
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const raw = localStorage.getItem(key);
    return raw != null ? JSON.parse(raw) : initialValue;
  });
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  return [value, setValue];
}

// useDebounce — a value that updates only after it stops changing
function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const t = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(t);
  }, [value, delay]);
  return debounced;
}

// useFetch — minimal data fetch with a race guard
function useFetch(url) {
  const [state, setState] = useState({ data: null, error: null, loading: true });
  useEffect(() => {
    let ignore = false;
    setState((s) => ({ ...s, loading: true }));
    fetch(url)
      .then((r) => r.json())
      .then((data) => !ignore && setState({ data, error: null, loading: false }))
      .catch((error) => !ignore && setState({ data: null, error, loading: false }));
    return () => { ignore = true; };
  }, [url]);
  return state;
}

// useOnlineStatus — subscribe to an external browser store
import { useSyncExternalStore } from 'react';

function useOnlineStatus() {
  return useSyncExternalStore(
    (callback) => {
      window.addEventListener('online', callback);
      window.addEventListener('offline', callback);
      return () => {
        window.removeEventListener('online', callback);
        window.removeEventListener('offline', callback);
      };
    },
    () => navigator.onLine,      // client snapshot
    () => true                   // server snapshot
  );
}

Custom Hooks compose: useFetch could build on useDebounce, a useChatRoom could call useEffectEvent. Pass reactive values (props, state) in as arguments and return reactive values out; keep the Hook body pure just like a component. See useSyncExternalStore.