Custom Hooks
Learn how to extract reusable stateful logic into your own use-prefixed functions so your components stay clean and your logic stays shareable.
You have already learned how useState, useEffect, and other built-in hooks work. But here is the real power move: you can build your own hooks. Imagine you have a toggle (open/closed, visible/hidden) appearing in five different components. Right now you are probably copying the same const [isOpen, setIsOpen] = useState(false) boilerplate into each one. Custom hooks let you write that logic once, give it a name, and use it everywhere — just like a regular function, but one that can hold state and side effects.
#What Is a Custom Hook?
A custom hook is just a JavaScript function whose name starts with `use` and that calls one or more React hooks inside it. That's it. The use prefix is not magic — it is a convention that tells React (and your linter) to apply the Rules of Hooks to this function. Under the hood, your custom hook is regular JavaScript; the hooks you call inside it do the heavy lifting.
Think of it like a kitchen appliance
A blender combines a motor, blades, and a jar into one reusable tool. You do not re-wire a motor every time you want a smoothie — you just press the button. A custom hook is the same idea: it packages up useState, useEffect, or whatever else you need into one reusable "appliance" your components can just plug in.
#Your First Custom Hook: useToggle
Let's build useToggle — a hook that manages a boolean that can flip between true and false. This covers the most common UI pattern: show/hide, open/close, on/off.
import { useState, useCallback } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(prev => !prev);
}, []);
return [value, toggle];
}
export default useToggle;Now any component can use it without knowing anything about useState or the toggling logic:
import useToggle from './useToggle';
function Accordion({ title, children }) {
const [isOpen, toggle] = useToggle(false);
return (
<div>
<button onClick={toggle}>{title}</button>
{isOpen && <div>{children}</div>}
</div>
);
}#A More Powerful Hook: useLocalStorage
useLocalStorage is a popular custom hook that works exactly like useState but persists the value in the browser's localStorage. When the user refreshes the page, the value survives. Let's build one:
import { useState } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
export default useLocalStorage;import useLocalStorage from './useLocalStorage';
function ThemePicker() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
);
}#The Rules of Hooks (They Apply to Custom Hooks Too)
React enforces two rules for all hooks — built-in and custom alike:
- Only call hooks at the top level. Never inside loops, conditions, or nested functions. This keeps the hook call order consistent across renders, which is how React tracks state.
- Only call hooks from React functions. That means function components or other custom hooks. Never from a plain helper function or class.
Conditional hook calls will break React
This is the most common beginner mistake:
``jsx // BAD — hooks must not be inside conditions if (isLoggedIn) { const [name, setName] = useState(''); } ``
React relies on the order of hook calls being identical every render. If you skip a hook call, React loses track of which state belongs to which hook and everything breaks. Always call hooks unconditionally at the top of your function.
#Why Custom Hooks Keep Components Clean
When logic lives directly in a component, the component mixes two concerns: what to render and how state works. As features grow, components balloon into hundreds of lines. Custom hooks let you pull the how into its own file, leaving the component to focus entirely on the what. This also makes testing easier — you can test a hook in isolation without rendering any UI.
Name your hook after what it does, not how
Call it useToggle, useLocalStorage, useWindowSize, useDebounce — names that describe the behaviour from the caller's perspective. Avoid names like useStateAndCallback that expose implementation details.
Which of the following is a valid place to call a hook inside a custom hook?
Each component gets its own state
Even though two components call the same custom hook, each one gets its own independent state. The hook is not a singleton — it is a recipe. Every time a component uses useToggle(), React creates a fresh useState slot for that component. Sharing logic does not mean sharing state.
Key takeaways
- A custom hook is a function whose name starts with `use` and calls other hooks inside — no special API required.
- Custom hooks let you extract reusable stateful logic so components stay focused on rendering.
- The Rules of Hooks apply equally to custom hooks: call unconditionally, only from React functions.
- Each component that calls the same custom hook gets its own independent state — logic is shared, state is not.
- Name hooks after the behaviour they expose (useToggle, useLocalStorage) to keep your code readable.
This code has a bug — what's wrong?
function useToggle(initialValue = false) {
if (initialValue) {
const [value, setValue] = useState(true);
return [value, () => setValue(prev => !prev)];
}
const [value, setValue] = useState(false);
return [value, () => setValue(prev => !prev)];
}Complete the toggle function so it flips the boolean based on its latest value. Fill in the parameter name used in the functional updater.
function useToggle(initialValue = false) { const [value, setValue] = useState(initialValue); const toggle = useCallback(() => { setValue( => !); }, []); return [value, toggle]; }
Arrange these lines into a correct useCounter custom hook that creates count state, defines an increment function, and returns them as an object.
function useCounter(initialValue = 0) { return { count, increment };const increment = useCallback(() => setCount(prev => prev + 1), []);
}
const [count, setCount] = useState(initialValue);
Two Accordion components each call the same useToggle hook. The first is toggled open; the second is left alone. What does the console.log print after the first is opened?
function useToggle(initial = false) {
const [open, setOpen] = useState(initial);
const toggle = () => setOpen(prev => !prev);
console.log(open);
return [open, toggle];
}
// <Accordion /> A: user clicks toggle once
// <Accordion /> B: never clickedBuild a custom hook called useCounter that manages a numeric counter. It should accept an initialValue (default 0) and return an object with count, increment, decrement, and reset functions. Then use it in a Counter component that displays the count and three buttons.
Try it live — edit the code and hit Run to see it rendered: