React
medium
mid
How would you make a custom React hook reusable across many different components?
Design custom hooks like small libraries: parameterize all inputs, return a stable shape ({ data, loading, error, refetch }), keep side-effects (subscriptions, listeners) inside with cleanup, avoid hard-coding UI concerns or context dependencies, document the contract, and write tests with @testing-library/react's renderHook.
6 min read·~15 min to think through
A reusable hook is just an API — design it like one.
Principles
- Parameterize inputs — don't read URL params or context internally if the hook should also work elsewhere.
- Return a stable shape — predictable destructuring:
{ data, loading, error, refetch }or[value, setValue]. - Encapsulate side-effects — set up listeners/subscriptions in useEffect with proper cleanup.
- Stable identities — wrap returned functions in useCallback so consumers can put them in deps without infinite loops.
- No UI — hooks return data and callbacks, never JSX.
- Generics over types — use TypeScript generics for shape flexibility.
Example: useFetch
tsx
export function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
const refetch = useCallback(async () => {
setLoading(true);
try {
const res = await fetch(url);
if (!res.ok) throw new Error(res.statusText);
setData(await res.json());
setError(null);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => {
let cancelled = false;
refetch().catch(() => {
if (cancelled) return;
});
return () => { cancelled = true; };
}, [refetch]);
return { data, loading, error, refetch };
}Consumer:
tsx
const { data, loading, error, refetch } = useFetch<User>('/api/user');Example: useLocalStorage (parameterized key)
tsx
export function useLocalStorage<T>(key: string, initial: T) {
const [val, setVal] = useState<T>(() => {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(val));
}, [key, val]);
return [val, setVal] as const;
}Composability
Hooks should compose. Build small ones and combine:
tsx
function useDebounced<T>(value: T, ms = 300) { ... }
function useSearch() {
const [q, setQ] = useState('');
const debounced = useDebounced(q);
const { data } = useFetch(`/search?q=${debounced}`);
return { q, setQ, results: data };
}Tests
tsx
import { renderHook, act } from '@testing-library/react';
test('counter increments', () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.inc());
expect(result.current.count).toBe(1);
});Anti-patterns
- Reading context inside the hook 'for convenience' — couples it to a provider tree.
- Returning unstable functions that change every render — breaks downstream useEffect deps.
- Doing imperative DOM work without a ref the caller passes in.
- Reaching for global state when a parameter would do.
Follow-up questions
- •How do you avoid stale closures inside a custom hook?
- •When should a hook return useCallback-wrapped functions?
- •How do you test a hook that uses useEffect?
Common mistakes
- •Hardcoding URLs or keys inside the hook — kills reusability.
- •Returning a fresh object/function each render that consumers put in deps — infinite loop.
- •Bundling unrelated concerns (data + UI + URL parsing) into one mega-hook.
Performance considerations
- •Hooks themselves are cheap. The cost comes from triggering effects every render due to unstable deps. Audit returned identities (useCallback/useMemo where needed) and make heavy work conditional on real changes.
Edge cases
- •If the parameter is an object/array, the consumer must memoize it or the hook's effects re-run forever.
- •SSR-safe hooks must guard `window`/`localStorage` access.
- •StrictMode double-mount means setup/teardown must be idempotent.
Real-world examples
- •react-use, usehooks.com, TanStack Query (useQuery is a public-API hook), React Hook Form (useForm). Each parameterizes inputs, returns stable shapes, and stays UI-agnostic.
Senior engineer discussion
Senior signal: thinking of hooks as a public API for a component. Naming, shape stability, error model, and observability matter. The same discipline that makes a good React component or function makes a good hook.
Related questions
React
Medium
6 min