React Cheatsheet
Hooks, patterns, and utilities
Built-in Hooks
useState
State management for functional components
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);
// Update
setCount(5);
setCount(prev => prev + 1);
setUser(prev => prev ? { ...prev, name: "New" } : null);
setItems(prev => [...prev, "new"]);
setItems(prev => prev.filter(x => x !== "remove"));useEffect
Side effects and lifecycle
// Every render
useEffect(() => { console.log("Rendered"); });
// Mount only
useEffect(() => { console.log("Mounted"); }, []);
// Dependency change
useEffect(() => { console.log(count); }, [count]);
// Cleanup
useEffect(() => {
const sub = api.subscribe();
return () => sub.unsubscribe();
}, []);
// Fetch pattern
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(e => e.name !== "AbortError" && setError(e));
return () => controller.abort();
}, [url]);useRef
Mutable references across renders
// DOM reference
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();
// Mutable value (no re-render)
const countRef = useRef(0);
countRef.current += 1;
// Store previous value
const prevRef = useRef<number>();
useEffect(() => { prevRef.current = value; }, [value]);
// Timer reference
const timerRef = useRef<NodeJS.Timeout>();
timerRef.current = setTimeout(() => {}, 1000);
clearTimeout(timerRef.current);useMemo
Memoize expensive computations
// Expensive computation
const sorted = useMemo(() => items.sort(), [items]);
// Filtered list
const filtered = useMemo(() =>
users.filter(u => u.name.includes(search)),
[users, search]);
// Derived calculation
const total = useMemo(() =>
cart.reduce((sum, i) => sum + i.price, 0),
[cart]);
// Stable object reference
const config = useMemo(() => ({ theme, locale }), [theme, locale]);useCallback
Memoize functions
// Basic
const handleClick = useCallback(() => {
doSomething();
}, []);
// With dependencies
const handleSubmit = useCallback((data: FormData) => {
submit(data, userId);
}, [userId]);
// For memoized children
const MemoChild = memo(({ onClick }: { onClick: () => void }) => (
<button onClick={onClick}>Click</button>
));
function Parent() {
const handleClick = useCallback(() => action(), []);
return <MemoChild onClick={handleClick} />;
}useReducer
Complex state with actions
type State = { count: number; error: string | null };
type Action =
| { type: "INC" }
| { type: "DEC" }
| { type: "SET"; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "INC": return { ...state, count: state.count + 1 };
case "DEC": return { ...state, count: state.count - 1 };
case "SET": return { ...state, count: action.payload };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0, error: null });
dispatch({ type: "INC" });
dispatch({ type: "SET", payload: 10 });useContext
Access context values
// Define
interface ThemeCtx { theme: "light" | "dark"; toggle: () => void }
const ThemeContext = createContext<ThemeCtx | null>(null);
// Custom hook (recommended)
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be in ThemeProvider");
return ctx;
}
// Provider
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<"light" | "dark">("light");
const toggle = useCallback(() =>
setTheme(t => t === "light" ? "dark" : "light"), []);
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
// Consume
const { theme, toggle } = useTheme();useLayoutEffect
Sync effect before browser paint
// Measure DOM before paint
useLayoutEffect(() => {
const rect = ref.current?.getBoundingClientRect();
setSize({ width: rect?.width, height: rect?.height });
}, []);
// Prevent flash
useLayoutEffect(() => {
if (shouldScroll) window.scrollTo(0, 0);
}, [shouldScroll]);useImperativeHandle
Customize ref exposed to parent
interface Handle { focus: () => void; clear: () => void }
const Input = forwardRef<Handle, Props>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => { if (inputRef.current) inputRef.current.value = ""; },
}), []);
return <input ref={inputRef} {...props} />;
});
// Parent
const ref = useRef<Handle>(null);
ref.current?.focus();useId
Generate unique IDs
function Field({ label }: { label: string }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}
// Multiple IDs
const id = useId();
const nameId = `${id}-name`;
const emailId = `${id}-email`;useTransition
Non-blocking state updates
const [isPending, startTransition] = useTransition();
function handleSearch(value: string) {
setQuery(value); // High priority
startTransition(() => {
setResults(filterItems(value)); // Low priority
});
}
return (
<>
<input onChange={e => handleSearch(e.target.value)} />
{isPending && <Spinner />}
</>
);useDeferredValue
Defer re-rendering of a value
function Results({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const results = useMemo(() => search(deferredQuery), [deferredQuery]);
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
{results.map(item => <Item key={item.id} {...item} />)}
</div>
);
}useSyncExternalStore
Subscribe to external stores
function useWindowWidth() {
return useSyncExternalStore(
(cb) => {
window.addEventListener("resize", cb);
return () => window.removeEventListener("resize", cb);
},
() => window.innerWidth, // Client
() => 1024 // Server
);
}
function useOnlineStatus() {
return useSyncExternalStore(
(cb) => {
window.addEventListener("online", cb);
window.addEventListener("offline", cb);
return () => {
window.removeEventListener("online", cb);
window.removeEventListener("offline", cb);
};
},
() => navigator.onLine,
() => true
);
}Custom Hooks
useLocalStorage
Persist state to localStorage
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === "undefined") return initial;
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// Usage
const [theme, setTheme] = useLocalStorage("theme", "dark");useDebounce
Debounce rapidly changing values
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 [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 300);
// Type "hello" → after 300ms: "hello"useThrottle
Limit update frequency
function useThrottle<T>(value: T, limit: number): T {
const [throttled, setThrottled] = useState(value);
const lastRan = useRef(Date.now());
useEffect(() => {
const handler = setTimeout(() => {
if (Date.now() - lastRan.current >= limit) {
setThrottled(value);
lastRan.current = Date.now();
}
}, limit - (Date.now() - lastRan.current));
return () => clearTimeout(handler);
}, [value, limit]);
return throttled;
}
// Usage
const throttledY = useThrottle(scrollY, 100);usePrevious
Track previous value
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => { ref.current = value; }, [value]);
return ref.current;
}
// Usage
const prev = usePrevious(count);
// count: 5, prev: 4useToggle
Boolean toggle
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
// Usage
const { value: isOpen, toggle, setFalse: close } = useToggle();useOnClickOutside
Detect clicks outside element
function useOnClickOutside<T extends HTMLElement>(
ref: RefObject<T>,
handler: () => void
) {
useEffect(() => {
const listener = (e: MouseEvent | TouchEvent) => {
if (!ref.current?.contains(e.target as Node)) handler();
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
}
// Usage
const ref = useRef<HTMLDivElement>(null);
useOnClickOutside(ref, () => setOpen(false));useMediaQuery
Responsive breakpoints
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
media.addEventListener("change", listener);
return () => media.removeEventListener("change", listener);
}, [query]);
return matches;
}
// Usage
const isMobile = useMediaQuery("(max-width: 768px)");
const isDark = useMediaQuery("(prefers-color-scheme: dark)");useWindowSize
Track window dimensions
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const update = () => setSize({
width: window.innerWidth,
height: window.innerHeight
});
update();
window.addEventListener("resize", update);
return () => window.removeEventListener("resize", update);
}, []);
return size;
}
// Usage
const { width, height } = useWindowSize();useIntersectionObserver
Detect element visibility
function useIntersectionObserver(
ref: RefObject<Element>,
options?: IntersectionObserverInit
): boolean {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(
([entry]) => setIsVisible(entry.isIntersecting),
options
);
observer.observe(ref.current);
return () => observer.disconnect();
}, [ref, options]);
return isVisible;
}
// Usage
const ref = useRef<HTMLDivElement>(null);
const isVisible = useIntersectionObserver(ref, { threshold: 0.5 });useFetch
Data fetching with state
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();
setLoading(true);
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(json => { setData(json); setError(null); })
.catch(e => { if (e.name !== "AbortError") setError(e); })
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage
const { data, loading, error } = useFetch<User[]>("/api/users");useEventListener
Declarative event listeners
function useEventListener<K extends keyof WindowEventMap>(
event: K,
handler: (e: WindowEventMap[K]) => void,
element: Window | HTMLElement = window
) {
const savedHandler = useRef(handler);
useEffect(() => { savedHandler.current = handler; }, [handler]);
useEffect(() => {
const listener = (e: Event) => savedHandler.current(e as WindowEventMap[K]);
element.addEventListener(event, listener);
return () => element.removeEventListener(event, listener);
}, [event, element]);
}
// Usage
useEventListener("keydown", e => {
if (e.key === "Escape") close();
});useCopyToClipboard
Copy text to clipboard
function useCopyToClipboard() {
const [copied, setCopied] = useState<string | null>(null);
const copy = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(text);
return true;
} catch {
setCopied(null);
return false;
}
}, []);
return { copied, copy };
}
// Usage
const { copied, copy } = useCopyToClipboard();
await copy("Hello!");useCounter
Counter with bounds
function useCounter(initial = 0, { min = -Infinity, max = Infinity } = {}) {
const [count, setCount] = useState(initial);
const inc = useCallback(() => setCount(c => Math.min(c + 1, max)), [max]);
const dec = useCallback(() => setCount(c => Math.max(c - 1, min)), [min]);
const reset = useCallback(() => setCount(initial), [initial]);
const set = useCallback((v: number) =>
setCount(Math.min(Math.max(v, min), max)), [min, max]);
return { count, inc, dec, reset, set };
}
// Usage
const { count, inc, dec } = useCounter(0, { min: 0, max: 10 });useHover
Track hover state
function useHover<T extends HTMLElement>(): [RefObject<T>, boolean] {
const [hovered, setHovered] = useState(false);
const ref = useRef<T>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const enter = () => setHovered(true);
const leave = () => setHovered(false);
el.addEventListener("mouseenter", enter);
el.addEventListener("mouseleave", leave);
return () => {
el.removeEventListener("mouseenter", enter);
el.removeEventListener("mouseleave", leave);
};
}, []);
return [ref, hovered];
}
// Usage
const [ref, isHovered] = useHover<HTMLDivElement>();useAsync
Execute async with state
type Status = "idle" | "pending" | "success" | "error";
function useAsync<T>(fn: () => Promise<T>) {
const [status, setStatus] = useState<Status>("idle");
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const execute = useCallback(async () => {
setStatus("pending");
try {
const result = await fn();
setData(result);
setStatus("success");
return result;
} catch (e) {
setError(e as Error);
setStatus("error");
throw e;
}
}, [fn]);
return { status, data, error, execute };
}
// Usage
const { status, data, execute } = useAsync(fetchUsers);useInterval
Declarative setInterval
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
// Usage
useInterval(() => setCount(c => c + 1), 1000);
useInterval(tick, isRunning ? 1000 : null); // null to pauseuseTimeout
Declarative setTimeout
function useTimeout(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setTimeout(() => savedCallback.current(), delay);
return () => clearTimeout(id);
}, [delay]);
}
// Usage
useTimeout(() => setVisible(false), 3000);Patterns
Compound Components
Flexible composition with shared state
const TabsContext = createContext<{
active: string;
setActive: (id: string) => void;
} | null>(null);
function Tabs({ children, defaultValue }: { children: ReactNode; defaultValue: string }) {
const [active, setActive] = useState(defaultValue);
return (
<TabsContext.Provider value={{ active, setActive }}>
{children}
</TabsContext.Provider>
);
}
function TabList({ children }: { children: ReactNode }) {
return <div role="tablist">{children}</div>;
}
function Tab({ id, children }: { id: string; children: ReactNode }) {
const ctx = useContext(TabsContext)!;
return (
<button
role="tab"
aria-selected={ctx.active === id}
onClick={() => ctx.setActive(id)}
>
{children}
</button>
);
}
function TabPanel({ id, children }: { id: string; children: ReactNode }) {
const ctx = useContext(TabsContext)!;
if (ctx.active !== id) return null;
return <div role="tabpanel">{children}</div>;
}
// Usage
<Tabs defaultValue="tab1">
<TabList>
<Tab id="tab1">Tab 1</Tab>
<Tab id="tab2">Tab 2</Tab>
</TabList>
<TabPanel id="tab1">Content 1</TabPanel>
<TabPanel id="tab2">Content 2</TabPanel>
</Tabs>Render Props
Share code via function prop
function MouseTracker({ render }: {
render: (pos: { x: number; y: number }) => ReactNode
}) {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", handler);
return () => window.removeEventListener("mousemove", handler);
}, []);
return <>{render(pos)}</>;
}
// Usage
<MouseTracker render={({ x, y }) => <p>Mouse: {x}, {y}</p>} />Higher-Order Component
Wrap component with additional logic
function withAuth<P extends object>(Component: ComponentType<P>) {
return function WithAuthComponent(props: P) {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
if (!user) return <Redirect to="/login" />;
return <Component {...props} />;
};
}
// Usage
const ProtectedPage = withAuth(Dashboard);Controlled vs Uncontrolled
Support both modes
interface InputProps {
value?: string; // Controlled
defaultValue?: string; // Uncontrolled
onChange?: (value: string) => void;
}
function Input({ value, defaultValue, onChange }: InputProps) {
const [internal, setInternal] = useState(defaultValue ?? "");
const isControlled = value !== undefined;
const currentValue = isControlled ? value : internal;
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!isControlled) setInternal(e.target.value);
onChange?.(e.target.value);
};
return <input value={currentValue} onChange={handleChange} />;
}
// Controlled
<Input value={text} onChange={setText} />
// Uncontrolled
<Input defaultValue="initial" onChange={console.log} />forwardRef
Forward ref to child element
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary";
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = "primary", className, ...props }, ref) => {
return (
<button
ref={ref}
className={`btn btn-${variant} ${className}`}
{...props}
/>
);
}
);
Button.displayName = "Button";
// Usage
const buttonRef = useRef<HTMLButtonElement>(null);
<Button ref={buttonRef} variant="primary">Click</Button>Error Boundary
Catch rendering errors
class ErrorBoundary extends Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("Error:", error, info);
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<p>Something went wrong</p>}>
<MyComponent />
</ErrorBoundary>Suspense & lazy
Code splitting and loading states
const LazyComponent = lazy(() => import("./HeavyComponent"));
function App() {
return (
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
);
}
// Multiple lazy components
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>