Context API e useReducer

4 gennaio 2026
15 min di lettura

Introduzione

Quando si costruiscono applicazioni React complesse, condividere lo state tra componenti può richiedere passare props attraverso molti livelli (prop drilling). Questo articolo esplora la Context API per condividere dati senza prop drilling e l’hook useReducer per gestire state complesso utilizzando il pattern del reducer.

Il Problema del Prop Drilling

In applicazioni React complesse, si ha spesso bisogno di condividere lo state tra componenti che si trovano in punti diversi dell’albero dei componenti. Un componente potrebbe dover visualizzare dei dati, mentre un altro componente, completamente separato, potrebbe dover aggiornare quegli stessi dati.

La soluzione più immediata è sollevare lo state al componente comune più vicino che ha accesso a tutti i componenti interessati, e poi passare i dati e le funzioni di aggiornamento attraverso le props. Questo approccio funziona, ma può portare a quello che viene chiamato prop drilling.

Cos’è il Prop Drilling

Il prop drilling si verifica quando si devono passare props attraverso molti livelli di componenti, anche se la maggior parte di questi componenti non utilizza direttamente quei dati. Essi li ricevono solo per passarli ai componenti figli.

Esempio di prop drilling

Immaginiamo un’applicazione e-commerce con questa struttura:

function App() {
const [cartItems, setCartItems] = useState([]);
return (
<>
<Header cartItems={cartItems} />
<Shop onAddToCart={handleAddToCart} />
</>
);
}
function Header({ cartItems }) {
// Header non usa cartItems direttamente, ma li passa a CartModal
return (
<header>
<CartModal cartItems={cartItems} />
</header>
);
}
function CartModal({ cartItems }) {
// CartModal usa finalmente cartItems
return <div>{cartItems.length} items</div>;
}
function Shop({ onAddToCart }) {
// Shop non usa onAddToCart direttamente, ma lo passa a Product
return (
<div>
<Product onAddToCart={onAddToCart} />
</div>
);
}
function Product({ onAddToCart }) {
// Product usa finalmente onAddToCart
return <button onClick={() => onAddToCart(id)}>Add to Cart</button>;
}

In questo esempio, Header e Shop ricevono props che non utilizzano direttamente, ma che devono solo inoltrare ai loro figli. Questo è prop drilling.

Il prop drilling aumenta il boilerplate e rende difficile capire quali componenti utilizzano effettivamente i dati e quali li stanno solo inoltrando.

Component Composition come Soluzione Parziale

Una prima soluzione al prop drilling è utilizzare la component composition, sfruttando la prop speciale children per ridurre i livelli di nesting e permettere ai componenti genitori di renderizzare direttamente i componenti che hanno bisogno dei dati.

Come Funziona la Component Composition

L’idea è trasformare componenti che fungono solo da wrapper in componenti che accettano children e li renderizzano direttamente, permettendo al componente genitore di passare direttamente i dati necessari ai componenti che li utilizzano.

Esempio: Refactoring con component composition

Invece di passare onAddToCart attraverso Shop:

// Prima: prop drilling
function App() {
return <Shop onAddToCart={handleAddToCart} />;
}
function Shop({ onAddToCart }) {
return <Product onAddToCart={onAddToCart} />;
}

Si può refactorizzare Shop per accettare children:

// Dopo: component composition
function App() {
return (
<Shop>
<Product onAddToCart={handleAddToCart} />
</Shop>
);
}
function Shop({ children }) {
return <div className="shop">{children}</div>;
}

Ora handleAddToCart viene passato direttamente a Product senza dover passare attraverso Shop.

La component composition aiuta ma non scala bene quando i componenti sono profondamente annidati o in rami diversi dell’albero. Per questi casi serve la Context API.

La Context API di React

La Context API è una funzionalità integrata in React progettata specificamente per condividere dati tra componenti senza dover passare props attraverso ogni livello dell’albero dei componenti.

Cos’è la Context API

L’idea alla base della Context API è creare un valore di contesto che viene “fornito” a un gruppo di componenti. Questo valore può essere facilmente collegato allo state, permettendo a qualsiasi componente che ha accesso al contesto di leggere e aggiornare lo state senza dover ricevere props.

