);
}
```
### usePrevious
```typescript
import { useRef, useEffect } from 'react';
function usePrevious(value: T): T | undefined {
const ref = useRef(undefined);
// Runs after render — ref holds value from previous render
useEffect(() => {
ref.current = value;
}, [value]);
// Returns value from before this render
return ref.current;
}
// Usage: animate on value change
function AnimatedCounter({ count }: { count: number }) {
const prevCount = usePrevious(count);
const direction = prevCount !== undefined && count > prevCount ? 'up' : 'down';
return (
{count}
);
}
```
### useEventListener
```typescript
import { useEffect, useRef } from 'react';
function useEventListener(
eventType: K,
handler: (event: WindowEventMap[K]) => void,
element: EventTarget = window
): void {
// Use ref so handler changes don't cause re-subscription
const handlerRef = useRef(handler);
useEffect(() => { handlerRef.current = handler; });
useEffect(() => {
const listener = (event: Event) =>
handlerRef.current(event as WindowEventMap[K]);
element.addEventListener(eventType, listener);
return () => element.removeEventListener(eventType, listener);
}, [eventType, element]);
}
// Usage
function KeyboardShortcut() {
useEventListener('keydown', event => {
if (event.key === 'Escape') closeModal();
if ((event.metaKey || event.ctrlKey) && event.key === 'k') openSearch();
});
}
```
---
## Hook Composition
Build complex hooks by composing simpler ones. Each hook should do one thing well.
```typescript
// Compose useFetch + useDebounce for a search hook
function useSearch(endpoint: string) {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
// Only fetch when query is non-empty
const url = debouncedQuery ? `${endpoint}?q=${encodeURIComponent(debouncedQuery)}` : null;
const { data, isLoading, error } = useFetch(url ?? '');
return {
query,
setQuery,
results: data ?? [],
isLoading: isLoading && !!debouncedQuery,
error,
};
}
// Compose local storage + media query for responsive theme
function useTheme() {
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
const [savedTheme, setSavedTheme] = useLocalStorage<'light' | 'dark' | 'system'>(
'theme',
'system'
);
const resolvedTheme: 'light' | 'dark' =
savedTheme === 'system' ? (prefersDark ? 'dark' : 'light') : savedTheme;
return { theme: resolvedTheme, savedTheme, setTheme: setSavedTheme };
}
```
---
## Rules of Hooks
Only call hooks at the top level of a React function component or another custom hook. Never inside conditions, loops, or nested functions.
```typescript
// VIOLATION: conditional hook call
function BadComponent({ isLoggedIn }: { isLoggedIn: boolean }) {
if (isLoggedIn) {
const user = useUser(); // ERROR: conditional
}
}
// FIX: always call hooks, conditionally use their values
function GoodComponent({ isLoggedIn }: { isLoggedIn: boolean }) {
const user = useUser();
if (!isLoggedIn) return null;
return
{user.name}
;
}
// VIOLATION: hook in a loop
function BadList({ ids }: { ids: string[] }) {
return ids.map(id => {
const data = useFetch(`/api/${id}`); // ERROR: in loop
return ;
});
}
// FIX: move hook logic into a child component
function GoodList({ ids }: { ids: string[] }) {
return ids.map(id => );
}
function ListItem({ id }: { id: string }) {
const data = useFetch(`/api/${id}`); // CORRECT: top level
return ;
}
```
---
## React 19 Hooks
### use() — Promises and Context
```typescript
import { use, Suspense } from 'react';
// Await a promise directly in render (must be wrapped in Suspense)
async function fetchUser(id: string): Promise {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
function UserCard({ userPromise }: { userPromise: Promise }) {
// Suspends until promise resolves; throws on rejection (ErrorBoundary handles it)
const user = use(userPromise);
return
{user.name}
;
}
function Page({ id }: { id: string }) {
const userPromise = fetchUser(id); // start fetch, pass promise down
return (
}>
);
}
// use() can also read context conditionally (unlike useContext)
function ConditionalTheme({ showLabel }: { showLabel: boolean }) {
if (!showLabel) return null;
const theme = use(ThemeContext); // conditional — allowed with use()
return Label;
}
```
### useFormStatus
```typescript
import { useFormStatus } from 'react-dom';
// Must be used inside a
);
}
```
### useOptimistic
```typescript
import { useOptimistic, useTransition } from 'react';
interface Message {
id: string;
text: string;
sending?: boolean;
}
function MessageList({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
// Reducer: how to merge optimistic update into current state
(currentMessages, newMessage: Message) => [
...currentMessages,
{ ...newMessage, sending: true },
]
);
async function sendMessage(formData: FormData) {
const text = formData.get('text') as string;
const tempMessage = { id: crypto.randomUUID(), text };
// Update UI immediately
addOptimisticMessage(tempMessage);
// Send to server (optimistic update reverts on error)
await saveMessage(text);
}
return (
<>
{optimisticMessages.map(msg => (
{msg.text}
))}
>
);
}
```
---
## Anti-patterns
| Anti-pattern | Problem | Fix |
|--------------|---------|-----|
| `useEffect` with no dep array syncing props to state | Runs every render | Compute derived value during render |
| Calling hooks from event handlers | Violates rules of hooks | Move hook to component top level |
| `useState` for server data | Manual loading/error state, stale data | Use TanStack Query |
| Large single `useEffect` doing multiple things | Hard to reason about, wrong deps | Split into separate `useEffect` calls per concern |
| `useCallback` on everything | Adds overhead, no benefit without memoized children | Only when callback is a dep or passed to `memo` component |
| Forgetting cleanup | Memory leaks, stale updates on unmounted component | Always return cleanup from `useEffect` |