
A practical guide to writing custom React Hooks: the patterns they replaced, the rules they must follow, when to extract one, and libraries that cover the rest.
Aurora Scharff
May 21, 2026
Custom Hooks let you share stateful logic between components. They've become the default way to reuse behavior in React, replacing older patterns like higher-order components, render props, and container/presentational components. In this post, we'll look at what custom Hooks replaced and why, the rules they have to follow, when to extract one, when not to, and where to find ready-made ones for common cases.
A custom Hook is a function whose name starts with use and that calls other Hooks. Here's a tiny one:
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
return { count, increment };
}
Now any component can use it:
function Counter() {
const { count, increment } = useCounter();
return <button onClick={increment}>{count}</button>;
}
The use prefix isn't decorative. Because custom Hooks run as part of React's render, they have to follow the Rules of React and the Rules of Hooks: they must be pure, they must not have side effects during render, they must not mutate values they didn't create, and they must call other Hooks at the top level. The same rules that apply to components apply to your Hooks. We'll come back to these in detail below.
Each call to a custom Hook is independent. Two components calling useCounter() each get their own state. Custom Hooks share logic, not state. If you need to share the actual value, lift state up or use Context.
Before Hooks were introduced in React 16.8, sharing stateful logic between components required indirect patterns. Each one had its tradeoffs, and custom Hooks now handle most of them more cleanly.
A higher-order component is a function that takes a component and returns a new one with extra behavior:
function withLoader(Component, url) {
return function LoaderComponent(props) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url).then(res => res.json()).then(setData);
}, []);
if (!data) return <div>Loading...</div>;
return <Component {...props} data={data} />;
};
}
const UserList = withLoader(List, '/api/users');
The same logic as a custom Hook:
function useLoader(url) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url).then(res => res.json()).then(setData);
}, [url]);
return data;
}
function UserList() {
const data = useLoader('/api/users');
if (!data) return <div>Loading...</div>;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
No wrapper component, no "wrapper hell" in the React DevTools, and the data flows where the consumer wants it.
Render props pass a function as a prop (often children, but it can be any prop name) and call it with the shared values:
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return children(position);
}
<MouseTracker>
{({ x, y }) => <div>Mouse: {x}, {y}</div>}
</MouseTracker>
The Hook version is flatter:
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return position;
}
function App() {
const { x, y } = useMousePosition();
return <div>Mouse: {x}, {y}</div>;
}
No callback nesting, and you can combine multiple Hooks side by side without the "render-prop pyramid".
The container/presentational split separated data-fetching logic into a "container" component and rendering into a "presentational" one:
function UserProfileContainer({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`).then(res => res.json()).then(setUser);
}, [userId]);
if (!user) return <div>Loading...</div>;
return <UserProfile user={user} />;
}
A custom Hook achieves the same separation without an extra component:
function useUser(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`).then(res => res.json()).then(setUser);
}, [userId]);
return user;
}
function UserProfile({ userId }) {
const user = useUser(userId);
if (!user) return <div>Loading...</div>;
return <div><h2>{user.name}</h2><p>{user.email}</p></div>;
}
Logic and presentation stay separate, but there's no wrapper to thread props through.
Custom Hooks have to follow the same rules as the built-in ones. There are two layers worth knowing.
These govern how Hooks are called:
try/catch/finally blocks, or after early returns. function useUser(id) {
if (!id) return null; // ❌ Early return before Hooks
const [user, setUser] = useState(null);
}
The eslint-plugin-react-hooks plugin catches violations automatically. If you feel like you need to call a Hook conditionally, restructure the component instead.
The broader Rules of React apply to the body of your Hook just like they apply to a component. Two matter most for custom Hooks:
Hooks must be pure. The same inputs should produce the same output. Don't mutate props, state, or any value you didn't create during render, and don't perform side effects:
// ❌ Impure: mutates the caller's array during render
function useSortedItems(items) {
items.sort();
return items;
}
// ✅ Pure: returns a new array
function useSortedItems(items) {
return [...items].sort();
}
Side effects belong inside useEffect(), event handlers, or refs. Never in the body of a Hook itself. React will call your Hook more than you expect (twice in development under StrictMode, plus concurrent renders that get discarded), and impurity surfaces as bugs that are hard to reproduce. For more on this, see Component Purity and StrictMode in React.
React calls your Hook, not you. Never call a custom Hook from a regular function, and never pass one around as a value. React tracks state by call order, so the calling pattern has to stay consistent.
The React docs are clear: don't add the use prefix to a function just because it's reusable. Extract a Hook when one of these is true:
The classic example is tracking online status:
// Before: logic mixed into the component
function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return <h1>{isOnline ? 'Online' : 'Offline'}</h1>;
}
Extracted into a Hook:
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? 'Online' : 'Offline'}</h1>;
}
The status logic now lives in one place where any component can reuse it.
Some extractions look helpful but aren't.
Avoid wrapping a single useState. A useToggle() Hook that returns [value, () => setValue(v => !v)] saves one line and adds an extra layer. Usually not worth it.
Avoid lifecycle Hooks like useMount():
// ❌ Don't do this
function useMount(fn) {
useEffect(() => { fn(); }, []);
}
This hides dependencies from the linter and pretends React has a lifecycle when it really works through synchronization. Write the Effect directly so the linter can verify your dependencies.
Don't use the use prefix on pure helpers. Functions that don't call any Hooks shouldn't start with use. A formatter like formatCurrency() or a calculator like sumPrices() is just a regular function. The linter relies on the use prefix to know where the Rules of Hooks apply.
Custom Hooks compose. Build small, focused primitives and combine them into bigger ones.
Start with a generic useLocalStorage():
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
Then build useTheme() on top of it:
function useTheme() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return { theme, toggleTheme };
}
Each Hook stays simple, and the higher-level Hook reads like a description of what it does.
A common question when designing custom Hooks: should you wrap every returned function in useCallback() so its identity stays stable across renders?
The older answer was yes. If the function gets passed into a dependency array or to a memoized child component, an unstable reference would cause unnecessary re-renders or Effect re-runs.
With the React Compiler, now stable, the answer changes. The Compiler memoizes your code automatically. From the official docs:
By default, React Compiler will memoize your code based on its analysis and heuristics. In most cases, this memoization will be as precise, or moreso, than what you may have written.
When the Compiler is enabled, you generally don't need to wrap functions returned from custom Hooks in useCallback(). The Compiler analyzes the Hook and produces equivalent memoization. The docs recommend relying on the Compiler for new code, and only reaching for useMemo() and useCallback() as an escape hatch when you need explicit, precise control.
If the Compiler isn't enabled in your project yet, the older rule still applies: wrap returned functions with useCallback() when their identity matters to consumers. For a deeper look, see React Compiler: No More useMemo and useCallback.
If your Hook accepts a callback from the consumer and uses it inside an Effect, the Effect will re-run every time the parent re-renders. The new useEffectEvent() API, stabilized in React 19.2, solves this by letting you read the latest callback without making the Effect reactive to it:
function useChatRoom({ roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);
useEffect(() => {
const connection = createConnection(roomId);
connection.on('message', msg => onMessage(msg));
connection.connect();
return () => connection.disconnect();
}, [roomId]);
}
Reach for it whenever your custom Hook accepts an event handler prop.
Before writing a custom Hook for a common need, check if one already exists. Several well-maintained libraries provide ready-to-use Hooks for the cases you'll hit most often:
useDebounce, useLocalStorage, useMediaQuery, useClickOutside, and many more. Copy-paste snippets you can adapt to your codebase.These libraries cover the bulk of "I need a Hook that does X" situations. Reach for one of them before writing your own, especially for things like debouncing, media queries, or click-outside detection. Save your custom Hooks for logic that's specific to your application.
useMount(), and the use prefix on pure helpers.useLocalStorage() becomes useTheme().useCallback(). The Compiler handles memoization for you.Custom Hooks are useful, but the best ones stay simple. Write the inline code first, extract a Hook the second time you reach for the same logic, and check the ecosystem before building something that already exists.
For more on the patterns custom Hooks replaced, see React Children and cloneElement: Component Patterns from the Docs.
Sources:
Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Writing Custom Hooks in React: Patterns, Pitfalls, and When to Reach for One
A practical guide to writing custom React Hooks: the patterns they replaced, the rules they must follow, when to extract one, and libraries that cover the rest.
Aurora Scharff
May 21, 2026

State Management in Nuxt: Pinia or sticking to basics?
Using Pinia vs Basic State Management: When Vue's built-in reactivity is enough and when Pinia earns its place in your project.
Reza Baar
May 20, 2026

How Eloquent Actually Builds Your Models
A deep dive into Laravel Eloquent under the hood — explore how models are resolved, hydrated, and persisted, and uncover the internal mechanics most developers use daily but rarely fully understand.
Steve McDougall
May 14, 2026
We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.
Let us help you get the resources you need.
