Hooks
Hooks let you use state, side effects, refs, and other React features inside function components. React 19 adds the use hook for reading Promises and Context anywhere, including inside conditionals.
Core Hooks
useState - Local State
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
// Functional update when new state depends on old
setCount((prev) => prev + 1);
// Object state - always spread previous state
setUser((prev) => prev ? { ...prev, name: "New Name" } : prev);
Gotcha:
useStateusesObject.isfor comparison. Setting the same value (same reference for objects) won’t trigger a re-render. But creating a new object{}with the same contents will.
useEffect - Side Effects
useEffect(() => {
// Runs after render when `id` changes
const controller = new AbortController();
fetch(`/api/users/${id}`, { signal: controller.signal })
.then((res) => res.json())
.then(setUser);
// Cleanup runs before next effect or on unmount
return () => controller.abort();
}, [id]); // dependency array - empty = mount only, omit = every render
| Dependency array | When it runs |
|---|---|
[a, b] | After render if a or b changed |
[] | Once after initial render (mount) |
| Omitted | After every render |
Tip: In React 19 with the compiler, dependency arrays are validated at compile time. Missing deps become build errors, not subtle bugs.
useRef - Stable References
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current?.focus();
}
return <input ref={inputRef} />;
}
// Also useful for mutable values that don't trigger re-renders
const renderCount = useRef(0);
renderCount.current += 1; // no re-render
useMemo and useCallback - Memoization
// Memoize expensive computation
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// Memoize callback to prevent child re-renders
const handleClick = useCallback(
(id: string) => setSelected(id),
[] // no deps - setSelected is stable
);
Tip: With React Compiler enabled, you can delete
useMemoanduseCallbackcalls. The compiler inserts memoization automatically. See React Compiler.
useReducer - Complex State Logic
type State = { items: Item[]; loading: boolean; error: string | null };
type Action =
| { type: "FETCH_START" }
| { type: "FETCH_SUCCESS"; items: Item[] }
| { type: "FETCH_ERROR"; error: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "FETCH_START":
return { ...state, loading: true, error: null };
case "FETCH_SUCCESS":
return { items: action.items, loading: false, error: null };
case "FETCH_ERROR":
return { ...state, loading: false, error: action.error };
}
}
const [state, dispatch] = useReducer(reducer, {
items: [],
loading: false,
error: null,
});
dispatch({ type: "FETCH_START" });
The use Hook (React 19)
use is special. It reads the current value of a Promise or Context. Unlike other hooks, it works inside conditionals and loops.
Reading Promises
import { use, Suspense } from "react";
// The promise must be created OUTSIDE the component
// (in a parent, a loader, or a cache)
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // suspends until resolved
return <h1>{user.name}</h1>;
}
// Parent wraps in Suspense
function Page() {
const userPromise = fetchUser(userId); // start fetching
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
Gotcha: Don’t create the Promise inside the component that calls
use. The component re-renders on suspend, which would create a new Promise, causing an infinite loop. Create it in a parent or cache it.
Reading Context (Conditional)
function StatusBadge({ showTheme }: { showTheme: boolean }) {
// This was impossible with useContext - hooks can't be in conditionals
// use() can be called conditionally
if (showTheme) {
const theme = use(ThemeContext);
return <span style={{ color: theme.primary }}>Active</span>;
}
return <span>Active</span>;
}
use vs useEffect for Data Fetching
| Approach | When to use |
|---|---|
use(promise) + Suspense | Fetching during render, data needed to display the component |
useEffect | Side effects that aren’t about fetching display data (analytics, subscriptions) |
| Server Components | Best option when you can fetch on the server |
| TanStack Query | Complex client-side caching, pagination, optimistic updates |
Custom Hooks
Extract reusable logic into custom hooks. Any function starting with use is a hook.
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
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");
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 - only updates after 300ms of no typing
const debouncedSearch = useDebounce(searchTerm, 300);
Rules of Hooks
- Only call hooks at the top level - not inside loops, conditions, or nested functions (except
use, which can be conditional) - Only call hooks from React functions - components or other custom hooks, not regular functions
- Name custom hooks with
useprefix - this lets React enforce the rules and lets the compiler optimize
// Bad - hook inside condition
function Bad({ loggedIn }: { loggedIn: boolean }) {
if (loggedIn) {
const [name, setName] = useState(""); // breaks rules
}
}
// Good - always call the hook, conditionally use the value
function Good({ loggedIn }: { loggedIn: boolean }) {
const [name, setName] = useState("");
if (!loggedIn) return <Login />;
return <span>{name}</span>;
}
// Exception: use() CAN be called conditionally
function AlsoGood({ loggedIn }: { loggedIn: boolean }) {
if (loggedIn) {
const user = use(userPromise); // this is fine
return <span>{user.name}</span>;
}
return <Login />;
}
Tip: The
eslint-plugin-react-hooksplugin (included in all React setups) catches rule violations at lint time. Don’t disable it.
Next: Server Components for the full server/client architecture