MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

React Forget: The React Compiler

Understand React's automatic memoization compiler: how it works and its impact.

ReactCompilerPerformanceFrontend

By MinhVo

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.

Compiler transformation visual

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:

  1. Identifies reactive values: Values derived from props, state, or context
  2. Tracks dependencies: What each value depends on
  3. Inserts memoization: Automatically caches values and functions
  4. 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.

Dependency graph visualization

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:

  1. Wraps reactive computations: Each derived value is wrapped in a memoization check that compares current inputs to previous inputs.

  2. Creates memoization slots: The compiler allocates storage for previous input values and computed results, similar to how hooks store state on fibers.

  3. Inserts comparison logic: Before recomputing, the compiler checks if inputs have changed using Object.is() comparison.

  4. 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:

  1. Remove manual memoization: Delete useMemo, useCallback, and React.memo wrappers
  2. Fix rule violations: Address any ESLint warnings about React rules
  3. Test thoroughly: Run your test suite to verify behavior is preserved
  4. 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.

Performance comparison chart

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

  1. 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.

  2. 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.

  3. Remove Manual Memoization: Delete useMemo, useCallback, and React.memo after enabling the compiler. They're redundant and can interfere with the compiler's analysis.

  4. Use TypeScript: TypeScript helps the compiler make better optimization decisions by providing type information that improves static analysis.

  5. Keep Components Focused: Smaller, focused components are easier for the compiler to optimize. Large monolithic components have more complex data flows.

  6. Test After Enabling: Run your full test suite to verify that compiled code behaves identically to the original.

  7. Profile Before and After: Use React DevTools Profiler to verify the compiler is reducing re-renders as expected.

  8. Update Regularly: The compiler is actively developed. Keep it updated for the latest optimizations and bug fixes.

Common Pitfalls and Solutions

PitfallImpactSolution
Mutating arrays/objectsCompiler can't track changes, stale dataUse non-mutating methods: map, filter, spread
Conditional hook callsCompilation failsMove hooks to top level
Impure render functionsIncorrect memoization, bugsEnsure renders are pure: same inputs → same outputs
External mutable referencesCompiler assumes immutabilityUse useRef or useSyncExternalStore
Mixing manual and auto memoConflicts, unpredictable behaviorRemove all manual memoization
Over-reliance on compilerIgnoring architectural issuesThe 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

FeatureReact ForgetManual useMemoPreact SignalsSolid.js
AutomationFullNonePartialN/A
Learning CurveLowMediumLowHigh
Bundle Size+5-15%Varies+1KB+7KB
Migration EffortLowHighMediumVery High
React CompatibilityFullFullPartialNone
Server ComponentsYesYesNoYes

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:

  1. The compiler automates memoization—no more useMemo, useCallback, or React.memo
  2. Write clean, simple code and let the compiler optimize rendering performance
  3. Follow the Rules of React—the compiler enforces them at build time
  4. Use immutable data patterns—don't mutate arrays or objects after using them
  5. Remove manual memoization after enabling the compiler
  6. Test thoroughly to verify compiled code preserves behavior
  7. 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.