React Custom Hooks

Build reusable custom hooks to extract component logic and share stateful behavior.

TL;DR

  1. 01Extract stateful logic from components into reusable hook functions.
  2. 02Start every custom hook name with the word use.
  3. 03Call other hooks inside your hook to compose behavior.

Tips

  1. 01Return values from custom hooks in the same format as built-in hooks — use arrays for positional access or objects for named access.

Warnings

  1. 01Each component that uses a custom hook gets its own isolated state instance — they don't automatically share state between them.

Basic Structure

  • Write a custom hook as a regular function that calls React hooks.
    function useToggle(initialValue = false) {
      const [value, setValue] = useState(initialValue);
    
      const toggle = useCallback(() => {
        setValue(v => !v);
      }, []);
    
      return [value, toggle];
    }
    
  • Combine useState and useEffect to track a browser API value over time.
    function useDebouncedValue(value, delayMs = 300) {
      const [debounced, setDebounced] = useState(value);
    
      useEffect(() => {
        const timer = setTimeout(() => setDebounced(value), delayMs);
        return () => clearTimeout(timer); // cancel if value changes again
      }, [value, delayMs]);
    
      return debounced;
    }
    
  • Call your hook from a component just like a built-in hook.
    function SearchBox() {
      const [query, setQuery] = useState('');
      const debouncedQuery = useDebouncedValue(query, 400);
    
      useEffect(() => {
        if (debouncedQuery) searchApi(debouncedQuery);
      }, [debouncedQuery]);
    
      return <input value={query} onChange={e => setQuery(e.target.value)} />;
    }
    
  • Keep each hook focused on a single concern.

Naming Rules

  • Always start custom hook names with use, like useToggle.
    // Good: clearly indicates it's a hook
    function useToggle() { }
    function useFetch(url) { }
    function useLocalStorage(key) { }
    
    // Bad: doesn't follow naming convention
    function toggle() { }
    function fetch() { }
  • Use camelCase after the use prefix for consistency.
  • Naming triggers React ESLint rules and warnings.
  • Pick names that describe what the hook does.

Common Patterns

  • Build useToggle for managing boolean state.
    function useToggle(initial = false) {
      const [value, setValue] = useState(initial);
      const toggle = () => setValue(!value);
      return [value, toggle];
    }
    
    // Usage
    function Modal() {
      const [isOpen, toggleOpen] = useToggle(false);
      return (
        <>
          <button onClick={toggleOpen}>Open</button>
          {isOpen && <div>Modal content</div>}
        </>
      );
    }
  • Build usePrevious to track previous prop or state values.
    function usePrevious(value) {
      const ref = useRef();
      useEffect(() => {
        ref.current = value;
      }, [value]);
      return ref.current;
    }

Composing and Typing Hooks

  • Compose several built-in hooks inside one custom hook to build a higher-level abstraction.
    function useOnlineStatus() {
      const [isOnline, setIsOnline] = useState(navigator.onLine);
    
      useEffect(() => {
        const goOnline = () => setIsOnline(true);
        const goOffline = () => setIsOnline(false);
        window.addEventListener('online', goOnline);
        window.addEventListener('offline', goOffline);
        return () => {
          window.removeEventListener('online', goOnline);
          window.removeEventListener('offline', goOffline);
        };
      }, []);
    
      return isOnline;
    }
    
  • Build one custom hook on top of another instead of duplicating logic.
    function useSyncedField(key, initialValue) {
      const [value, setValue] = useLocalStorage(key, initialValue); // reuse an existing hook
      const isOnline = useOnlineStatus(); // compose a second hook
      return { value, setValue, isOnline };
    }
    
  • Type a generic custom hook so callers get the correct inferred return type.
    function useToggle<T = boolean>(initial: T): [T, () => void] {
      const [value, setValue] = useState(initial);
      const toggle = useCallback(() => setValue(v => !v as T), []);
      return [value, toggle];
    }
    
    const [isOpen, toggleOpen] = useToggle(false); // isOpen inferred as boolean
    
  • Test a custom hook in isolation with renderHook from @testing-library/react.
    import { renderHook, act } from '@testing-library/react';
    
    test('useToggle flips its value', () => {
      const { result } = renderHook(() => useToggle(false));
      act(() => result.current[1]()); // call the toggle function
      expect(result.current[0]).toBe(true);
    });
    
  • renderHook wraps your hook in a tiny test component so you never need a real UI to test it.
  • Keep composed hooks shallow — two or three layers deep is easier to trace than a long chain.

Sharing State Across Components

  • Each component gets its own state instance of a custom hook.
    function useCounter() {
      const [count, setCount] = useState(0);
      return [count, () => setCount(count + 1)];
    }
    
    function Component1() {
      const [count, increment] = useCounter();
      return <button onClick={increment}>{count}</button>;
    }
    
    function Component2() {
      const [count, increment] = useCounter();
      return <button onClick={increment}>{count}</button>;
    }
    // Each component has separate count state
  • Use Context API with custom hooks to share state.
    function useAuth() {
      const context = useContext(AuthContext);
      if (!context) throw new Error("useAuth needs provider");
      return context;
    }

FAQ