Common Custom Hooks
Many custom hooks appear again and again across React apps. Studying them helps you recognize good hook boundaries.
useToggle
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue((current) => !current);
}, []);
const turnOn = useCallback(() => setValue(true), []);
const turnOff = useCallback(() => setValue(false), []);
return { value, toggle, turnOn, turnOff };
}Use this for simple open/closed UI such as panels, menus, and dialogs.
useDebouncedValue
function useDebouncedValue(value, delayMs) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const id = setTimeout(() => {
setDebouncedValue(value);
}, delayMs);
return () => clearTimeout(id);
}, [value, delayMs]);
return debouncedValue;
}This helps avoid running expensive work or requests for every keystroke.
usePrevious
function usePrevious(value) {
const previousRef = useRef();
useEffect(() => {
previousRef.current = value;
}, [value]);
return previousRef.current;
}The first render returns undefined because there is no previous value yet.
useLocalStorageState
function useLocalStorageState(key, initialValue) {
const [value, setValue] = useState(() => {
const item = window.localStorage.getItem(key);
return item === null ? initialValue : JSON.parse(item);
});
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}Edge cases matter here. localStorage may be unavailable in some environments, JSON parsing can fail, and server rendering does not have window.
useFetch Awareness
A simple useFetch hook is a good exercise, but production apps often need caching, retries, deduplication, stale data handling, pagination, and invalidation.
Libraries such as TanStack Query or framework data APIs may be better than a hand-written fetch hook for serious data loading.
useEventListener
function useEventListener(target, eventName, handler) {
useEffect(() => {
if (!target) {
return;
}
target.addEventListener(eventName, handler);
return () => {
target.removeEventListener(eventName, handler);
};
}, [target, eventName, handler]);
}This hook is simple, but the handler dependency matters. If callers pass a new function every render, the listener is removed and added repeatedly. Sometimes that is fine; sometimes you need a stable callback or a ref-based latest handler pattern.
What is a common edge case for a usePrevious hook?
Common Mistakes
Do not copy a common hook without understanding its edge cases. Hooks that touch storage, network, timers, or events need cleanup and environment awareness.
Do not build a data-fetching library accidentally. Once you need cache invalidation and request deduplication, use a tool designed for that problem.
Do not ignore accessibility when hooks drive UI state. A disclosure hook still needs components to render correct ARIA attributes and focus behavior.
Practice Challenge
Implement useClickOutside.
Requirements:
- return a ref to attach to an element
- call a handler when a pointer event starts outside the element
- clean up the document listener
- handle the initial
ref.current === nullcase - explain how you would test it
Recap
Common hooks are patterns, not magic. Use them to learn boundaries, cleanup, dependency handling, and edge cases. In production, choose between a small custom hook and a mature library based on the complexity of the problem.