Custom Hooks

Difficulty: Intermediate

Custom hooks are JavaScript functions whose names start with 'use' and that call other hooks internally. They are the primary mechanism for extracting and reusing stateful logic across multiple components. Unlike render props or higher-order components, custom hooks don't add extra nodes to the component tree - they compose naturally and share logic without changing component structure.

The naming convention is essential: a custom hook must start with 'use' (e.g., useAuth, useFetch, useLocalStorage). This convention serves two purposes. First, it tells React's linter that the function follows hook rules, enabling the ESLint plugin to verify correct hook usage inside it. Second, it communicates to developers that the function contains stateful logic and follows hook semantics - it can't be called conditionally and must be used at the top level of a component.

Each call to a custom hook gets its own isolated state. If two components call useCounter(), they each get independent count values. Custom hooks share logic (the code), not state (the data). This is fundamentally different from global state solutions. The hook is essentially a template for creating independent state instances.

The return value of a custom hook is entirely up to you. Common patterns include returning a single value, an array (like useState returns [value, setter]), or an object with named properties. Objects are preferred when returning many values because consumers can destructure only what they need. Arrays are preferred when there are exactly two values (following the useState convention) because consumers can name them freely.

Custom hooks can call any combination of built-in hooks and other custom hooks. This composability is one of their greatest strengths. A useAuth hook might use useState, useEffect, and useContext internally. A usePaginatedFetch hook might compose useReducer with a useFetch hook. There is no limit to how deeply hooks can compose, as long as the rules of hooks are respected at every level.

Code examples

Basic custom hook: useCounter

function useCounter(initialValue) {
  const [count, setCount] = React.useState(initialValue);

  const increment = React.useCallback(() => setCount(c => c + 1), []);
  const decrement = React.useCallback(() => setCount(c => c - 1), []);
  const reset = React.useCallback(() => setCount(initialValue), [initialValue]);

  return { count, increment, decrement, reset };
}

// Two components using the same hook get INDEPENDENT state
function ComponentA() {
  const { count, increment } = useCounter(0);
  increment();
  console.log('A count:', count);
  return null;
}

function ComponentB() {
  const { count, increment } = useCounter(10);
  increment();
  console.log('B count:', count);
  return null;
}

ComponentA();
ComponentB();

Each component gets its own independent counter state. The custom hook encapsulates the logic (state + operations) while each call creates a separate state instance.

Custom hook: useToggle

function useToggle(initialValue) {
  const [value, setValue] = React.useState(initialValue);

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

  const setTrue = React.useCallback(() => setValue(true), []);
  const setFalse = React.useCallback(() => setValue(false), []);

  return { value, toggle, setTrue, setFalse };
}

function Modal() {
  const { value: isOpen, toggle, setTrue, setFalse } = useToggle(false);

  console.log('Modal is open:', isOpen);
  console.log('toggle is a function:', typeof toggle === 'function');
  console.log('setTrue is a function:', typeof setTrue === 'function');
  console.log('setFalse is a function:', typeof setFalse === 'function');
  return null;
}

Modal();

useToggle encapsulates boolean state management. The returned object provides multiple ways to update the value. Renaming during destructuring (value: isOpen) keeps the consumer's code clear.

Custom hook composing other hooks

function usePrevious(value) {
  const ref = React.useRef(undefined);
  React.useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

function useCounterWithPrevious(initial) {
  const [count, setCount] = React.useState(initial);
  const previousCount = usePrevious(count);

  return { count, previousCount, setCount };
}

function Display() {
  const { count, previousCount } = useCounterWithPrevious(5);
  console.log('Current:', count);
  console.log('Previous:', previousCount);
  console.log('Custom hooks can compose other custom hooks: true');
  return null;
}

Display();

useCounterWithPrevious composes useState with the usePrevious custom hook. This demonstrates the composability of hooks - building complex behavior from simpler pieces.

Key points

Concepts covered

custom hooks, hook extraction, code reuse, naming conventions, stateful logic sharing