useState, useEffect, useContext, useRef, useMemo, useCallback, and custom hooks
Declare state variable and its setter function
const [count, setCount] = useState(0);
const [name, setName] = useState<string>("");Update state based on previous value
setCount(prev => prev + 1);
setItems(prev => [...prev, newItem]);Manage object state — spread to merge updates
const [user, setUser] = useState({ name: "", age: 0 });
setUser(prev => ({ ...prev, name: "Alice" }));Run side effects after every render
useEffect(() => {
document.title = `Count: ${count}`;
});Run effect only when specific values change
useEffect(() => {
fetchUser(userId);
}, [userId]);Run effect once when component mounts
useEffect(() => {
loadData();
}, []); // empty deps = mount onlyReturn cleanup function to cancel subscriptions
useEffect(() => {
const sub = subscribe(id);
return () => sub.unsubscribe();
}, [id]);Create a context with a default value
const ThemeContext = createContext<"light" | "dark">("light");Read context value in a component
const theme = useContext(ThemeContext);Wrap components to provide context value
<ThemeContext.Provider value="dark">
<App />
</ThemeContext.Provider>Access a DOM element directly
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />
inputRef.current?.focus();Store a value that persists without causing re-render
const timerRef = useRef<NodeJS.Timeout | null>(null);
timerRef.current = setTimeout(fn, 1000);Memoize expensive computed values
const total = useMemo(() => {
return items.reduce((sum, i) => sum + i.price, 0);
}, [items]);Memoize callback functions to prevent child re-renders
const handleClick = useCallback((id: number) => {
dispatch({ type: "DELETE", id });
}, [dispatch]);Manage complex state with a reducer function
type Action = { type: "INC" } | { type: "DEC" };
function reducer(state: number, action: Action): number {
if (action.type === "INC") return state + 1;
return state - 1;
}
const [count, dispatch] = useReducer(reducer, 0);Extract reusable stateful logic into a custom hook
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = () => setCount(c => c + 1);
const reset = () => setCount(initial);
return { count, increment, reset };
}Custom hook for async data loading with loading and error states
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(r => r.json())
.then(d => { if (!cancelled) setData(d); })
.catch(e => { if (!cancelled) setError(e.message); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}Delay state updates until user stops typing
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Usage
const debouncedSearch = useDebounce(searchQuery, 300);Persist state to localStorage with automatic sync
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 = (val: T) => {
setValue(val);
localStorage.setItem(key, JSON.stringify(val));
};
return [value, set] as const;
}Never call hooks inside loops, conditions, or nested functions — hooks must be called at the top level
Memoize expensive computations with useMemo and stable callbacks with useCallback
Always clean up subscriptions, timers, and event listeners in the cleanup function returned from useEffect
Use the eslint-plugin-react-hooks exhaustive-deps rule to catch missing dependencies
Extract complex hook logic into named custom hooks (prefixed with use) for reusability and testability