| name | react |
| description | React hooks + performance patterns. Use for useEffect, useMemo, useCallback, memo, rerender, derived state, performance, bundle, optimize, hooks, state, useTransition, Promise.all, waterfall, async |
React Fundamentals
Core Philosophy
Effects are escape hatches. They synchronize React with external systems. If no external system is involved, you likely don't need one.
Performance optimization is targeted. Don't memoize everything. Profile first, optimize measurable bottlenecks.
useEffect Decision Tree
Do I need useEffect?
Is there an external system involved?
├── No → Don't use useEffect
│ ├── Derived state? → Calculate during render
│ ├── Event response? → Handle in event handler
│ └── Data fetching? → Use TanStack Query
└── Yes → Maybe use useEffect
├── Browser APIs (focus, scroll, localStorage)
├── Third-party widgets (maps, charts)
├── Network connections (WebSockets)
└── Analytics/logging
Derived State (No Effect Needed)
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
const fullName = `${firstName} ${lastName}`;
const sortedItems = useMemo(
() => items.toSorted((a, b) => a.name.localeCompare(b.name)),
[items],
);
Event Handlers (No Effect Needed)
useEffect(() => {
if (submitted) {
navigate('/success');
}
}, [submitted]);
const handleSubmit = async () => {
await submitForm();
navigate('/success');
};
Performance Patterns
useMemo - Expensive Calculations Only
const doubled = useMemo(() => value * 2, [value]);
const sortedItems = useMemo(
() => items.toSorted((a, b) => a.price - b.price),
[items],
);
const filterFn = useMemo(() => (item) => item.active, []);
useCallback - For Memoized Children Only
const handleClick = useCallback(() => doSomething(), []);
return <button onClick={handleClick}>Click</button>;
const handleClick = useCallback(() => doSomething(id), [id]);
return <MemoizedChild onClick={handleClick} />;
Avoiding Re-renders
<Child style={{ color: 'red' }} />;
const style = useMemo(() => ({ color: 'red' }), []);
<Child style={style} />;
const style = { color: 'red' };
function Parent() {
return <Child style={style} />;
}
Async Patterns
Parallel Fetches
const user = await getUser(id);
const posts = await getPosts(id);
const comments = await getComments(id);
const [user, posts, comments] = await Promise.all([
getUser(id),
getPosts(id),
getComments(id),
]);
useTransition for Non-Urgent Updates
const [isPending, startTransition] = useTransition();
const handleSearch = (query: string) => {
setQuery(query);
startTransition(() => {
setFilteredResults(filterItems(query));
});
};
Lazy State Initialization
const [items, setItems] = useState(parseExpensiveData(raw));
const [items, setItems] = useState(() => parseExpensiveData(raw));
Set/Map for O(1) Lookups
const isSelected = (id: string) => selectedIds.includes(id);
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
const isSelected = (id: string) => selectedSet.has(id);
Common Mistakes
| Mistake | Fix |
|---|
| Derived state in useEffect | Calculate during render |
| Effect chains (A→B→C) | Derive all values directly |
| useMemo for simple values | Only memoize expensive computations |
| useCallback everywhere | Only for memoized child components |
| Object literals as props | Define outside component or useMemo |
| Manual data fetching | Use TanStack Query |
| Sequential awaits | Use Promise.all for parallel ops |
| Array.includes in loops | Use Set with useMemo for O(1) |
| useState(expensiveInit()) | Use useState(() => expensiveInit()) |
Delegation
- Data fetching: For queries and caching, see tanstack-query skill
- Form state: For form handling, see tanstack-form skill
- URL state: For routing and params, see tanstack-router skill
- Error handling: For error boundaries, see error-boundaries skill
- Full-stack flows: For integrated patterns, see integration-patterns skill
- Code review: After optimizing components, delegate to
code-reviewer agent