Mastering Re-renders in React 19

Mastering Re-renders in React 19

React’s rendering model is simple: when state changes, the UI updates. However, unnecessary re-renders can lead to performance bottlenecks in large applications. With React 19, the landscape of optimization has shifted dramatically.

What Causes Re-renders?

In React, a component re-renders for three main reasons:

  1. State Changes: When state declared with useState or useReducer updates.
  2. Parent Re-renders: When a parent component re-renders, all its children re-render recursively (by default).
  3. Context Changes: When a Context Provider’s value changes, all consumers re-render.

The “Old” Way: Manual Memoization

Before React 19, we relied heavily on React.memo, useMemo, and useCallback to prevent wasted renders.

import { memo, useState, useCallback } from "react";

const ExpensiveComponent = memo(({ onClick, data }) => {
  console.log("ExpensiveComponent rendered");
  return <button onClick={onClick}>{data}</button>;
});

function App() {
  const [count, setCount] = useState(0);

  // We had to memoize this function to prevent ExpensiveComponent from re-rendering
  const handleClick = useCallback(() => {
    console.log("Clicked");
  }, []);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <ExpensiveComponent onClick={handleClick} data="Click me" />
    </div>
  );
}

This manual optimization was error-prone. If you forgot a dependency in the array, you’d get stale closures. If you forgot memo, the optimization wouldn’t work.

Enter React 19 and the React Compiler

React 19 introduces the React Compiler (formerly React Forget). It automatically memoizes your components, props, and dependency arrays during the build process.

This means you no longer need to manually wrap components in memo or functions in useCallback for performance reasons in most cases. The compiler understands the data flow and only re-renders what is strictly necessary.

How React 19 Code Looks

With the compiler enabled, the previous example becomes much simpler:

import { useState } from "react";

// No memo needed!
function ExpensiveComponent({ onClick, data }) {
  console.log("ExpensiveComponent rendered");
  return <button onClick={onClick}>{data}</button>;
}

function App() {
  const [count, setCount] = useState(0);

  // No useCallback needed!
  function handleClick() {
    console.log("Clicked");
  }

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <ExpensiveComponent onClick={handleClick} data="Click me" />
    </div>
  );
}

The compiler detects that handleClick and data haven’t changed semantically even if App re-renders due to count changing, so it won’t re-render ExpensiveComponent.

Optimizing Context in React 19

Context is another common source of re-renders. In React 19, the new use API simplifies how we consume context, but the optimization principles remain similar, though the Compiler helps here too.

import { createContext, use, useState } from "react";

const ThemeContext = createContext("light");

function ThemeButton() {
  // New 'use' API for reading context
  const theme = use(ThemeContext);
  return <button className={theme}>I am styled!</button>;
}

Summary

  1. Don’t obsess over manual memoization anymore.
  2. Enable the React Compiler in your build setup.
  3. Focus on composition and state placement. Keep state as local as possible.
  4. Use the new React 19 features like Actions and use to simplify data flow, which naturally leads to fewer render cycles.

React 19 allows us to write “plain JavaScript” again, letting the framework handle the reactivity performance under the hood.