React Custom Hooks
Build reusable custom hooks to extract component logic and share stateful behavior.
TL;DR
- 01Extract stateful logic from components into reusable hook functions.
- 02Start every custom hook name with the word use.
- 03Call other hooks inside your hook to compose behavior.
Tips
- 01Return values from custom hooks in the same format as built-in hooks — use arrays for positional access or objects for named access.
Warnings
- 01Each component that uses a custom hook gets its own isolated state instance — they don't automatically share state between them.
Basic Structure
- Write a custom hook as a regular function that calls React hooks.
function useToggle(initialValue = false) { const [value, setValue] = useState(initialValue); const toggle = useCallback(() => { setValue(v => !v); }, []); return [value, toggle]; } - Combine useState and useEffect to track a browser API value over time.
function useDebouncedValue(value, delayMs = 300) { const [debounced, setDebounced] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebounced(value), delayMs); return () => clearTimeout(timer); // cancel if value changes again }, [value, delayMs]); return debounced; } - Call your hook from a component just like a built-in hook.
function SearchBox() { const [query, setQuery] = useState(''); const debouncedQuery = useDebouncedValue(query, 400); useEffect(() => { if (debouncedQuery) searchApi(debouncedQuery); }, [debouncedQuery]); return <input value={query} onChange={e => setQuery(e.target.value)} />; } - Keep each hook focused on a single concern.
Naming Rules
- Always start custom hook names with use, like useToggle.
// Good: clearly indicates it's a hook function useToggle() { } function useFetch(url) { } function useLocalStorage(key) { } // Bad: doesn't follow naming convention function toggle() { } function fetch() { } - Use camelCase after the use prefix for consistency.
- Naming triggers React ESLint rules and warnings.
- Pick names that describe what the hook does.
Common Patterns
- Build useToggle for managing boolean state.
function useToggle(initial = false) { const [value, setValue] = useState(initial); const toggle = () => setValue(!value); return [value, toggle]; } // Usage function Modal() { const [isOpen, toggleOpen] = useToggle(false); return ( <> <button onClick={toggleOpen}>Open</button> {isOpen && <div>Modal content</div>} </> ); } - Build usePrevious to track previous prop or state values.
function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; }
Composing and Typing Hooks
- Compose several built-in hooks inside one custom hook to build a higher-level abstraction.
function useOnlineStatus() { const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { const goOnline = () => setIsOnline(true); const goOffline = () => setIsOnline(false); window.addEventListener('online', goOnline); window.addEventListener('offline', goOffline); return () => { window.removeEventListener('online', goOnline); window.removeEventListener('offline', goOffline); }; }, []); return isOnline; } - Build one custom hook on top of another instead of duplicating logic.
function useSyncedField(key, initialValue) { const [value, setValue] = useLocalStorage(key, initialValue); // reuse an existing hook const isOnline = useOnlineStatus(); // compose a second hook return { value, setValue, isOnline }; } - Type a generic custom hook so callers get the correct inferred return type.
function useToggle<T = boolean>(initial: T): [T, () => void] { const [value, setValue] = useState(initial); const toggle = useCallback(() => setValue(v => !v as T), []); return [value, toggle]; } const [isOpen, toggleOpen] = useToggle(false); // isOpen inferred as boolean - Test a custom hook in isolation with renderHook from @testing-library/react.
import { renderHook, act } from '@testing-library/react'; test('useToggle flips its value', () => { const { result } = renderHook(() => useToggle(false)); act(() => result.current[1]()); // call the toggle function expect(result.current[0]).toBe(true); }); - renderHook wraps your hook in a tiny test component so you never need a real UI to test it.
- Keep composed hooks shallow — two or three layers deep is easier to trace than a long chain.
Sharing State Across Components
- Each component gets its own state instance of a custom hook.
function useCounter() { const [count, setCount] = useState(0); return [count, () => setCount(count + 1)]; } function Component1() { const [count, increment] = useCounter(); return <button onClick={increment}>{count}</button>; } function Component2() { const [count, increment] = useCounter(); return <button onClick={increment}>{count}</button>; } // Each component has separate count state - Use Context API with custom hooks to share state.
function useAuth() { const context = useContext(AuthContext); if (!context) throw new Error("useAuth needs provider"); return context; }
FAQ
Extract into a custom hook when the same stateful logic (fetching, form handling, subscriptions) appears in multiple components, or when a single component's logic grows complex enough to benefit from separation. Custom hooks let you test and reuse that logic independently of any UI.
No — hooks must be called at the top level of a function, never inside conditionals, loops, or nested functions. This rule applies inside custom hooks too, not just components.
You can't share state via a hook alone — each component calling the hook gets its own independent state instance. To share state, lift it to a common parent and pass it down, or use a context provider that the hook reads from internally.
Return an array (like useState) when consumers will typically rename the values and order is obvious; return an object (like useReducer's dispatch pattern or React Query) when there are many return values or names carry meaning. Mixing both in a single hook adds confusion.
No — a custom hook runs in the context of the component that calls it, so re-render behavior is identical to inlining the logic. The hook abstraction has no runtime overhead.