React — Pitfalls¶
OWNER: floris, floor ALSO_USED_BY: alexander, tobias LAST_VERIFIED: 2026-03-26 GE_STACK_VERSION: React 19.x
Overview¶
Known failure modes in React development. Every entry is a real mistake that has cost debugging time. Format: ANTI_PATTERN with FIX — agents match against these during code review.
1. Stale Closures¶
1.1 Stale State in Callbacks¶
ANTI_PATTERN: reading state directly inside a callback registered once
// BUG: count is always 0 in the interval
useEffect(() => {
const id = setInterval(() => {
console.log(count); // stale — captures initial value
setCount(count + 1); // always sets to 1
}, 1000);
return () => clearInterval(id);
}, []); // empty deps = closure never refreshes
FIX: use functional state update
useEffect(() => {
const id = setInterval(() => {
setCount((prev) => prev + 1); // always reads latest
}, 1000);
return () => clearInterval(id);
}, []);
1.2 Stale Props in Effects¶
ANTI_PATTERN: using a prop in an effect without it in the dependency array
useEffect(() => {
fetchData(userId); // stale if userId changes
}, []); // missing userId dependency
FIX: include all used values in the dependency array
1.3 Stale Closure in Event Handlers¶
ANTI_PATTERN: event handler captures stale value from render scope
const handleClick = () => {
setTimeout(() => {
alert(count); // shows value at click time, not current
}, 3000);
};
FIX: use a ref for latest value when needed in delayed callbacks
const countRef = useRef(count);
countRef.current = count;
const handleClick = () => {
setTimeout(() => {
alert(countRef.current); // always current
}, 3000);
};
2. Infinite Re-renders¶
2.1 Object/Array in Dependency Array¶
ANTI_PATTERN: inline object or array as useEffect dependency
// BUG: new object every render → effect runs every render
useEffect(() => {
fetchData(filters);
}, [{ status: "active", page: 1 }]); // new ref each render
FIX: memoize or extract to stable reference
const filters = useMemo(() => ({ status: "active", page: 1 }), []);
useEffect(() => {
fetchData(filters);
}, [filters]);
2.2 State Update Inside Render¶
ANTI_PATTERN: calling setState during render without condition
function Component({ items }: Props) {
const [filtered, setFiltered] = useState(items);
setFiltered(items.filter(i => i.active)); // BUG: infinite loop
return <List items={filtered} />;
}
FIX: compute derived values during render — no state needed
function Component({ items }: Props) {
const filtered = items.filter(i => i.active); // derived, no state
return <List items={filtered} />;
}
2.3 useEffect That Updates Its Own Dependency¶
ANTI_PATTERN: effect updates state that is in its own dependency array
FIX: compute during render or use functional update
// Option A: derive during render
const transformedData = useMemo(() => transform(data), [data]);
// Option B: if you must use effect, guard against redundant updates
useEffect(() => {
const result = transform(rawData);
setData((prev) => {
if (JSON.stringify(prev) === JSON.stringify(result)) return prev;
return result;
});
}, [rawData]);
2.4 Missing Function Stability¶
ANTI_PATTERN: passing inline function as prop to memoized child
// Parent re-renders → new function ref → MemoChild re-renders
<MemoChild onSelect={(id) => handleSelect(id)} />
FIX: stabilize with useCallback
const onSelect = useCallback((id: string) => handleSelect(id), [handleSelect]);
<MemoChild onSelect={onSelect} />
3. useEffect Traps¶
3.1 Data Fetching in useEffect¶
ANTI_PATTERN: fetching data with useEffect + useState
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("/api/data")
.then((res) => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);
FIX: use TanStack Query — handles caching, race conditions, dedup, refetch
const { data, isLoading, error } = useQuery({
queryKey: ["data"],
queryFn: () => fetch("/api/data").then((res) => res.json()),
});
3.2 Missing Cleanup¶
ANTI_PATTERN: subscribing in useEffect without cleanup
useEffect(() => {
window.addEventListener("resize", handleResize);
// BUG: never unsubscribed → memory leak
}, []);
FIX: always return a cleanup function
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
3.3 Race Condition on Fetch¶
ANTI_PATTERN: no abort on rapid dependency changes
useEffect(() => {
fetch(`/api/user/${userId}`)
.then((res) => res.json())
.then(setUser); // BUG: stale response overwrites fresh one
}, [userId]);
FIX: use AbortController (or TanStack Query which handles this)
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user/${userId}`, { signal: controller.signal })
.then((res) => res.json())
.then(setUser)
.catch((err) => {
if (err.name !== "AbortError") throw err;
});
return () => controller.abort();
}, [userId]);
3.4 Syncing Props to State¶
ANTI_PATTERN: useEffect to copy props into state
const [localValue, setLocalValue] = useState(prop);
useEffect(() => {
setLocalValue(prop); // unnecessary sync — extra render
}, [prop]);
FIX: use the prop directly, or use key to reset
// Option A: use prop directly (derived state)
const displayValue = format(prop);
// Option B: reset component with key when prop changes
<Editor key={documentId} initialContent={content} />
4. Memo Misuse¶
4.1 Memoizing Cheap Operations¶
ANTI_PATTERN: useMemo for trivial computations
const count = useMemo(() => items.length, [items]); // pointless
const label = useMemo(() => `${name} (${role})`, [name, role]); // pointless
FIX: compute directly — memo overhead exceeds the computation cost
4.2 React.memo Without Stable Props¶
ANTI_PATTERN: memoizing a component but passing unstable props
const MemoList = React.memo(ExpensiveList);
// Parent still creates new object every render
<MemoList config={{ sort: "asc", limit: 10 }} />
FIX: stabilize props with useMemo or extract to constant
4.3 Memo With React Compiler¶
ANTI_PATTERN: adding manual useMemo/useCallback when React Compiler is active FIX: React Compiler handles memoization automatically — remove manual memo IF: profiling proves the compiler missed a specific case THEN: add manual memo with a comment explaining why
5. Key Prop Mistakes¶
5.1 Using Index as Key¶
ANTI_PATTERN: using array index as key for dynamic lists
{items.map((item, index) => (
<Item key={index} data={item} /> // BUG: reorder/delete causes wrong state
))}
FIX: use a stable unique identifier
5.2 Missing Key on Fragments¶
ANTI_PATTERN: mapping to fragments without key
{pairs.map((pair) => (
<> {/* BUG: missing key */}
<dt>{pair.label}</dt>
<dd>{pair.value}</dd>
</>
))}
FIX: use <Fragment> with explicit key
import { Fragment } from "react";
{pairs.map((pair) => (
<Fragment key={pair.id}>
<dt>{pair.label}</dt>
<dd>{pair.value}</dd>
</Fragment>
))}
5.3 Not Using Key to Reset Component State¶
ANTI_PATTERN: useEffect to reset local state when an ID changes
FIX: change the key prop — React unmounts and remounts fresh
6. Context Re-render Cascades¶
6.1 Monolithic Context¶
ANTI_PATTERN: single context with many unrelated fields
const AppContext = createContext({
theme: "light",
user: null,
sidebarOpen: false,
notifications: [],
locale: "en",
});
// Every consumer re-renders when ANY field changes
FIX: split into focused contexts
const ThemeContext = createContext<Theme>("light");
const UserContext = createContext<User | null>(null);
const SidebarContext = createContext<SidebarState>(defaultSidebar);
6.2 Unstable Context Value¶
ANTI_PATTERN: creating new object in provider render
<AuthContext.Provider value={{ user, login, logout }}>
{/* New object every render → all consumers re-render */}
</AuthContext.Provider>
FIX: memoize the context value
const value = useMemo(() => ({ user, login, logout }), [user, login, logout]);
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
6.3 Context for Frequently Changing Values¶
ANTI_PATTERN: context for values that change on every frame (mouse position, scroll) FIX: use a ref + subscription pattern or zustand with selectors
7. Server Component Pitfalls¶
7.1 Importing Client Code in Server Components¶
ANTI_PATTERN: importing a client-only library in a Server Component FIX: move the import to a "use client" component
7.2 Passing Non-Serializable Props Across Boundary¶
ANTI_PATTERN: passing functions, Dates, Maps from Server to Client Component FIX: serialize to plain objects — pass ISO strings, arrays, plain objects
7.3 "use client" Too High in the Tree¶
ANTI_PATTERN: "use client" on a layout or page — makes entire subtree client FIX: push "use client" to the smallest leaf that needs interactivity
8. Zod v4 Pitfall (GE-Specific)¶
ANTI_PATTERN: accessing .errors on a ZodError object
const result = schema.safeParse(data);
if (!result.success) {
console.log(result.error.errors); // UNDEFINED in Zod v4
}
FIX: use .issues in Zod v4
Cross-References¶
READ_ALSO: wiki/docs/stack/react/index.md READ_ALSO: wiki/docs/stack/react/patterns.md READ_ALSO: wiki/docs/stack/react/state-management.md READ_ALSO: wiki/docs/stack/react/forms.md READ_ALSO: wiki/docs/stack/react/checklist.md