Back to blog

React Compiler in 2026: The End of Manual useMemo and useCallback

Hello HaWkers, one of the biggest changes in the React ecosystem in 2026 is the mass adoption of the React Compiler. The manual memoization that plagued developers for years is finally becoming a thing of the past.

Let's understand what changed and how to write modern React.

The Problem the Compiler Solves

The Pain of Manual Memoization

For years, React developers had to deal with this:

// The hell of manual memoization (how it was before)

import { useMemo, useCallback, memo } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

interface UserListProps {
  users: User[];
  onSelect: (user: User) => void;
  filter: string;
}

// Memoized component
const UserCard = memo(({ user, onSelect }: {
  user: User;
  onSelect: (user: User) => void;
}) => {
  return (
    <div onClick={() => onSelect(user)}>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
});

function UserList({ users, onSelect, filter }: UserListProps) {
  // useMemo to filter users
  const filteredUsers = useMemo(() => {
    return users.filter(u =>
      u.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [users, filter]);

  // useCallback to stabilize reference
  const handleSelect = useCallback((user: User) => {
    console.log('Selected:', user);
    onSelect(user);
  }, [onSelect]);

  // useMemo for expensive computation
  const stats = useMemo(() => ({
    total: users.length,
    filtered: filteredUsers.length,
    percentage: (filteredUsers.length / users.length * 100).toFixed(1)
  }), [users.length, filteredUsers.length]);

  return (
    <div>
      <p>Showing {stats.filtered} of {stats.total} ({stats.percentage}%)</p>
      {filteredUsers.map(user => (
        <UserCard
          key={user.id}
          user={user}
          onSelect={handleSelect}
        />
      ))}
    </div>
  );
}

Problems with this approach:

  • Verbose and hard to read code
  • Easy to forget dependencies
  • Over-memoization (memoizing things that don't need it)
  • Under-memoization (forgetting to memoize what needs it)
  • Hard to know when to memoize

The React Compiler

How It Works

// With React Compiler (2026) - THE SAME code, without manual memoization

interface User {
  id: number;
  name: string;
  email: string;
}

interface UserListProps {
  users: User[];
  onSelect: (user: User) => void;
  filter: string;
}

// No memo() - compiler optimizes automatically
function UserCard({ user, onSelect }: {
  user: User;
  onSelect: (user: User) => void;
}) {
  return (
    <div onClick={() => onSelect(user)}>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}

// Clean and natural code
function UserList({ users, onSelect, filter }: UserListProps) {
  // No useMemo - compiler detects and optimizes
  const filteredUsers = users.filter(u =>
    u.name.toLowerCase().includes(filter.toLowerCase())
  );

  // No useCallback - compiler stabilizes automatically
  const handleSelect = (user: User) => {
    console.log('Selected:', user);
    onSelect(user);
  };

  // No useMemo - compiler knows it's derived computation
  const stats = {
    total: users.length,
    filtered: filteredUsers.length,
    percentage: (filteredUsers.length / users.length * 100).toFixed(1)
  };

  return (
    <div>
      <p>Showing {stats.filtered} of {stats.total} ({stats.percentage}%)</p>
      {filteredUsers.map(user => (
        <UserCard
          key={user.id}
          user={user}
          onSelect={handleSelect}
        />
      ))}
    </div>
  );
}

What the compiler does:

  1. Analyzes code at compile time
  2. Detects values that can be memoized
  3. Automatically inserts memoization where needed
  4. Ensures callbacks are stable
  5. Automatically optimizes re-renders

What the Compiler Analyzes

Intelligent Detection

// The compiler understands common patterns

function ProductPage({ productId }: { productId: string }) {
  const [quantity, setQuantity] = useState(1);

  // Compiler detects: depends only on productId
  // Automatically memoizes
  const product = products.find(p => p.id === productId);

  // Compiler detects: depends on product and quantity
  // Memoizes and recalculates when needed
  const totalPrice = product ? product.price * quantity : 0;

  // Compiler detects: function passed as prop
  // Automatically stabilizes reference
  const handleAddToCart = () => {
    addToCart(productId, quantity);
  };

  // Compiler detects: object created during render
  // Memoizes to prevent child re-renders
  const cartItem = {
    productId,
    quantity,
    price: totalPrice
  };

  return (
    <div>
      <h1>{product?.name}</h1>
      <QuantitySelector
        value={quantity}
        onChange={setQuantity}  // setter is stable by nature
      />
      <p>Total: ${totalPrice}</p>
      <AddToCartButton
        item={cartItem}       // memoized object
        onAdd={handleAddToCart} // stabilized callback
      />
    </div>
  );
}

Configuration in 2026

Standard Setup

// next.config.ts (Next.js 15+)
import type { NextConfig } from 'next';

const config: NextConfig = {
  experimental: {
    // React Compiler enabled by default in 2026
    reactCompiler: true,
  },
};

export default config;

// For Vite projects
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import reactCompiler from 'babel-plugin-react-compiler';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [reactCompiler],
      },
    }),
  ],
});

ESLint Plugin

// eslint.config.js
import reactCompiler from 'eslint-plugin-react-compiler';

export default [
  {
    plugins: {
      'react-compiler': reactCompiler,
    },
    rules: {
      // Warns when code might be problematic
      'react-compiler/react-compiler': 'error',
    },
  },
];

When to Still Use Manual Hooks

Special Cases

// Cases where manual memoization still makes sense

