useContext, useRef, useMemo, useCallback
Advanced hooks for sharing state globally (useContext), referencing DOM nodes (useRef), caching values (useMemo), and caching functions (useCallback).
These solve specific performance and architecture problems: avoid prop drilling, access DOM directly, prevent unnecessary re-calculations and re-renders.
useContext: global state like auth/theme. useRef: DOM access, storing values without re-render. useMemo: expensive calculations. useCallback: stable function references for child components.
Each hook has a specific signature. useContext reads from a Provider. useRef returns a mutable .current. useMemo/useCallback take a function and dependency array.
Code Examples
#1useContext — avoid prop drilling
"use client";import { createContext, useContext, useState } from "react";// 1. Create the contextinterface AuthContextType {user: User | null;login: (email: string, password: string) => Promise<void>;logout: () => void;}const AuthContext = createContext<AuthContextType | null>(null);// 2. Create provider componentexport function AuthProvider({ children }: { children: React.ReactNode }) {const [user, setUser] = useState<User | null>(null);const login = async (email: string, password: string) => {const data = await fetch("/api/auth/login", {method: "POST",body: JSON.stringify({ email, password }),}).then(r => r.json());setUser(data.user);};const logout = () => setUser(null);return (<AuthContext.Provider value={{ user, login, logout }}>{children}</AuthContext.Provider>);}// 3. Custom hook for easy consumptionexport function useAuth() {const context = useContext(AuthContext);if (!context) throw new Error("useAuth must be inside AuthProvider");return context;}// 4. Use anywhere in the tree — no prop drilling!function Header() {const { user, logout } = useAuth();return (<header>{user ? (<><span>Welcome, {user.name}</span><button onClick={logout}>Logout</button></>) : (<a href="/login">Login</a>)}</header>);}
#2useRef, useMemo, useCallback
"use client";import { useRef, useMemo, useCallback, useState } from "react";// useRef — 1. DOM accessfunction SearchInput() {const inputRef = useRef<HTMLInputElement>(null);const focusInput = () => {inputRef.current?.focus(); // directly access DOM element};return (<><input ref={inputRef} type="text" placeholder="Search..." /><button onClick={focusInput}>Focus Search</button></>);}// useRef — 2. Store value without triggering re-renderfunction Timer() {const intervalRef = useRef<NodeJS.Timeout | null>(null);const start = () => {intervalRef.current = setInterval(() => console.log("tick"), 1000);};const stop = () => {if (intervalRef.current) clearInterval(intervalRef.current);};return <><button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;}// useMemo — cache expensive calculationfunction ProductList({ products, category }: Props) {// Only recalculates when products or category changesconst filteredProducts = useMemo(() => {console.log("Filtering..."); // watch when this runsreturn products.filter(p => p.category === category).sort((a, b) => a.price - b.price);}, [products, category]); // dependenciesreturn <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>;}// useCallback — stable function referencefunction Parent() {const [count, setCount] = useState(0);// Without useCallback: new function created on every render// ChildComponent would re-render every time Parent re-renders// With useCallback: same function reference between rendersconst handleItemClick = useCallback((id: number) => {console.log("Clicked:", id);}, []); // no dependencies = created oncereturn (<><button onClick={() => setCount(c => c + 1)}>Count: {count}</button><ChildComponent onItemClick={handleItemClick} /></>);}
Practice This Concept
Open the playground, paste any example, and experiment. The best way to learn is by breaking things.
Open in Playground