Five React Performance Patterns That Are Now Anti-Patterns in 2026
Open any AI-assisted React project, and you'll likely see the same picture: useMemo, useCallback, and React.memo everywhere, as if the code would crash without them.
But it's 2026, and the React Compiler has officially landed. Many of those manual "optimizations" you wrote are not only useless but are actually slowing down your code.
The Bottom Line First: Why You Should Change
The React Compiler (officially released with React 19) does one thing: it automatically analyzes components at compile time and performs fine-grained memoization at the expression level.
This means the useMemo, useCallback, and React.memo you write by hand can all be done automatically by the compiler, and it does so more finely and accurately.
The problems with manual optimization:
| Dimension | Manual Optimization | React Compiler |
|---|---|---|
| Granularity | Component/Hook level | Expression level (finer) |
| Accuracy | Dependency arrays often wrong | Compiler analysis, no omissions |
| Code Volume | 30-50% more wrapper code | Zero extra code |
| Maintenance | Manually update when deps change | Automatic tracking |
| Error Rate | Wrong deps = cache never updates | This problem doesn't exist |
ESLint React v5.3 has already added no-unnecessary-use-memo and no-unnecessary-use-callback rules — the official toolchain is telling you to stop writing them.
Let's look at five "optimizations" that have become anti-patterns.
Anti-pattern 1: Wrapping All Calculations in useMemo
The most common AI-generated code pattern:
function UserList({ users, filter }) {
const filteredUsers = useMemo(() => {
return users.filter(u => u.name.includes(filter));
}, [users, filter]);
const sortedUsers = useMemo(() => {
return [...filteredUsers].sort((a, b) => a.name.localeCompare(b.name));
}, [filteredUsers]);
const stats = useMemo(() => ({
total: sortedUsers.length,
active: sortedUsers.filter(u => u.active).length,
}), [sortedUsers]);
return <div>{/* ... */}</div>;
}
Three useMemos doing one thing: filter + sort + stats.
The React Compiler automatically analyzes the dependency relationships in this code and skips all calculations when users or filter haven't changed. Writing three useMemos by hand is doing what the compiler already does, while also adding:
- Maintenance cost for three dependency arrays
- Memory overhead for three closures
- A 50% drop in code readability
The 2026 way to write it:
function UserList({ users, filter }) {
const sorted = users
.filter(u => u.name.includes(filter))
.sort((a, b) => a.name.localeCompare(b.name));
const stats = {
total: sorted.length,
active: sorted.filter(u => u.active).length,
};
return <div>{/* ... */}</div>;
}
Clean, direct, and automatically optimized by the compiler.
The only exception: If your calculation is genuinely heavy (like complex aggregation on 100,000 data points) and you can prove it's a bottleneck with React DevTools Profiler — then keep useMemo. But 99% of scenarios aren't like this.
Anti-pattern 2: Wrapping All Functions Passed to Child Components in useCallback
function Dashboard() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
const handleReset = useCallback(() => {
setCount(0);
}, []);
const handleExport = useCallback(() => {
exportData(count);
}, [count]);
return (
<>
<Button onClick={handleClick} />
<Button onClick={handleReset} />
<ExportButton onClick={handleExport} />
</>
);
}
This code has three problems:
- useCallback itself doesn't prevent child component re-renders — unless the child component uses
React.memo - Without React.memo, all useCallback does is provide a "stable reference" — but the React Compiler can already automatically determine which components need to be skipped
handleExport's dependency array includescount, so it gets recreated every time count changes — useCallback is doing nothing here
A more direct problem: AI tools love generating useCallback. Because the training data is full of "React Performance Optimization Best Practices" articles teaching "always use useCallback for functions passed to child components." This advice might have made sense in 2023; in 2026, it's noise.
The 2026 way to write it:
function Dashboard() {
const [count, setCount] = useState(0);
return (
<>
<Button onClick={() => setCount(c => c + 1)} />
<Button onClick={() => setCount(0)} />
<ExportButton onClick={() => exportData(count)} />
</>
);
}
Anti-pattern 3: Wrapping Every Exported Component in React.memo
const UserCard = React.memo(function UserCard({ user, onSelect }) {
return (
<div onClick={() => onSelect(user.id)}>
<Avatar src={user.avatar} />
<span>{user.name}</span>
</div>
);
});
React.memo's logic is: skip rendering if props haven't changed. The problems are:
- The React Compiler is already doing the same thing, and at a finer granularity — it can skip partial expressions inside a component without needing to skip the entire component
- React.memo performs a shallow comparison of props on every render — if props change frequently, this comparison itself is a waste
- It only makes sense when paired with useCallback — but if you're no longer writing useCallback, React.memo is also unnecessary
A good rule of thumb: If your component's rendering cost is lower than the cost of a props shallow comparison (which is true for most simple components), React.memo is a negative optimization.
The 2026 way to write it: Export directly, no wrapping.
function UserCard({ user, onSelect }) {
return (
<div onClick={() => onSelect(user.id)}>
<Avatar src={user.avatar} />
<span>{user.name}</span>
</div>
);
}
Anti-pattern 4: useEffect + useState for Data Fetching
This might be the most deep-rooted anti-pattern, and it's the pattern AI-generated code loves to write the most:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchUser(userId)
.then(data => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [userId]);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <div>{user.name}</div>;
}
25 lines of code doing one thing: fetching data. And it has these hidden dangers:
| Problem | Consequence |
|---|---|
| Race conditions | When userId changes rapidly, old requests overwrite new data |
| Waterfall requests | Parent fetches → child starts fetching → grandchild waits again |
| No caching | Same userId re-fetches on every mount |
| Initial render flash | Loading state always goes through true→false |
| SSR unfriendly | useEffect doesn't run during SSR |
The 2026 way to write it (using TanStack Query):
function UserProfile({ userId }) {
const { data: user, isPending, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
if (isPending) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <div>{user.name}</div>;
}
Or using React 19's use:
function UserProfile({ userPromise }) {
const user = use(userPromise);
return <div>{user.name}</div>;
}
From 25 lines to 3 lines. Automatically handles race conditions, caching, deduplication, and background refetching.
This isn't a style preference; it's an architectural gap. Using useEffect for data fetching has already been marked as not recommended in the official React documentation.
Anti-pattern 5: Over-Splitting Components to "Optimize Performance"
An extreme case I've seen: a form page was split into 47 components — one component for each input, one for each label, one for each validation message. The reason given was "to avoid the entire form re-rendering."
This is a misunderstanding of React's rendering mechanism.
Component splitting does not equal performance optimization. For every extra component, React adds:
- One more reconciliation comparison
- One more Fiber node
- One more round of props serialization and comparison
Reasonable splitting criteria should be:
| Scenario | Should Split | Should Not Split |
|---|---|---|
| Component exceeds 200 lines | ✅ Maintainability | - |
| Needs independent reuse | ✅ Reusability | - |
| Has independent state logic | ✅ Separation of concerns | - |
| "This part might update frequently" | ❌ Premature optimization | ✅ Let the Compiler handle it |
| "Splitting smaller improves performance" | ❌ False assumption | ✅ Verify with Profiler |
The real performance optimization to do is state co-location: put state in the component that actually needs it, rather than lifting it to a parent and then relying on splitting to avoid re-renders.
// ❌ State lifting + over-splitting
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<>
<NameInput value={name} onChange={setName} />
<EmailInput value={email} onChange={setEmail} />
</>
);
}
// ✅ State co-location, each manages its own
function Form() {
return (
<>
<NameInput />
<EmailInput />
</>
);
}
function NameInput() {
const [name, setName] = useState('');
return <input value={name} onChange={e => setName(e.target.value)} />;
}
Cheat Sheet: React Performance Optimization Decisions in 2026
| Previous "Best Practice" | 2026 Approach | When You Still Need the Old Way |
|---|---|---|
| useMemo wrapping calculations | Write directly, Compiler handles it | Complex aggregation on 100k+ data points, confirmed bottleneck by Profiler |
| useCallback wrapping functions | Inline directly | Passed to third-party libraries that depend on reference stability |
| React.memo wrapping components | Export directly | Extremely heavy rendering components (Canvas/3D), confirmed by Profiler |
| useEffect for data fetching | TanStack Query / use() / SWR | Never needed (no exceptions) |
| Over-splitting components | Split by logic + state co-location | Never needed (no exceptions) |
| Manual shouldComponentUpdate | Delete it | In 2026, there shouldn't be any Class components left |
The single criterion for judgment: Run React DevTools Profiler first. If there's no evidence it's a bottleneck, don't optimize it.
Minimum Steps to Upgrade to React Compiler
If your project hasn't enabled the React Compiler yet:
npm install react-compiler-runtime
npm install -D babel-plugin-react-compiler
Add one line to your Babel config:
{
"plugins": [
["babel-plugin-react-compiler", {}]
]
}
For Vite users:
// vite.config.js
import { reactCompiler } from 'react-compiler-runtime/vite';
export default {
plugins: [reactCompiler()],
};
After enabling, you can gradually delete unnecessary useMemo/useCallback/React.memo. You don't need to delete them all at once — the compiler and manual optimizations can coexist, it's just that the manual parts become redundant code.
Don't Let AI Make Decisions for You
This article is essentially saying one thing: Don't be afraid to delete useMemo just because AI generated it.
The training data for AI code generators is full of "best practice" articles from 2020-2024. Those articles were correct at the time — React had no compiler, and manual memoization was the only option. But it's 2026 now, the tools have changed, and the practices must change too.
Using AI to write code is fine. But when reviewing, you need to know what to keep and what to delete.
Has your project upgraded to the React Compiler? How many lines of useMemo did you delete after the upgrade? Let's chat in the comments.