// 1. VERY expensive computations with fine control
function DataVisualization({ data }: { data: number[] }) {
  // For calculations that take seconds, you might
  // want explicit control
  const processedData = useMemo(() => {
    return expensiveStatisticalAnalysis(data);
  }, [data]);

  // Or use the new useDeferredValue API to not block
  const deferredData = useDeferredValue(data);

  return <Chart data={processedData} />;
}

// 2. Integration with external libraries
function MapComponent({ markers }: { markers: Marker[] }) {
  // External libraries may have their own
  // comparison rules
  const memoizedMarkers = useMemo(
    () => markers.map(m => createMapMarker(m)),
    [markers]
  );

  return <ExternalMapLibrary markers={memoizedMarkers} />;
}

// 3. Refs and imperative handles
function VideoPlayer({ src }: { src: string }) {
  const videoRef = useRef<HTMLVideoElement>(null);

  // useCallback still useful for imperative APIs
  const play = useCallback(() => {
    videoRef.current?.play();
  }, []);

  const pause = useCallback(() => {
    videoRef.current?.pause();
  }, []);

  // Exposing imperative methods
  useImperativeHandle(ref, () => ({
    play,
    pause,
  }), [play, pause]);

  return <video ref={videoRef} src={src} />;
}

Modern Patterns in 2026

Server Components + Compiler

// Combining React Server Components with Compiler

// app/products/page.tsx (Server Component)
async function ProductsPage() {
  // Data fetched on server
  const products = await fetchProducts();

  return (
    <div>
      <h1>Products</h1>
      {/* Client Component receives serialized data */}
      <ProductGrid products={products} />
    </div>
  );
}

// components/ProductGrid.tsx (Client Component)
'use client';

function ProductGrid({ products }: { products: Product[] }) {
  const [filter, setFilter] = useState('');
  const [sort, setSort] = useState<'price' | 'name'>('name');

  // Compiler optimizes everything automatically
  const filtered = products.filter(p =>
    p.name.toLowerCase().includes(filter.toLowerCase())
  );

  const sorted = [...filtered].sort((a, b) => {
    if (sort === 'price') return a.price - b.price;
    return a.name.localeCompare(b.name);
  });

  return (
    <div>
      <FilterInput value={filter} onChange={setFilter} />
      <SortSelect value={sort} onChange={setSort} />
      <div className="grid">
        {sorted.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

Actions and Mutations

// Modern pattern with Server Actions

// actions/cart.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function addToCart(productId: string, quantity: number) {
  await db.cart.add({ productId, quantity });
  revalidatePath('/cart');
}

// components/AddToCartButton.tsx
'use client';

import { addToCart } from '@/actions/cart';
import { useTransition } from 'react';

function AddToCartButton({ productId }: { productId: string }) {
  const [isPending, startTransition] = useTransition();
  const [quantity, setQuantity] = useState(1);

  // Compiler optimizes automatically
  const handleClick = () => {
    startTransition(async () => {
      await addToCart(productId, quantity);
    });
  };

  return (
    <button onClick={handleClick} disabled={isPending}>
      {isPending ? 'Adding...' : 'Add to Cart'}
    </button>
  );
}

Migrating Legacy Code

Migration Strategy

// Step 1: Enable compiler in project

// Step 2: Gradually remove unnecessary memoization

// BEFORE
const MemoizedComponent = memo(function Component({ data }) {
  const processed = useMemo(() => process(data), [data]);
  const handler = useCallback(() => doSomething(), []);
  return <div onClick={handler}>{processed}</div>;
});

// AFTER
function Component({ data }) {
  const processed = process(data);
  const handler = () => doSomething();
  return <div onClick={handler}>{processed}</div>;
}

// Step 3: Trust the ESLint plugin to warn about problems

// Step 4: Test performance before and after

What to Remove

// Migration checklist

const migrationChecklist = {
  // Safe to remove
  safeToRemove: [
    'React.memo() on simple components',
    'useMemo() for objects passed as props',
    'useCallback() for event handlers',
    'useMemo() for simple derived computations'
  ],

  // Evaluate case by case
  evaluate: [
    'useMemo() for very expensive computations',
    'useCallback() in external libraries',
    'memo() with custom comparison function'
  ],

  // Keep
  keep: [
    'useRef() - not memoization',
    'useState() - not memoization',
    'useEffect() - side effects',
    'useLayoutEffect() - synchronous side effects'
  ]
};

Performance in 2026

Comparison

// Typical migration results

const performanceComparison = {
  bundleSize: {
    before: '145kb',
    after: '142kb',
    note: 'Compiler adds little overhead'
  },

  developerExperience: {
    before: 'Many dependency bugs forgotten',
    after: 'Zero memoization concerns',
    timesSaved: 'Significant'
  },

  runtime: {
    before: 'Inconsistent performance',
    after: 'Consistent automatic optimization',
    improvement: '10-30% in apps with incorrect memoization'
  },

  codeReadability: {
    before: 'Polluted with memoization hooks',
    after: 'Clean and straight to the point',
    improvement: 'Significant'
  }
};

Conclusion

The React Compiler represents a paradigm shift: from "how do I optimize this?" to "write natural code and let the compiler optimize."

Key takeaways:

  1. Manual memoization is a thing of the past - The compiler does it better than humans
  2. Cleaner code - Focus on logic, not optimization
  3. Fewer bugs - No more forgotten dependency errors
  4. Consistent performance - Automatic and intelligent optimization

If you haven't enabled the React Compiler in your project yet, 2026 is the year to do it. The React community has already adopted it en masse, and major frameworks have first-class support.

To learn more about the current JavaScript ecosystem, check out: TypeScript Is the Standard in 2026.

Let's go! 🦅

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments