Introduction
React Forget was the codename for what is now officially known as the React Compiler—a build-time tool that fundamentally changes how React applications are optimized. First announced by the React team at React Conf 2021, the compiler represents years of research into making React faster without requiring developers to manually optimize their code.
The core insight behind React Forget is simple: React's rendering model, where components re-render when their parent re-renders, often leads to unnecessary work. Developers have been told to use useMemo, useCallback, and React.memo to prevent these unnecessary re-renders, but this manual optimization is tedious, error-prone, and often done incorrectly. React Forget automates this entirely.
This guide explains the compiler's internal workings, the problems it solves, its impact on the React ecosystem, and how it changes the way we write React code. Understanding React Forget helps you write better React code today and prepares you for the future of the framework.
Understanding React Forget: Core Concepts
The Memoization Problem
React's rendering model is designed around simplicity: when state changes, re-render the component and its children. While this creates a straightforward mental model, it has a performance cost. Consider this component:
function ProductPage({ product }) {
const [quantity, setQuantity] = useState(1);
const [couponCode, setCouponCode] = useState('');
// This creates a new array every render
const features = product.features.map(f => f.description);
// This creates a new function every render
const handleAddToCart = () => {
addToCart(product.id, quantity);
};
// This recalculates every render even if product hasn't changed
const priceWithTax = product.price * 1.1;
return (
<div>
<ProductFeatures features={features} />
<PriceDisplay price={priceWithTax} />
<QuantitySelector value={quantity} onChange={setQuantity} />
<CouponInput value={couponCode} onChange={setCouponCode} />
<AddToCartButton onClick={handleAddToCart} />
</div>
);
}When the user types in the coupon input, setCouponCode triggers a re-render. This recreates features, handleAddToCart, and recalculates priceWithTax. Since these are new references and values, child components like ProductFeatures, PriceDisplay, and AddToCartButton all re-render—even though nothing they depend on has changed.
The Manual Solution
Before React Forget, developers had to manually optimize:
function ProductPage({ product }) {
const [quantity, setQuantity] = useState(1);
const [couponCode, setCouponCode] = useState('');
const features = useMemo(
() => product.features.map(f => f.description),
[product.features]
);
const handleAddToCart = useCallback(
() => addToCart(product.id, quantity),
[product.id, quantity]
);
const priceWithTax = useMemo(
() => product.price * 1.1,
[product.price]
);
return (
<div>
<ProductFeatures features={features} />
<PriceDisplay price={priceWithTax} />
<QuantitySelector value={quantity} onChange={setQuantity} />
<CouponInput value={couponCode} onChange={setCouponCode} />
<AddToCartButton onClick={handleAddToCart} />
</div>
);
}This is verbose and error-prone. Wrong dependency arrays cause stale closures or unnecessary re-computations. Many developers either over-optimize (wrapping everything) or under-optimize (forgetting to memoize), neither of which is ideal.
What React Forget Actually Does
React Forget analyzes your component code at build time and automatically inserts the equivalent of useMemo and useCallback at the optimal points. The compiler:
- Identifies reactive values: Values derived from props, state, or context
- Tracks dependencies: What each value depends on
- Inserts memoization: Automatically caches values and functions
- Optimizes rendering: Prevents unnecessary re-renders of child components
The result is that the first code example behaves identically to the manually optimized second example, but without any developer effort.
Architecture and Design Patterns
How the Compiler Analyzes Code
The compiler uses static analysis to understand your component's data flow. It builds an internal representation that tracks:
Mutable ranges: Regions of code where a variable can be modified. The compiler ensures memoized values are invalidated when their mutable range changes.
Reactive scopes: Blocks of code that execute in response to state changes. Each scope has a set of dependencies that determine when it needs to re-execute.
Data dependencies: A graph of which values depend on which inputs, allowing the compiler to determine the minimal recomputation needed.
function Example({ items, sortOrder }) {
// Reactive scope 1: depends on items and sortOrder
const sorted = items.sort((a, b) => {
if (sortOrder === 'asc') return a.value - b.value;
return b.value - a.value;
});
// Reactive scope 2: depends on sorted (transitively on items and sortOrder)
const doubled = sorted.map(item => item.value * 2);
// Reactive scope 3: depends on doubled (transitively on items and sortOrder)
const total = doubled.reduce((sum, val) => sum + val, 0);
return <Display value={total} items={sorted} />;
}The compiler recognizes that if items changes, all three scopes need to recompute. If only sortOrder changes, the same scopes recompute because sorted depends on it. But if neither changes (e.g., a parent re-renders with the same props), none of the scopes execute—producing the same memoized values.
The Transformation Process
When the compiler transforms a component, it:
-
Wraps reactive computations: Each derived value is wrapped in a memoization check that compares current inputs to previous inputs.
-
Creates memoization slots: The compiler allocates storage for previous input values and computed results, similar to how hooks store state on fibers.
-
Inserts comparison logic: Before recomputing, the compiler checks if inputs have changed using
Object.is()comparison. -
Preserves semantics: The transformed code produces identical output to the original—memoization is purely a performance optimization, not a behavioral change.
Compile-Time vs Runtime Optimization
React Forget operates at compile time, which has significant advantages:
No runtime overhead for analysis: The analysis happens once during build, not every time the component renders.
Smarter optimization: The compiler can make optimization decisions that would be impractical at runtime, like cross-component analysis.
Deterministic output: Given the same input code, the compiler always produces the same optimized output.
Bundle size considerations: The compiler may slightly increase bundle size due to inserted memoization code, but the runtime performance gains far outweigh this cost.
Step-by-Step Implementation
Step 1: Understanding What Gets Compiled
The compiler handles these React patterns:
// Function components - fully supported
function MyComponent({ prop1, prop2 }) {
const derived = computeDerived(prop1);
return <Child value={derived} />;
}
// Custom hooks - fully supported
function useCustomHook(data) {
const processed = processData(data);
const callback = useCallback(() => doSomething(processed), [processed]);
return { processed, callback };
}
// Class components - not supported (use class lifecycle methods instead)
// Server components - supported with React 19+Step 2: Code That Won't Compile
Some patterns prevent compilation:
// Mutation after use in JSX - compiler can't track this
function BadComponent({ items }) {
const sorted = items.sort((a, b) => a.value - b.value); // Mutates the array!
return <List items={sorted} />;
}
// Fix: create a new array instead
function GoodComponent({ items }) {
const sorted = [...items].sort((a, b) => a.value - b.value);
return <List items={sorted} />;
}
// Conditional hook calls - violates rules of hooks
function BadComponent({ condition }) {
if (condition) {
const [value, setValue] = useState(0); // Won't compile
}
}
// Fix: always call hooks at the top level
function GoodComponent({ condition }) {
const [value, setValue] = useState(0);
if (condition) {
// Use value conditionally, not the hook call
}
}Step 3: Migrating Existing Code
When enabling the compiler in an existing codebase:
- Remove manual memoization: Delete
useMemo,useCallback, andReact.memowrappers - Fix rule violations: Address any ESLint warnings about React rules
- Test thoroughly: Run your test suite to verify behavior is preserved
- Profile performance: Use React DevTools to verify reduced re-renders
// Before migration
const MemoizedChild = React.memo(function Child({ data, onClick }) {
return <div onClick={onClick}>{data.name}</div>;
});
function Parent() {
const [state, setState] = useState(0);
const data = useMemo(() => ({ name: 'test' }), []);
const handleClick = useCallback(() => setState(s => s + 1), []);
return <MemoizedChild data={data} onClick={handleClick} />;
}
// After migration (with compiler)
function Child({ data, onClick }) {
return <div onClick={onClick}>{data.name}</div>;
}
function Parent() {
const [state, setState] = useState(0);
const data = { name: 'test' };
const handleClick = () => setState(s => s + 1);
return <Child data={data} onClick={handleClick} />;
}Step 4: Writing Compiler-Friendly Code
Write code that the compiler can optimize effectively:
// Good: immutable data patterns
function FilterableList({ items, filter }) {
const filtered = items.filter(item => item.active === filter);
const sorted = [...filtered].sort((a, b) => a.name.localeCompare(b.name));
return <List items={sorted} />;
}
// Good: stable callback references
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
await submitForm({ name, email });
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}Step 5: Server Components and React Forget
Server Components benefit from the compiler differently since they don't render on the client:
// Server Component - compiler optimizes server-side computation
async function ProductPage({ id }) {
const product = await fetchProduct(id);
const reviews = await fetchReviews(id);
const averageRating = reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length;
return (
<div>
<ProductDetails product={product} />
<Rating value={averageRating} />
<ReviewList reviews={reviews} />
</div>
);
}The compiler ensures that if ProductPage re-renders with the same id, the expensive computations aren't repeated.
Real-World Use Cases and Case Studies
Use Case 1: Data Grid with Sorting and Filtering
function DataGrid({ rawData, columns }) {
const [sortColumn, setSortColumn] = useState('name');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const [filters, setFilters] = useState<Record<string, string>>({});
const [searchQuery, setSearchQuery] = useState('');
// Compiler automatically memoizes each derived computation
const searchFiltered = rawData.filter(row =>
Object.values(row).some(val =>
String(val).toLowerCase().includes(searchQuery.toLowerCase())
)
);
const columnFiltered = searchFiltered.filter(row =>
Object.entries(filters).every(([key, value]) =>
!value || String(row[key]).toLowerCase().includes(value.toLowerCase())
)
);
const sorted = [...columnFiltered].sort((a, b) => {
const compare = String(a[sortColumn]).localeCompare(String(b[sortColumn]));
return sortDirection === 'asc' ? compare : -compare;
});
const stats = {
total: rawData.length,
filtered: sorted.length,
percentage: Math.round((sorted.length / rawData.length) * 100),
};
return (
<div>
<SearchBar value={searchQuery} onChange={setSearchQuery} />
<ColumnFilters columns={columns} filters={filters} onChange={setFilters} />
<StatsBar stats={stats} />
<Table data={sorted} columns={columns} onSort={setSortColumn} />
</div>
);
}Use Case 2: Multi-Step Form
function MultiStepForm() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState(initialFormData);
const [validationErrors, setValidationErrors] = useState({});
// Compiler memoizes validation based on formData
const personalValid = validatePersonal(formData.personal);
const addressValid = validateAddress(formData.address);
const paymentValid = validatePayment(formData.payment);
const currentStepValid = step === 1 ? personalValid : step === 2 ? addressValid : paymentValid;
const allStepsValid = personalValid && addressValid && paymentValid;
const progress = Math.round((step / 3) * 100);
return (
<div>
<ProgressBar value={progress} />
{step === 1 && <PersonalInfoStep data={formData.personal} onChange={updatePersonal} />}
{step === 2 && <AddressStep data={formData.address} onChange={updateAddress} />}
{step === 3 && <PaymentStep data={formData.payment} onChange={updatePayment} />}
<StepNavigation
step={step}
canProceed={currentStepValid}
onPrev={() => setStep(s => s - 1)}
onNext={() => setStep(s => s + 1)}
/>
</div>
);
}Use Case 3: Real-Time Search with Debouncing
function SearchInterface({ items }) {
const [query, setQuery] = useState('');
const [category, setCategory] = useState('all');
const [sortBy, setSortBy] = useState('relevance');
const results = items
.filter(item => category === 'all' || item.category === category)
.filter(item => item.title.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'rating') return b.rating - a.rating;
return 0; // relevance = default order
});
const resultCount = results.length;
const suggestions = query.length > 2
? items.filter(i => i.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
: [];
return (
<div>
<SearchInput value={query} onChange={setQuery} suggestions={suggestions} />
<CategoryFilter value={category} onChange={setCategory} />
<SortSelect value={sortBy} onChange={setSortBy} />
<ResultCount count={resultCount} />
<SearchResults results={results} />
</div>
);
}Best Practices for Production
-
Write Immutable Code: The compiler assumes data is immutable. Don't mutate arrays or objects; create new ones instead. Use spread operator,
map,filter, and other non-mutating methods. -
Follow the Rules of React: Don't call hooks conditionally, don't mutate state directly, and ensure hooks are called at the top level. These rules are now enforced at build time.
-
Remove Manual Memoization: Delete
useMemo,useCallback, andReact.memoafter enabling the compiler. They're redundant and can interfere with the compiler's analysis. -
Use TypeScript: TypeScript helps the compiler make better optimization decisions by providing type information that improves static analysis.
-
Keep Components Focused: Smaller, focused components are easier for the compiler to optimize. Large monolithic components have more complex data flows.
-
Test After Enabling: Run your full test suite to verify that compiled code behaves identically to the original.
-
Profile Before and After: Use React DevTools Profiler to verify the compiler is reducing re-renders as expected.
-
Update Regularly: The compiler is actively developed. Keep it updated for the latest optimizations and bug fixes.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| Mutating arrays/objects | Compiler can't track changes, stale data | Use non-mutating methods: map, filter, spread |
| Conditional hook calls | Compilation fails | Move hooks to top level |
| Impure render functions | Incorrect memoization, bugs | Ensure renders are pure: same inputs → same outputs |
| External mutable references | Compiler assumes immutability | Use useRef or useSyncExternalStore |
| Mixing manual and auto memo | Conflicts, unpredictable behavior | Remove all manual memoization |
| Over-reliance on compiler | Ignoring architectural issues | The compiler optimizes re-renders, not bad architecture |
Performance Optimization
Measuring Compiler Impact
// Enable React DevTools Profiler
import { Profiler } from 'react';
function onRender(id, phase, actualDuration) {
if (actualDuration > 16) { // Flag renders taking more than one frame
console.warn(`Slow render: ${id} took ${actualDuration}ms`);
}
}
function App() {
return (
<Profiler id="App" onRender={onRender}>
<AppContent />
</Profiler>
);
}Expected Performance Gains
Based on the React team's benchmarks and community reports:
- Initial render: Minimal impact (compiler doesn't optimize mount)
- Re-renders with unchanged props: 30-60% fewer component re-renders
- Memory usage: Slight increase from memoization storage
- Bundle size: 5-15% increase from inserted memoization code
- Overall interaction latency: 20-40% improvement in complex UIs
Comparison with Alternatives
| Feature | React Forget | Manual useMemo | Preact Signals | Solid.js |
|---|---|---|---|---|
| Automation | Full | None | Partial | N/A |
| Learning Curve | Low | Medium | Low | High |
| Bundle Size | +5-15% | Varies | +1KB | +7KB |
| Migration Effort | Low | High | Medium | Very High |
| React Compatibility | Full | Full | Partial | None |
| Server Components | Yes | Yes | No | Yes |
Advanced Patterns
Compiler with Complex State Machines
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
const dispatch = (action) => {
setState(prevState => reducer(prevState, action));
};
return [state, dispatch];
}
function OrderForm() {
const [state, dispatch] = useReducer(orderReducer, initialOrderState);
// Compiler optimizes these derived values
const canProceed = validateStep(state.currentStep, state.data);
const orderTotal = calculateTotal(state.data.items);
const shippingCost = calculateShipping(state.data.address, orderTotal);
const grandTotal = orderTotal + shippingCost;
return (
<div>
<StepIndicator current={state.currentStep} />
<OrderSummary total={grandTotal} shipping={shippingCost} />
<StepContent step={state.currentStep} data={state.data} dispatch={dispatch} />
</div>
);
}Optimizing Context Providers
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const [fontSize, setFontSize] = useState(16);
// Compiler memoizes this object reference
const value = {
theme,
fontSize,
toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light'),
increaseFontSize: () => setFontSize(s => s + 2),
decreaseFontSize: () => setFontSize(s => Math.max(12, s - 2)),
};
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}Testing Strategies
Testing Compiled Components
import { render, screen, fireEvent, renderHook } from '@testing-library/react';
describe('Compiled component behavior', () => {
it('preserves referential stability', () => {
const renders: number[] = [];
function Child({ value }: { value: number }) {
renders.push(Date.now());
return <div>{value}</div>;
}
function Parent() {
const [count, setCount] = useState(0);
const [unrelated, setUnrelated] = useState('');
const derived = count * 2;
return (
<div>
<input value={unrelated} onChange={(e) => setUnrelated(e.target.value)} />
<Child value={derived} />
<button onClick={() => setCount(c => c + 1)}>Inc</button>
</div>
);
}
render(<Parent />);
const initialRenderCount = renders.length;
// Typing in input should not re-render Child (compiler optimization)
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'test' } });
expect(renders.length).toBe(initialRenderCount);
// Clicking button SHOULD re-render Child
fireEvent.click(screen.getByText('Inc'));
expect(renders.length).toBe(initialRenderCount + 1);
});
});Future Outlook
React Forget/Compiler is one of the most significant developments in React's history. Future directions include:
- Cross-component optimization: Analyzing multiple components together for better optimization
- Server Component integration: Deeper optimization of server-side computations
- IDE integration: Real-time feedback in VS Code about compiler optimizations
- Metrics and reporting: Built-in compilation reports showing optimization effectiveness
- Community tools: Ecosystem of compiler plugins for framework-specific optimizations
Conclusion
React Forget, now the React Compiler, represents a fundamental shift in how React applications are optimized. By automatically inserting memoization at build time, it eliminates the most tedious and error-prone aspect of React development.
Key takeaways:
- The compiler automates memoization—no more
useMemo,useCallback, orReact.memo - Write clean, simple code and let the compiler optimize rendering performance
- Follow the Rules of React—the compiler enforces them at build time
- Use immutable data patterns—don't mutate arrays or objects after using them
- Remove manual memoization after enabling the compiler
- Test thoroughly to verify compiled code preserves behavior
- Profile before and after to measure the actual performance impact
The compiler lets you focus on building features rather than worrying about render optimization. It's the future of React performance, and understanding it prepares you for the next generation of React development.