Back to all notes

Custom Hooks: Extract and Reuse Stateful Logic in React

Stop duplicating logic across components. Learn how to design focused, reusable custom hooks with real-world examples.

4 min read

Introduction

A custom hook is a function that encapsulates reusable, stateful logic using React hooks. It lets you separate behavior from presentation so components stay small and focused.

Why this matters

As apps grow, you often copy the same state and effects across components—counters, timers, fetch logic, form state. Copy‑paste works… until it doesn’t. Bugs multiply, fixes drift, and testing becomes inconsistent.

Custom hooks let you extract that stateful logic into a function that any component can use. They’re just JavaScript functions that use other hooks under the hood. You keep UI and behavior separate, and your components stay small and focused.

The problem

A common anti‑pattern is keeping all stateful logic inside each component. That means:

  • You can’t reuse the logic without duplicating it.
  • Testing the logic independently is harder.
  • The component is doing two jobs: state management and rendering.

Inefficient approach

Logic stays trapped in the component, so every component that needs a counter reimplements the same state and handlers:

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1); // duplicated everywhere

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

function AnotherCounter() {
  // Same logic duplicated - hard to maintain
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);

  return (
    <div>
      <p>Another Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Without Custom Hooks</h1>
      <Counter />
      <AnotherCounter />
    </div>
  );
}

export default App;

The solution

Extract the counter logic into a dedicated hook and keep the component purely presentational. This gives a clean API you can reuse anywhere.

import { useState } from "react";

function useCounter() {
  const [count, setCount] = useState(0);

  function increment() {
    setCount(count + 1);
  }

  return { count, increment };
}

function Counter() {
  const { count, increment } = useCounter();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

function AnotherCounter() {
  // Reuse the same hook - no duplication
  const { count, increment } = useCounter();

  return (
    <div>
      <p>Another Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>With Custom Hooks</h1>
      <Counter />
      <AnotherCounter />
    </div>
  );
}

export default App;

Step-by-step

  1. Create useCounter and add const [count, setCount] = useState(0).
  2. Define increment that updates state.
  3. Return a small API: { count, increment }.
  4. In Counter, call the hook and render the values.
  5. Verify that state is local to the hook and UI stays presentational.

Why this is better

  • The component renders UI only; the hook manages behavior.
  • You can reuse useCounter across different components or pages.
  • Logic is easy to test in isolation—call the hook in a test renderer and assert behavior.

Edge cases and improvements

  • Avoid stale state when updates depend on the previous value. Prefer the functional updater form:
setCount(c => c + 1);
  • Return a stable API. Consumers shouldn’t care how you implement it. For example, you could later add decrement, reset, or make the initial value configurable:
function useCounter(initial = 0, step = 1) {
  const [count, setCount] = useState(initial);
  const increment = () => setCount(c => c + step);
  const decrement = () => setCount(c => c - step);
  const reset = () => setCount(initial);
  return { count, increment, decrement, reset };
}

Best practices from experience

  • Keep hooks focused. If a hook does too many things, split it.
  • Name by intent: useCounter, useUser, useDebouncedValue—not useUtils.
  • Document return values and any invariants. Your future self will thank you.
  • Don’t prematurely abstract—extract a hook when duplication appears or logic becomes hard to test.

Takeaway

Custom hooks help you separate behavior from presentation and keep your components lean. Use them to centralize shared logic and create a small, stable API that’s easy to reuse and test.

Knowledge base

Problem snippet:

const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);

Solution snippet:

function useCounter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(c => c + 1);
  return { count, increment };
}
const { count, increment } = useCounter();