跪拜 Guibai
← Back to the summary

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.

image.png

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:

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:

  1. useCallback itself doesn't prevent child component re-renders — unless the child component uses React.memo
  2. 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
  3. handleExport's dependency array includes count, 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:

  1. 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
  2. React.memo performs a shallow comparison of props on every render — if props change frequently, this comparison itself is a waste
  3. 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:

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.