Quando un componente è “avvolto” da un Provider del contesto, può accedere direttamente al valore del contesto utilizzando l’hook useContext (o use in React 19+), eliminando completamente la necessità di prop drilling.

Creare un Context

Per creare un context, si utilizza la funzione createContext di React:

import { createContext } from 'react';
// Creazione del context con un valore iniziale
export const CartContext = createContext({
items: [],
addItemToCart: () => {},
});

Il valore passato a createContext è il valore iniziale (o default) che verrà utilizzato se un componente cerca di accedere al context senza essere avvolto da un Provider. Questo valore è utile anche per migliorare l’autocompletamento nell’IDE.

Dettagli tecnici su createContext

createContext restituisce un oggetto che contiene:

  • Provider: Un componente React che viene utilizzato per fornire il valore del context ai componenti figli
  • Consumer: Un componente alternativo per accedere al context (meno comune, vedremo dopo)

Il valore iniziale passato a createContext viene utilizzato solo se un componente cerca di accedere al context senza essere avvolto da un Provider. In pratica, si dovrebbe sempre utilizzare un Provider, ma il valore iniziale aiuta con l’autocompletamento e può servire come fallback.

Fornire il Context con il Provider

Dopo aver creato il context, si deve fornirlo ai componenti utilizzando il componente Provider. Il Provider deve avvolgere tutti i componenti che hanno bisogno di accedere al context.

import { CartContext } from './store/shopping-cart-context';
function App() {
return (
<CartContext.Provider value={{ items: [] }}>
<Header />
<Shop />
</CartContext.Provider>
);
}

Nota importante: In React 19+, si può usare CartContext direttamente come componente. In versioni precedenti (React 18 e inferiori), si deve usare CartContext.Provider. Per compatibilità con versioni precedenti, si usa CartContext.Provider.

Il value prop è obbligatorio e contiene il valore effettivo che si vuole condividere attraverso il context. Questo valore può essere qualsiasi cosa: un numero, una stringa, un oggetto, un array, o anche funzioni.

Collegare il Context allo State

Per rendere il context dinamico, si collega il valore del context allo state del componente:

function App() {
const [shoppingCart, setShoppingCart] = useState({
items: []
});
return (
<CartContext.Provider value={shoppingCart}>
<Header />
<Shop />
</CartContext.Provider>
);
}

Ora, quando lo state shoppingCart cambia, tutti i componenti che accedono al context vedranno automaticamente il nuovo valore.

Consumare il Context con useContext

Per accedere al valore del context in un componente, si utilizza l’hook useContext:

