Custom Hooks Patterns
Reusable stateful logic extracted into use* functions that follow the Rules of Hooks.
Busca en todas las páginas de la documentación
Reusable stateful logic extracted into use* functions that follow the Rules of Hooks.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Wrap a boolean state with a stable toggle helper for dialogs, menus, and feature flags in UI.
function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn((v) => !v), []);
return [on, toggle, setOn] as const;
}Mirror state to localStorage so refreshes restore the last value. Guard for SSR where window is missing.
function useLocalStorage(key: string, initial: string) {
const [value, setValue] = useState(() =>
typeof window === "undefined" ? initial : localStorage.getItem(key) ?? initial,
);
useEffect(() => {
localStorage.setItem(key, value);
}, [key, value]);
return [value, setValue] as const;
}Subscribe to a CSS media query and return whether it currently matches.
function useMediaQuery(query: string) {
const [match, setMatch] = useState(false);
useEffect(() => {
const mql = window.matchMedia(query);
const onChange = () => setMatch(mql.matches);
onChange();
mql.addEventListener("change", onChange);
return () => mql.removeEventListener("change", onChange);
}, [query]);
return match;
}Delay propagating a rapidly changing value (search text) until the user pauses.
function useDebouncedValue<T>(value: T, ms: number) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = window.setTimeout(() => setDebounced(value), ms);
return () => window.clearTimeout(id);
}, [value, ms]);
return debounced;
}Expose the value from the previous render for comparisons and transitions.
function usePrevious<T>(value: T) {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}Run a handler when a pointer event lands outside a target element - common for popovers.
function useOnClickOutside(ref: RefObject<HTMLElement | null>, handler: () => void) {
useEffect(() => {
const onDown = (e: MouseEvent) => {
if (!ref.current?.contains(e.target as Node)) handler();
};
document.addEventListener("mousedown", onDown);
return () => document.removeEventListener("mousedown", onDown);
}, [ref, handler]);
}Minimal fetch hook with loading and error flags. Prefer libraries for production caching; this shows the shape.
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const ac = new AbortController();
fetch(url, { signal: ac.signal })
.then((r) => r.json())
.then(setData)
.catch((e) => e.name !== "AbortError" && setError(e));
return () => ac.abort();
}, [url]);
return { data, error };
}Declarative interval that always sees the latest callback via a ref.
function useInterval(fn: () => void, ms: number | null) {
const fnRef = useRef(fn);
fnRef.current = fn;
useEffect(() => {
if (ms === null) return;
const id = window.setInterval(() => fnRef.current(), ms);
return () => window.clearInterval(id);
}, [ms]);
}Keep a stable function identity while always calling the latest implementation - useful for event subscriptions.
function useEvent<A extends unknown[], R>(fn: (...args: A) => R) {
const ref = useRef(fn);
ref.current = fn;
return useCallback((...args: A) => ref.current(...args), []);
}Custom hooks may call other hooks. Build complex behavior from small, tested pieces.
function useSearch(items: Item[]) {
const [query, setQuery] = useState("");
const debounced = useDebouncedValue(query, 200);
const results = useMemo(
() => items.filter((i) => i.name.includes(debounced)),
[items, debounced],
);
return { query, setQuery, results };
}Tuples (as const) are concise for 2-3 values; objects scale better when callers need named fields.
// Tuple: const [value, setValue] = useThing();
// Object: const { value, setValue, reset } = useThing();Never call hooks inside conditions or loops. Move the condition inside the hook or split components.
// Bad: if (enabled) useEffect(...)
// Good: useEffect(() => { if (!enabled) return; ... }, [enabled])Each component that calls a custom hook gets its own independent state. Hooks share code, not a global singleton (unless you add one).
function A() { const [n] = useToggle(); /* own state */ }
function B() { const [n] = useToggle(); /* different state */ }Generate stable unique ids for label/input pairing across SSR and client.
function Field({ label }: { label: string }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}Effects inside hooks must clean up the same way as in components. Callers should not need to know about the subscriptions.
function useWindowEvent<K extends keyof WindowEventMap>(
type: K,
handler: (ev: WindowEventMap[K]) => void,
) {
useEffect(() => {
window.addEventListener(type, handler);
return () => window.removeEventListener(type, handler);
}, [type, handler]);
}Stack versions: React 19 · TypeScript (strict) · Rules of Hooks enforced
Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