React

Context e performance: cosa causa i re-render

Il context non ri-renderizza tutta l’app: ri-renderizza i consumatori quando il valore cambia identità, e la causa più comune è un oggetto creato nel provider.

In sintesi

  • Si ri-renderizzano solo i componenti che leggono il context, non tutto l’albero.
  • Un valore creato nel render del provider cambia identità a ogni render e sveglia ogni consumatore.
  • Separare stato e azioni in due context riduce i render senza aggiungere librerie.

Il context non ri-renderizza tutta l’app: ri-renderizza i componenti che leggono quel context quando il valore cambia identità. Il costo nasce dal valore ricreato a ogni render del provider, non dal context in sé.

Chi si ri-renderizza

Un componente che chiama useContext si iscrive al context. Quando il provider passa un valore diverso, React ri-renderizza tutti i consumatori, anche quelli che leggono solo una parte del valore. I componenti che non leggono il context non vengono toccati.

Il valore che cambia identità

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  // new object on every render: every consumer re-renders
  const value = { theme, setTheme };

  return (
    {children}
  );
}

Il provider si ri-renderizza a ogni cambio di tema e crea un oggetto nuovo: il confronto fallisce e ogni consumatore si ri-renderizza anche se legge solo setTheme. Con useMemo sul valore l’oggetto resta lo stesso finché theme non cambia.

Separare i context

La seconda correzione è dividere stato e azioni: un context per il valore che cambia e uno per le funzioni stabili. I componenti che leggono solo le azioni smettono di ri-renderizzarsi quando il tema cambia.

Un provider, molti figli

Anche i figli del provider si ri-renderizzano quando il provider stesso si ri-renderizza, a meno che non siano memoizzati: il context non è l’unica causa. Prima di aggiungere memo ovunque, controlla se il provider sta ricreando il valore.

Osservarlo

npm create vite@latest context-demo -- --template react-ts
cd context-demo
npm install
npm run dev

Con l’app aperta, il Profiler di React DevTools registra un’interazione e mostra quali componenti si sono ri-renderizzati e perché. È il modo più rapido per distinguere un problema di context da un problema di props.

Due record che vivono di context: sidebar, dove lo stato di apertura passa da un provider ai suoi figli, e theme-toggle, che legge e scrive il tema. Il resto sta tra i 111.985 componenti e i 1.556 blocchi.

La limitazione

Separare i context riduce i re-render ma aggiunge provider annidati: ogni livello è un componente in più e un ordine da rispettare. E nessuna delle due correzioni aiuta se il valore letto cambia davvero a ogni interazione: lì il problema è nel dato, non nel context.

Dove continuare

Il componente memoizzato che completa la correzione è in React.memo; il valore calcolato invece che copiato è in stato derivato; il confronto fra le librerie headless che usano questo schema è in Radix vs Base UI vs React Aria.

Domande frequenti

Il context causa il re-render di tutti i componenti?

No. Si ri-renderizzano solo i componenti che chiamano useContext su quel context e che stanno sotto il provider. Un componente che non legge il context non riceve l’aggiornamento.

Meglio context o passare le props?

Le props restano la scelta giusta per due o tre livelli; il context conviene quando lo stesso valore serve a molti componenti distanti. Il context non elimina i re-render, cambia solo il modo in cui il valore arriva.

Il context sostituisce Redux o Zustand?

No. Il context distribuisce un valore, non gestisce selezioni, cache o aggiornamenti parziali: ogni cambio ri-renderizza tutti i consumatori. Per uno stato complesso servono store con selettori.

Perché il provider ri-renderizza anche i figli?

Perché i figli sono parte del render del provider, indipendentemente dal context. Se il provider ricrea il valore o il suo stato interno cambia, i figli si ri-renderizzano salvo memoizzazione.

useMemo sul valore del provider è obbligatorio?

No. Serve quando il provider ha consumatori costosi e si ri-renderizza spesso. Su un provider che cambia una volta al minuto la differenza non si misura e il memo aggiunge solo codice.