React Custom Hooks
Extract and share stateful component logic into reusable, composable custom hook functions.
TL;DR
- Extract repeated stateful logic into custom functions prefixed with
use. - Compose built-in primitives like
useStateanduseEffectinside custom hooks. - Return tuples or structured objects to provide flexible
consumerinterfaces.
Hook Extraction Basics
Boolean Toggle HookEncapsulate boolean state toggling into a reusable helper.
export function useToggle(initial = false) {
const [val, setVal] = useState(initial);
const toggle = useCallback(() => {
setVal((v) => !v);
}, []);
return [val, toggle] as const;
}Consuming Custom HookCall custom hook inside any functional React component.
export function ModalTrigger() {
const [isOpen, toggleOpen] = useToggle();
return <Button onClick={toggleOpen}>Toggle</Button>;
}Window Dimension HookTrack window resize events synchronized with state.
export function useWindowSize() {
const read = () => ({
w: window.innerWidth,
h: window.innerHeight,
});
const [size, setSize] = useState(read);
useEffect(() => {
const onResize = () => setSize(read());
window.addEventListener('resize', onResize);
return () =>
window.removeEventListener('resize', onResize);
}, []);
return size;
}Persistence and Storage Hooks
LocalStorage HookPersist state in localStorage.
export function useLocalStorage<T>(
key: string, init: T
) {
const [value, setValue] = useState<T>(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : init;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}Document Title HookSync the tab title with state.
export function useDocumentTitle(title: string) {
useEffect(() => {
document.title = title;
}, [title]);
}Media Query HookTrack a CSS media query.
export function useMediaQuery(query: string) {
const [match, setMatch] = useState(false);
useEffect(() => {
const media = matchMedia(query);
media.onchange = () => setMatch(media.matches);
setMatch(media.matches);
return () => { media.onchange = null; };
}, [query]);
return match;
}Async and Data Hooks
Async Fetch HookFetch JSON and track loading.
export function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url).then(r => r.json()).then(setData)
.finally(() => setLoading(false));
}, [url]);
return { data, loading };
}Debounced Value HookWait for input to pause.
export function useDebounce<T>(value: T, delay = 300) {
const [result, setResult] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setResult(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return result;
}Previous Value HookKeep the prior render's value.
export function usePrevious<T>(value: T) {
const ref = useRef<T>();
useEffect(() => { ref.current = value; }, [value]);
return ref.current;
}Hook Design Best Practices
Single ResponsibilityBuild focused micro-hooks that compose gracefully together.
// Good: Focused hooks
const isMobile = useMediaQuery('(max-width: 768px)');
const theme = useLocalStorage('theme', 'dark');Return Tuple as constMark array returns with as const for exact tuple types.
return [state, actions] as const;Lint Enforced RulesEnsure eslint-plugin-react-hooks is active in project.
// .eslintrc.json
{
"extends": ["plugin:react-hooks/recommended"]
}Tips
- Prefix all custom hook function names with
useso React ESLint plugins can enforceRules of Hooksautomatically. - Keep custom hooks focused on a single responsibility to maximize reusability across unrelated visual
components.
Warnings
- Custom hooks share stateful logic, not state itself; each component calling a custom hook gets isolated independent
state. - Never invoke
hooksinside conditional blocks, nested loops, or helper functions inside your custom hook body.
In Practice
Create a custom hook that monitors navigator online status and alerts users on disconnect.
- Initialize state with current navigator.onLine boolean value.
- Attach online and offline window event listeners inside an effect.
- Update state whenever connection status changes.
- Return the online boolean value for use in any component.
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(
() => navigator.onLine
);
useEffect(() => {
const handleOn = () => setIsOnline(true);
const handleOff = () => setIsOnline(false);
window.addEventListener('online', handleOn);
window.addEventListener('offline', handleOff);
return () => {
window.removeEventListener('online', handleOn);
window.removeEventListener('offline', handleOff);
};
}, []);
return isOnline;
}FAQ
No. Custom hooks share stateful logic and behavior, not state values. Every invocation of a custom hook instantiates completely independent state and effect instances.
The use prefix is a strict React convention. It informs linters and the React runtime that the function contains hook calls and must follow the Rules of Hooks.
Return arrays like [value, setter] when consumers benefit from custom naming via destructuring. Return objects when exporting many properties to avoid rigid position ordering.