Learn how to extract reusable stateful logic into custom hooks, follow naming conventions, and build real-world hooks like useLocalStorage, useToggle, and useDebounce.
As your React application grows, you will notice that different components often share the same stateful logic. For example, multiple components might need to track whether a modal is open, subscribe to window resize events, or debounce user input.
Before hooks, the only ways to share stateful logic were Higher-Order Components (HOCs) and Render Props — both of which led to deeply nested component trees ("wrapper hell") and made code harder to follow.
Custom hooks solve this problem elegantly. A custom hook is simply a JavaScript function whose name starts with use and that calls other hooks inside it. You extract the shared logic into the custom hook, and any component that needs it can call it directly — no wrappers, no nesting.
// Before: duplicated logic in two components
function ComponentA() {
const [isOpen, setIsOpen] = React.useState(false);
const toggle = () => setIsOpen(prev => !prev);
// ...
}
function ComponentB() {
const [isOpen, setIsOpen] = React.useState(false);
const toggle = () => setIsOpen(prev => !prev);
// ...
}
// After: shared logic in a custom hook
function useToggle(initial = false) {
const [value, setValue] = React.useState(initial);
const toggle = () => setValue(prev => !prev);
return [value, toggle];
}
function ComponentA() {
const [isOpen, toggle] = useToggle();
// ...
}Each component that calls useToggle gets its own independent copy of the state. Custom hooks share logic, not state.
Custom hooks must follow a strict naming convention and the same rules as built-in hooks.
use prefixEvery custom hook must start with the word use, followed by a capital letter. This is not just a convention — React's linter relies on it to enforce the rules of hooks.
// Good — recognized as a hook
function useCounter() { ... }
function useLocalStorage(key) { ... }
function useWindowSize() { ... }
// Bad — NOT recognized as a hook
function getCounter() { ... } // linter won't check hook rules
function counter() { ... } // same problemCustom hooks must follow the same two rules as all hooks:
// WRONG — conditional hook call
function useBadHook(condition) {
if (condition) {
const [value, setValue] = React.useState(0); // breaks!
}
}
// RIGHT — always call the hook, use the condition for behavior
function useGoodHook(condition) {
const [value, setValue] = React.useState(0);
// use condition in the return or in an effect
}Custom hooks can call other custom hooks. This is one of the most powerful patterns — you can compose hooks together:
function useDebounce(value, delay) {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function useDebouncedSearch(query) {
const debouncedQuery = useDebounce(query, 300);
const [results, setResults] = React.useState([]);
// ... fetch based on debouncedQuery
return results;
}Let us build three practical custom hooks that you will use regularly in real projects.
Persist state to localStorage so it survives page refreshes:
function useLocalStorage(key, initialValue) {
const [value, setValue] = React.useState(() => {
try {
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
React.useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
function App() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
// theme persists across page refreshes!
}Note the lazy initializer passed to useState — the arrow function () => { ... } runs only once on the first render, avoiding a localStorage.getItem call on every render.
A simple but extremely common hook for boolean state:
function useToggle(initial = false) {
const [value, setValue] = React.useState(initial);
const toggle = React.useCallback(() => {
setValue(prev => !prev);
}, []);
return [value, toggle];
}
// Usage
function Modal() {
const [isOpen, toggleOpen] = useToggle();
return (
<>
<button onClick={toggleOpen}>
{isOpen ? 'Close' : 'Open'}
</button>
{isOpen && <div className="modal">Modal content</div>}
</>
);
}Delay updating a value until the user stops changing it. Essential for search inputs:
function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Usage — only searches after user stops typing for 300ms
function SearchBar() {
const [query, setQuery] = React.useState('');
const debouncedQuery = useDebounce(query, 300);
React.useEffect(() => {
if (debouncedQuery) {
// fetch search results
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}Custom hooks can return anything: a single value, an array, or an object. The convention depends on the use case.
Following the useState pattern, return an array when callers will likely want to rename the values:
function useToggle(initial) {
// ...
return [value, toggle]; // array
}
// Caller can name them anything:
const [isOpen, toggleOpen] = useToggle(false);
const [isDark, toggleDark] = useToggle(true);When you return many related values, use an object so callers can destructure only what they need:
function useCounter(initial = 0) {
const [count, setCount] = React.useState(initial);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initial);
return { count, increment, decrement, reset }; // object
}
// Caller destructures by name:
const { count, increment, reset } = useCounter(10);When the hook computes a single derived value:
function useDebounce(value, delay) {
// ...
return debounced; // single value
}
const debouncedSearch = useDebounce(searchTerm, 500);Choose the return shape that makes the hook most ergonomic for its callers.
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// Custom hook: useCounter
function useCounter(initial = 0) {
const [count, setCount] = React.useState(initial);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initial);
return { count, increment, decrement, reset };
}
// Custom hook: useToggle
function useToggle(initial = false) {
const [value, setValue] = React.useState(initial);
const toggle = () => setValue(prev => !prev);
return [value, toggle];
}
function App() {
const { count, increment, decrement, reset } = useCounter(0);
const [isVisible, toggleVisible] = useToggle(true);
return (
<div>
<h2>useCounter Demo</h2>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
<h2>useToggle Demo</h2>
<button onClick={toggleVisible}>
{isVisible ? "Hide" : "Show"} Content
</button>
{isVisible && <p>This content is toggled!</p>}
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>What is the primary naming requirement for a custom hook?