← React & TypeScript

React Fundamentals

When does a component re-render in React?

Show answer — try answering out loud first

A component re-renders when its state changes, when its parent re-renders, or when a context it consumes changes. Note: "rendering" means React re-runs the component's function; it doesn't mean the DOM updates, that only happens if the result changed.

Rendering a component is, literally, running its function to get new JSX. The three causes of a re-render:

  • State change: you call the set function of a useState (or similar) with a different value.
  • Parent re-render: if the parent renders, by default all of its children do too, even if their props haven't changed. That's why changing a prop isn't an independent cause: the new props arrive because the parent re-rendered.
  • Context change: if the component consumes a context with useContext and the provider's value changes.

And the DOM? This is where reconciliation comes in:

  1. React runs your component and gets a lightweight description of the UI in JavaScript objects (the famous Virtual DOM).
  2. It compares it with the previous render's (diffing) and applies only the differences to the real DOM.

That's why re-rendering is cheap most of the time: if nothing changed in the result, React doesn't touch the DOM. Think of the Virtual DOM as a draft: it's faster to correct the draft and write only the changes to the final copy than to rewrite the whole page.

import { useState } from "react";

function Reloj({ hora }: { hora: number }) {
  console.log("render de Reloj");
  return <p>Segundos: {hora}</p>;
}
function Etiqueta() {
  console.log("render de Etiqueta"); // also runs because of the parent
  return <span>Contador de la app</span>;
}

function App() {
  const [segundos, setSegundos] = useState(0);
  console.log("render de App");
  return (
    <div>
      <Etiqueta />
      <Reloj hora={segundos} />
      <button onClick={() => setSegundos((prev) => prev + 1)}>Avanzar</button>
    </div>
  );
}

// On click: console -> "render de App", "render de Etiqueta",
// "render de Reloj" (Etiqueta re-renders because its parent did).
// In the DOM only the text "Segundos: N" changes: React compared the
// result and Etiqueta's <span> stayed identical.

Mutating the state and expecting a re-render:

const [usuario, setUsuario] = useState({ nombre: "Ana" });
const renombrar = () => {
  usuario.nombre = "Luis"; // BAD: mutation, the reference is THE SAME
  setUsuario(usuario); // React compares with Object.is and doesn't re-render
};

// GOOD: new reference => React detects the change and re-renders
const renombrarBien = () => setUsuario((prev) => ({ ...prev, nombre: "Luis" }));

React decides to re-render by comparing the new value with the previous one. If you mutate the object, the reference doesn't change and React assumes there's nothing to do; that's why you always create a new object or array.

"A component re-renders for three reasons: its own state changed, its parent re-rendered, or a context it consumes changed. Rendering means React re-runs the component's function to get the new JSX; it doesn't imply touching the DOM. Then comes reconciliation: React compares the new description with the previous one using the Virtual DOM and applies only the differences to the real DOM. That's why a re-render is usually cheap, and I only optimize with memo or useMemo when I actually detect a performance problem."

Quick challenge

Without running the code, predict what the console prints when you click the button once, and what changes in the DOM.

function Hijo() {
  console.log("render Hijo");
  return <p>Soy el hijo</p>;
}
function Padre() {
  const [n, setN] = useState(0);
  console.log("render Padre");
  return (
    <div>
      <Hijo />
      <button onClick={() => setN(n + 1)}>n = {n}</button>
    </div>
  );
}
See solution
// Console on click: "render Padre" and then "render Hijo".
// Padre re-renders because its state changed (n went from 0 to 1); Hijo,
// because its parent rendered, even though it receives no props.
// In the DOM only the button's text changes: "n = 0" -> "n = 1". The child's <p>
// produced the same result, so reconciliation doesn't touch that
// node: rendering is not the same as updating the DOM.