Custom hooks are JavaScript functions that start with the word "use" and can call other React hooks inside them. They let you extract component logic into reusable functions — sharing stateful logic between components without changing your component hierarchy.
The three rules of hooks:
How custom hooks work: When you call a custom hook, React runs all the hooks inside it in the context of the calling component. Each component that uses the hook gets its own independent state — hooks don't share state between components.
Code — useLocalStorage (persistent state):
1import { useState, useEffect } from "react";23function useLocalStorage<T>(key: string, initialValue: T) {4 const [value, setValue] = useState<T>(() => {5 try {6 const stored = localStorage.getItem(key);7 return stored ? JSON.parse(stored) : initialValue;8 } catch {9 return initialValue;10 }11 });1213 useEffect(() => {14 localStorage.setItem(key, JSON.stringify(value));15 }, [key, value]);1617 return [value, setValue] as const;18}1920// Usage in any component21function Settings() {22 const [theme, setTheme] = useLocalStorage("theme", "light");23 const [language, setLanguage] = useLocalStorage("lang", "en");2425 return (26 <div>27 <select value={theme} onChange={e => setTheme(e.target.value)}>28 <option value="light">Light</option>29 <option value="dark">Dark</option>30 </select>31 <select value={language} onChange={e => setLanguage(e.target.value)}>32 <option value="en">English</option>33 <option value="es">Spanish</option>34 </select>35 </div>36 );37}
Code — useDebounce (delay value updates):
1import { useState, useEffect } from "react";23function useDebounce<T>(value: T, delay: number): T {4 const [debouncedValue, setDebouncedValue] = useState(value);56 useEffect(() => {7 const timer = setTimeout(() => setDebouncedValue(value), delay);8 return () => clearTimeout(timer);9 }, [value, delay]);1011 return debouncedValue;12}1314// Usage15function SearchInput() {16 const [query, setQuery] = useState("");17 const debouncedQuery = useDebounce(query, 300);1819 useEffect(() => {20 if (debouncedQuery) {21 fetchResults(debouncedQuery);22 }23 }, [debouncedQuery]);2425 return <input value={query} onChange={e => setQuery(e.target.value)} />;26}
Code — useMediaQuery (responsive design):
1import { useState, useEffect } from "react";23function useMediaQuery(query: string): boolean {4 const [matches, setMatches] = useState(false);56 useEffect(() => {7 const media = window.matchMedia(query);8 setMatches(media.matches);9 const handler = (e: MediaQueryListEvent) => setMatches(e.matches);10 media.addEventListener("change", handler);11 return () => media.removeEventListener("change", handler);12 }, [query]);1314 return matches;15}1617// Usage18function Layout() {19 const isMobile = useMediaQuery("(max-width: 768px)");20 return isMobile ? <MobileLayout /> : <DesktopLayout />;21}
Benefits of custom hooks:
Custom hooks vs utility functions: A utility function is a plain function that doesn't use hooks. A custom hook can use other hooks (useState, useEffect, etc.). If your logic needs state or effects, it must be a custom hook.