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.
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
- Create
useCounterand addconst [count, setCount] = useState(0). - Define
incrementthat updates state. - Return a small API:
{ count, increment }. - In
Counter, call the hook and render the values. - 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
useCounteracross 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—notuseUtils. - 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();