Custom Hooks
Learn how to extract and reuse stateful logic across components by building your own custom Hooks.
Introduction
As applications grow, you often find the same piece of stateful logic — tracking window size, syncing a value to localStorage, subscribing to an event, managing a form field — copied and pasted across several components. Copy-pasted logic is hard to maintain: fix a bug in one copy, and the others quietly stay broken.
Custom Hooks solve this by letting you extract that logic into its own reusable function, built out of React's built-in Hooks. This lesson shows why custom Hooks exist, the rules for building one, and walks through two practical examples: useWindowWidth and useLocalStorage.
- Why custom Hooks exist and the problem they solve.
- The "use" naming convention and why it matters.
- How to build a custom Hook that tracks window width.
- How to reuse the same custom Hook across multiple components.
- That custom Hooks can call other Hooks internally.
Why Custom Hooks Exist
Before Hooks, sharing stateful logic between components required patterns like Higher-Order Components or render props, both of which add extra layers of nesting and indirection. Custom Hooks let you extract stateful logic into a plain JavaScript function that any component can call directly, with no extra wrapping components involved.
Reusable Logic
Write a piece of stateful logic once and use it across as many components as you need.
Cleaner Components
Components stay focused on rendering UI, while a custom Hook handles the underlying logic.
Easier to Test
Logic in a custom Hook can be tested independently of any particular component.
Just a Function
A custom Hook is a normal JavaScript function that happens to call other Hooks — no new syntax to learn.
The Naming Convention
Every custom Hook must start with the word "use" — for example useWindowWidth, useLocalStorage, or useFetch. This is not just a style preference: React's own linter rules (like the Hooks ESLint plugin) use this naming convention to know which functions are Hooks, so they can correctly enforce the Rules of Hooks, such as not calling Hooks conditionally.
- React and its tooling identify Hooks by the "use" prefix, not by any special keyword.
- A function named calculateTotal() is treated as a regular function — React would not warn you if you broke the Rules of Hooks inside it.
- A function named useSomething() is checked by linting tools to make sure Hooks inside it are called correctly.
Building useWindowWidth
Let's build a custom Hook that tracks the browser window's width and updates whenever the window is resized. Internally, it simply combines useState and useEffect — the same Hooks you already know — packaged into one reusable function.
import { useState, useEffect } from "react";
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
// Cleanup: remove the listener when the component unmounts
return () => window.removeEventListener("resize", handleResize);
}, []);
return width;
}
export default useWindowWidth;Notice this looks exactly like logic you might write directly inside a component — the only difference is it lives in its own function named with the "use" prefix, so it can be reused anywhere.
Using the Hook in Multiple Components
Once defined, useWindowWidth can be called from any number of components, and each one gets its own independent, live-updating width value.
import useWindowWidth from "./useWindowWidth";
function Header() {
const width = useWindowWidth();
return <header>Window width: {width}px</header>;
}import useWindowWidth from "./useWindowWidth";
function Sidebar() {
const width = useWindowWidth();
return (
<aside>
{width < 768 ? "Mobile layout" : "Desktop layout"}
</aside>
);
}Both Header and Sidebar independently track window width.
Resizing the browser updates both components automatically,
even though neither one duplicates the resize-listener logic.Each component that calls useWindowWidth() gets its own separate useState and useEffect internally — calling a custom Hook does not share state between components, it shares the logic that creates that state.
Building useLocalStorage
Custom Hooks can call other Hooks internally, and even other custom Hooks. Here is a second example: useLocalStorage, which behaves like useState but automatically persists its value to the browser's localStorage.
import { useState, useEffect } from "react";
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
export default useLocalStorage;import useLocalStorage from "./useLocalStorage";
function NotesApp() {
const [note, setNote] = useLocalStorage("my-note", "");
return (
<textarea
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Type a note — it will be saved automatically"
/>
);
}Typing into the textarea saves the note to localStorage.
Refreshing the page restores the last saved note automatically,
because useLocalStorage reads it back on initial render.Common Mistakes
- Naming a Hook without the "use" prefix — this breaks linting tools and makes the Rules of Hooks unenforceable.
- Calling a custom Hook conditionally or inside a loop, which violates the same Rules of Hooks that apply to built-in Hooks.
- Assuming a custom Hook shares state between every component that calls it — each call creates its own independent state.
- Putting unrelated logic into a single giant custom Hook instead of splitting it into smaller, focused Hooks.
Best Practices
- Always prefix custom Hook names with "use".
- Keep each custom Hook focused on one piece of logic, the same way you keep components focused on one responsibility.
- Return whatever shape makes the Hook easiest to use — a single value, an array like useState, or an object with named properties.
- Clean up subscriptions, timers, and event listeners inside custom Hooks just as you would in a plain useEffect.
- Extract logic into a custom Hook as soon as you notice it duplicated across two or more components.
Frequently Asked Questions
Is a custom Hook a component?
No. A custom Hook is a plain JavaScript function that calls other Hooks. It does not return JSX and is not rendered like a component.
Do two components using the same custom Hook share state?
No. Each call to the Hook creates its own independent state — using useWindowWidth() in two components gives each one its own separate width value.
Can a custom Hook call another custom Hook?
Yes. Custom Hooks can freely call built-in Hooks or other custom Hooks, as long as the Rules of Hooks are followed at every level.
Where should I put custom Hook files?
A common convention is a dedicated hooks/ folder, with one file per Hook, named to match the Hook itself (e.g. useWindowWidth.js).
Do custom Hooks need to be published as packages to be reused?
No. Reuse just means importing the same function into multiple components within your own project — publishing is only necessary if you want to share it across separate projects.
Key Takeaways
- Custom Hooks let you extract and reuse stateful logic shared across multiple components.
- A custom Hook must start with "use" so React's tooling can enforce the Rules of Hooks.
- A custom Hook is simply a function that calls other Hooks internally.
- You built useWindowWidth (tracking resize events) and useLocalStorage (persisting state) as working examples.
- Each component calling a custom Hook gets its own independent state, not shared state.
Summary
Custom Hooks turn repeated stateful logic into a single, reusable, well-named function, keeping components smaller and easier to maintain. Because a custom Hook is just a regular function built from other Hooks, there is no new concept to learn — only a naming convention and the same Rules of Hooks you already follow.
In this lesson, you learned why custom Hooks exist, the "use" naming rule, and built two working examples reused across multiple components. Next, you will step back and look at the full component lifecycle — mounting, updating, and unmounting — and how Hooks map onto each phase.
- You understand why custom Hooks exist and the problem they solve.
- You know the "use" naming convention and why it matters.
- You built useWindowWidth and reused it across two components.
- You built useLocalStorage and saw a custom Hook call multiple built-in Hooks.