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:
- Analyzes code at compile time
- Detects values that can be memoized
- Automatically inserts memoization where needed
- Ensures callbacks are stable
- 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 afterWhat 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:
- Manual memoization is a thing of the past - The compiler does it better than humans
- Cleaner code - Focus on logic, not optimization
- Fewer bugs - No more forgotten dependency errors
- 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.

