Custom hooks, hook composition, and patterns like useReducer with Context to manage complex state without Redux.
Custom Hooks: Extracting Reusable Logic
The golden rule: if you have stateful logic that appears in more than one component, it’s a candidate for a custom hook.
useFetch — the classic
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage:
function UserProfile({ id }: { id: string }) {
const { data, loading, error } = useFetch<User>(`/api/users/${id}`);
if (loading) return <Spinner />;
if (error) return <Error />;
return <Profile user={data!} />;
}
useLocalStorage
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initial;
} catch {
return initial;
}
});
const set = useCallback((val: T | ((prev: T) => T)) => {
setValue(prev => {
const next = typeof val === 'function' ? (val as (p: T) => T)(prev) : val;
localStorage.setItem(key, JSON.stringify(next));
return next;
});
}, [key]);
return [value, set] as const;
}
useReducer + Context: Global State Without Redux
For moderately complex global state, useReducer with Context is enough and doesn’t require external dependencies.
// store/theme.tsx
type Theme = 'dark' | 'light';
type Action = { type: 'TOGGLE_THEME' } | { type: 'SET_THEME'; payload: Theme };
function reducer(state: Theme, action: Action): Theme {
switch (action.type) {
case 'TOGGLE_THEME': return state === 'dark' ? 'light' : 'dark';
case 'SET_THEME': return action.payload;
default: return state;
}
}
const ThemeContext = createContext<{
theme: Theme;
dispatch: Dispatch<Action>;
} | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, dispatch] = useReducer(reducer, 'dark');
return (
<ThemeContext.Provider value={{ theme, dispatch }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be inside ThemeProvider');
return ctx;
}
Hook Composition
Hooks compose naturally — you can build complex hooks from simple ones:
function useUserData(userId: string) {
const { data: user, loading: userLoading } = useFetch<User>(`/api/users/${userId}`);
const { data: posts, loading: postsLoading } = useFetch<Post[]>(`/api/users/${userId}/posts`);
return {
user,
posts,
loading: userLoading || postsLoading,
};
}
useCallback and useMemo — When to Use Them
The rule: use them when the cost of recomputing is greater than the cost of comparing.
// ✅ Use useMemo — expensive calculation
const sortedItems = useMemo(
() => items.sort((a, b) => b.score - a.score),
[items]
);
// ✅ Use useCallback — function passed to memoized component
const handleSubmit = useCallback(
(data: FormData) => {
mutate(data);
},
[mutate]
);
// ❌ Not needed — calculation is trivial
const doubled = useMemo(() => count * 2, [count]);
useRef Beyond DOM Refs
useRef stores mutable values that should not trigger re-renders:
function useInterval(callback: () => void, delay: number) {
const savedCallback = useRef(callback);
// Update the ref without re-render
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
Conclusion
Hooks change how you think in React: instead of “which lifecycle method do I use here”, you think “what effect does this value have”. Composition is the real superpower — small, focused hooks that combine into complex behaviors.
The useReducer + Context pattern covers 80% of cases where you would have previously reached for Redux. Start simple and add complexity only when you need it.