import { useContext } from 'react';
import { CartContext } from './store/shopping-cart-context';
function Cart() {
const cartCtx = useContext(CartContext);
return (
<div>
{cartCtx.items.length === 0 ? (
<p>No items in cart</p>
) : (
<ul>
{cartCtx.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)}
</div>
);
}

useContext riceve il context object come argomento e restituisce il valore corrente del context. Se il componente non è avvolto da un Provider, restituirà il valore iniziale passato a createContext.

use vs useContext: Quale usare?

React 19 introduce un nuovo hook chiamato use che può essere utilizzato anche per accedere ai context:

import { use } from 'react';
function Cart() {
const cartCtx = use(CartContext);
// ... resto del codice
}

Differenze principali:

  • use: Più corto, può essere utilizzato condizionalmente (dentro if), disponibile solo in React 19+
  • useContext: Più verboso, non può essere utilizzato condizionalmente, disponibile da React 16.8+

Quando usare quale:

  • Usa useContext per compatibilità con versioni precedenti e quando non hai bisogno di accesso condizionale
  • Usa use se stai usando React 19+ e hai bisogno di accesso condizionale al context

Per la maggior parte dei casi, useContext è la scelta più sicura e compatibile.

Condividere Funzioni attraverso il Context

Oltre a condividere dati, si possono anche condividere funzioni attraverso il context. Questo permette ai componenti di aggiornare lo state senza dover ricevere funzioni come props:

function App() {
const [shoppingCart, setShoppingCart] = useState({
items: []
});
function handleAddItemToCart(id) {
setShoppingCart(prevCart => {
// Logica per aggiungere l'item al carrello
const existingItem = prevCart.items.find(item => item.id === id);
if (existingItem) {
return {
items: prevCart.items.map(item =>
item.id === id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
} else {
return {
items: [...prevCart.items, { id, quantity: 1 }]
};
}
});
}
const contextValue = {
items: shoppingCart.items,
addItemToCart: handleAddItemToCart
};
return (
<CartContext.Provider value={contextValue}>
<Header />
<Shop />
</CartContext.Provider>
);
}

Ora qualsiasi componente può chiamare addItemToCart senza dover riceverla come prop:

function Product({ id }) {
const cartCtx = useContext(CartContext);
return (
<button onClick={() => cartCtx.addItemToCart(id)}>
Add to Cart
</button>
);
}

Pattern: Context Provider Component

Quando si gestisce context complessi con molto state e logica, è una buona pratica creare un componente Provider separato che gestisce tutto lo state e la logica del context. Questo mantiene il componente App pulito e permette di avere più context senza ingombrare un singolo componente.

Esempio completo: CartContextProvider
store/shopping-cart-context.jsx
import { createContext, useState } from 'react';
export const CartContext = createContext({
items: [],
addItemToCart: () => {},
updateItemQuantity: () => {},
removeItem: () => {}
});
export default function CartContextProvider({ children }) {
const [shoppingCart, setShoppingCart] = useState({
items: []
});
function handleAddItemToCart(id) {
setShoppingCart(prevCart => {
const existingItem = prevCart.items.find(item => item.id === id);
if (existingItem) {
return {
items: prevCart.items.map(item =>
item.id === id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
} else {
return {
items: [...prevCart.items, { id, quantity: 1 }]
};
}
});
}
function handleUpdateItemQuantity(id, amount) {
setShoppingCart(prevCart => ({
items: prevCart.items.map(item =>
item.id === id
? { ...item, quantity: item.quantity + amount }
: item
).filter(item => item.quantity > 0)
}));
}
function handleRemoveItem(id) {
setShoppingCart(prevCart => ({
items: prevCart.items.filter(item => item.id !== id)
}));
}
const contextValue = {
items: shoppingCart.items,
addItemToCart: handleAddItemToCart,
updateItemQuantity: handleUpdateItemQuantity,
removeItem: handleRemoveItem
};
return (
<CartContext.Provider value={contextValue}>
{children}
</CartContext.Provider>
);
}

Poi in App.jsx:

import CartContextProvider from './store/shopping-cart-context';
function App() {
return (
<CartContextProvider>
<Header />
<Shop />
</CartContextProvider>
);
}

Questo pattern mantiene tutto lo state e la logica del carrello in un unico posto, rendendo il codice più organizzato e più facile da mantenere.

Re-render quando il Context Cambia

È importante capire che quando il valore del context cambia, tutti i componenti che utilizzano useContext per accedere a quel context verranno ri-renderizzati automaticamente da React. Questo è il comportamento desiderato, perché permette all’interfaccia utente di aggiornarsi quando i dati cambiano.

React ri-esegue la funzione del componente quando:

  • Lo state interno del componente cambia
  • Le props del componente cambiano
  • Il valore del context a cui il componente è connesso cambia

Questo garantisce che l’interfaccia utente sia sempre sincronizzata con i dati.

Consumer Component (Approccio Alternativo)

Oltre a useContext, esiste un approccio alternativo per accedere al context utilizzando il componente Consumer. Questo approccio è meno comune e più verboso, ma può essere utile in alcuni casi specifici o quando si lavora con codebase più vecchie.

Esempio con Consumer component
import { CartContext } from './store/shopping-cart-context';
function Cart() {
return (
<CartContext.Consumer>
{(cartCtx) => (
<div>
{cartCtx.items.length === 0 ? (
<p>No items in cart</p>
) : (
<ul>
{cartCtx.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)}
</div>
)}
</CartContext.Consumer>
);
}

Il Consumer richiede una funzione come child (render prop pattern) che riceve il valore del context come parametro. Questo approccio è più verboso e meno leggibile rispetto a useContext, quindi si consiglia di usare useContext nella maggior parte dei casi.

useReducer: Gestire State Complesso

Quando si gestisce state complesso con molte proprietà e logica di aggiornamento, useState può diventare difficile da gestire. useReducer è un’alternativa che utilizza il pattern del reducer per organizzare la logica di aggiornamento.

Sintassi Base

import { useReducer } from 'react';
const [state, dispatch] = useReducer(reducer, initialState);
  • reducer: Funzione che specifica come lo state viene aggiornato
  • initialState: Valore iniziale dello state
  • state: State corrente
  • dispatch: Funzione per inviare azioni al reducer

Esempio Base: Counter

import { useReducer } from 'react';
// Reducer definito FUORI dal componente
function reducer(state, action) {
if (action.type === 'incremented_age') {
return { age: state.age + 1 };
}
throw Error('Unknown action.');
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
return (
<>
<button onClick={() => dispatch({ type: 'incremented_age' })}>
Increment age
</button>
<p>Hello! You are {state.age}.</p>
</>
);
}

Come funziona: dispatch({ type: 'incremented_age' }) invia un’azione al reducer, che calcola il nuovo state. React aggiorna lo state e ri-renderizza il componente.

Scrivere un Reducer

Il reducer è una funzione pura che riceve (state, action) e restituisce il nuovo state. Usa switch per gestire diversi tipi di azioni:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
return {
...state,
name: action.nextName
};
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}

Regole importanti:

  • Deve essere definito fuori dal componente
  • Deve essere puro: non modifica lo state esistente, restituisce sempre un nuovo oggetto
  • Usa ...state per copiare tutte le proprietà esistenti
// ❌ SBAGLIATO: modifica lo state esistente
function reducer(state, action) {
state.age++; // Non fare questo!
return state;
}
// ✅ CORRETTO: restituisce un nuovo oggetto
function reducer(state, action) {
return { ...state, age: state.age + 1 };
}

Dispatchare Azioni

dispatch({ type: 'ACTION_TYPE', ...altriDati });

Importante: Dopo dispatch, lo state non cambia immediatamente. Si aggiorna nel prossimo render (comportamento identico a useState).

Struttura delle Azioni

Le azioni hanno una proprietà type (obbligatoria) e possono avere altre proprietà per i dati:

// Azione semplice
dispatch({ type: 'RESET_CART' });
// Azione con payload (convenzione comune)
dispatch({ type: 'ADD_ITEM', payload: { id: 1, quantity: 2 } });
// Azione con proprietà specifiche
dispatch({ type: 'UPDATE_ITEM', productId: 1, amount: 1 });

Quando Usare useReducer vs useState

useReducer quando:

  • State complesso (oggetti/array con molte proprietà)
  • Logiche di aggiornamento complesse
  • Molte funzioni di aggiornamento simili

useState quando:

  • State semplice (numeri, stringhe, booleani)
  • Logiche di aggiornamento semplici
  • Poche funzioni di aggiornamento

Esempio: Todo List

function todosReducer(state, action) {
switch (action.type) {
case 'added':
return [...state, { id: action.id, text: action.text, done: false }];
case 'changed':
return state.map(todo =>
todo.id === action.todo.id ? action.todo : todo
);
case 'deleted':
return state.filter(todo => todo.id !== action.id);
default:
throw Error('Unknown action: ' + action.type);
}
}
function TodoList() {
const [todos, dispatch] = useReducer(todosReducer, []);
function handleAddTodo(text) {
dispatch({ type: 'added', id: nextId++, text });
}
function handleDeleteTodo(id) {
dispatch({ type: 'deleted', id });
}
// ... resto del componente
}

Combinare Context API e useReducer

Pattern comune: usare useReducer all’interno di un Context Provider per gestire state complesso e eliminare prop drilling.

store/shopping-cart-context.jsx
import { createContext, useReducer } from 'react';
export const CartContext = createContext({
items: [],
addItemToCart: () => {},
removeItem: () => {}
});
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find(item => item.id === action.payload);
if (existingItem) {
return {
items: state.items.map(item =>
item.id === action.payload
? { ...item, quantity: item.quantity + 1 }
: item
)
};
}
return {
items: [...state.items, { id: action.payload, quantity: 1 }]
};
}
case 'REMOVE_ITEM':
return {
items: state.items.filter(item => item.id !== action.payload)
};
default:
return state;
}
}
export default function CartContextProvider({ children }) {
const [shoppingCart, dispatch] = useReducer(cartReducer, { items: [] });
const contextValue = {
items: shoppingCart.items,
addItemToCart: (id) => dispatch({ type: 'ADD_ITEM', payload: id }),
removeItem: (id) => dispatch({ type: 'REMOVE_ITEM', payload: id })
};
return (
<CartContext.Provider value={contextValue}>
{children}
</CartContext.Provider>
);
}

Uso:

App.jsx
<CartContextProvider>
<Header />
<Shop />
</CartContextProvider>
// Product.jsx
function Product({ id }) {
const cartCtx = useContext(CartContext);
return <button onClick={() => cartCtx.addItemToCart(id)}>Add to Cart</button>;
}

Note Importanti

Posizionamento del reducer: Deve essere definito fuori dal componente perché è una funzione pura che non ha bisogno di accesso a props o valori del componente, ed evita ricreazioni ad ogni render.

// ✅ CORRETTO: fuori dal componente
function reducer(state, action) { /* ... */ }
function Component() {
const [state, dispatch] = useReducer(reducer, initialState);
}
// ❌ SBAGLIATO: dentro il componente
function Component() {
function reducer(state, action) { /* ... */ }
const [state, dispatch] = useReducer(reducer, initialState);
}

Initializer function (terzo parametro): Utile quando lo state iniziale richiede calcoli costosi. Passa la funzione come terzo argomento:

function createInitialState(username) {
// Calcoli costosi
return { todos: /* ... */ };
}
// ❌ Chiamata ad ogni render
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ✅ Chiamata solo una volta
const [state, dispatch] = useReducer(reducer, username, createInitialState);

Problemi comuni:

  • State non si aggiorna: Assicurati di restituire un nuovo oggetto, non modificare lo state esistente
  • State diventa undefined: Usa ...state per copiare tutte le proprietà esistenti
  • “Too many re-renders”: Non dispatchare durante il render, solo in event handlers
  • Reducer eseguito due volte: Normale in Strict Mode (solo sviluppo), non causa problemi se il reducer è puro

Considerazioni sulle Performance

Quando si usa la Context API, qualsiasi cambiamento nel valore del context causa il re-render di tutti i componenti che consumano quel context, anche se utilizzano solo una parte del valore.

Ottimizzazione: Separare Context per dati che cambiano a frequenze diverse

Se si hanno dati che cambiano a frequenze diverse, si può considerare di separarli in context diversi:

// Context per dati che cambiano spesso
const CartContext = createContext();
// Context per dati che cambiano raramente
const UserContext = createContext();
function App() {
return (
<UserContext.Provider value={userData}>
<CartContext.Provider value={cartData}>
<Header />
<Shop />
</CartContext.Provider>
</UserContext.Provider>
);
}

In questo modo, quando cambiano i dati del carrello, solo i componenti che usano CartContext vengono ri-renderizzati, non quelli che usano solo UserContext.

Una convenzione comune è organizzare i context in una cartella store o contexts, con ogni file che contiene la definizione del context, il componente Provider e eventuali reducer.

Context API: Elimina il prop drilling creando un context con createContext, fornendolo con un Provider, e accedendovi con useContext.

useReducer: Gestisce state complesso con il pattern reducer:

  • Sintassi: const [state, dispatch] = useReducer(reducer, initialState)
  • Reducer: Funzione pura (state, action) => newState definita fuori dal componente
  • Dispatch: dispatch({ type: 'ACTION_TYPE', ...dati }) per aggiornare lo state
  • Regole: Reducer puro, restituisce sempre nuovo oggetto, usa ...state per copiare proprietà

La combinazione Context API + useReducer è un pattern comune per gestire state globale complesso senza prop drilling.

Continua la lettura

Leggi il prossimo capitolo: "Funzionamento Interno di React e Ottimizzazione delle Performance"

Continua a leggere