C'è un pattern che vedo in quasi ogni team a cui mi unisco: uno sviluppatore scopre un'astrazione potente — Redux, MobX, XState, un event bus custom — e la installa «perché prima o poi ci servirà.» Sei mesi dopo, il team mantiene 200 righe di boilerplate per uno stato che potrebbe vivere in una chiamata useState. Nessuno ricorda perché lo store esiste. Nessuno osa rimuoverlo.
Questo è l'opposto del buon engineering. Il buon engineering è scegliere lo strumento più semplice che risolve il problema reale. KISS — Keep It Simple, Stupid — non è un acronimo carino. È il principio più sottovalutato nello sviluppo software, specialmente nell'ecosistema React dove il panorama degli strumenti incoraggia attivamente il sovra-ingegnerizzare.
La trappola dell'ingegnosità
L'effetto Dunning-Kruger colpisce più forte nella gestione dello stato. Uno sviluppatore che ha appena imparato Redux pensa che ogni pezzo di stato appartenga a uno store globale. Crea action type, reducer, selector, middleware — per un filtro di ricerca usato da due componenti su una pagina. Sembra produttivo. Sembra professionale. Non è nessuna delle due cose.
Il codice ingegnoso è un debito. Ogni astrazione che introduci è un concetto che il tuo team deve imparare, mantenere e debuggare. Redux aggiunge action, reducer, selector, middleware e un layer di configurazione dello store. Sono cinque nuovi concetti per la gestione dello stato — in un framework che ha già una gestione dello stato integrata. Stai pagando una tassa di complessità su ogni feature che costruisci, e il ritorno su quell'investimento è raramente positivo.
Sono entrato in progetti ad AXA e in una grande banca svizzera dove la rimozione completa di Redux — sostituito con useState e useContext — ha ridotto il codice di gestione dello stato del 70% e ha reso le feature più veloci da costruire. Non perché Redux sia un cattivo software. Perché era lo strumento sbagliato per ciò di cui quelle applicazioni avevano realmente bisogno.
// Redux approach for a simple filter + list UI
// store/filtersSlice.ts
const filtersSlice = createSlice({
name: 'filters',
initialState: { search: '', category: 'all' },
reducers: {
setSearch: (state, action) => { state.search = action.payload; },
setCategory: (state, action) => { state.category = action.payload; },
resetFilters: (state) => { state.search = ''; state.category = 'all'; },
},
});
// store/index.ts — configure store, combine reducers
// store/hooks.ts — typed useSelector, useDispatch
// components/Filters.tsx — useSelector + useDispatch
// components/List.tsx — useSelector to read filters
// 5 files, 80+ lines of boilerplate for two strings.
// The same thing with useState:
function ProductPage() {
const [search, setSearch] = useState('');
const [category, setCategory] = useState('all');
return (
<>
<Filters search={search} category={category}
onSearch={setSearch} onCategory={setCategory} />
<ProductList search={search} category={category} />
</>
);
}
// 1 file. 10 lines. Same result. Easier to read, easier to change.React State basta (la maggior parte delle volte)
React ha due primitive di stato integrate: useState per lo stato locale e useContext per lo stato condiviso. Insieme, coprono la grande maggioranza delle applicazioni reali. Prima di cercare una libreria esterna, chiediti: posso risolvere questo con quello che React mi dà già?
useState gestisce lo stato locale al componente — input di form, toggle, flag di caricamento, tab selezionati. useContext gestisce lo stato di cui più componenti hanno bisogno — l'utente corrente, un carrello, una preferenza di tema. Combinali e hai una soluzione di gestione dello stato con zero dipendenze, zero boilerplate, e compresa da ogni sviluppatore React sul pianeta.
L'obiezione che sento è sempre sulla «scalabilità». Ma ho lavorato su grandi applicazioni a Vontobel e Migros — piattaforme di trading in tempo reale, e-commerce multi-brand — e Context + useState hanno scalato bene. La chiave è posizionare i provider strategicamente: non un unico context gigante alla radice, ma context focalizzati per ogni dominio. Un CartProvider avvolge il checkout. Un ThemeProvider avvolge l'app. Un DashboardProvider avvolge la pagina dashboard. Ognuno è piccolo, focalizzato e facile da comprendere.
// When multiple components need the same data and
// props drilling goes beyond 3 levels — use Context.
interface CartContext {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
total: number;
}
const CartCtx = createContext<CartContext | null>(null);
function useCart() {
const ctx = useContext(CartCtx);
if (!ctx) throw new Error('useCart must be used within CartProvider');
return ctx;
}
// Smart component: owns the state, provides it via context
function CartProvider({ children }: { children: ReactNode }) {
const [items, setItems] = useState<CartItem[]>([]);
const addItem = (item: CartItem) =>
setItems((prev) => [...prev, item]);
const removeItem = (id: string) =>
setItems((prev) => prev.filter((i) => i.id !== id));
const total = items.reduce((sum, i) => sum + i.price, 0);
return (
<CartCtx.Provider value={{ items, addItem, removeItem, total }}>
{children}
</CartCtx.Provider>
);
}
// Any descendant can read cart state — no prop chains
function CartBadge() {
const { items } = useCart();
return <span>{items.length}</span>;
}
function CartTotal() {
const { total } = useCart();
return <span>{formatCurrency(total)}</span>;
}L'albero dei componenti è il tuo state manager
Questo si collega direttamente alla separazione tra smart e dumb component di cui ho già scritto. L'albero dei componenti non è solo una gerarchia di rendering — è un sistema di distribuzione dello stato. Gli smart component in alto possiedono lo stato e lo passano in basso. I dumb component sotto ricevono i dati e li renderizzano.
Quando metti lo stato nel posto giusto — l'antenato comune più basso dei componenti che ne hanno bisogno — non hai bisogno di uno store. L'albero React fa la distribuzione gratuitamente. Un componente DashboardPage che recupera i dati e li passa a StatsGrid e ChartSection fa esattamente ciò che farebbe Redux, ma senza cerimonie.
Questo intendo con «il codice più stupido è il più intelligente». Uno smart component con useState e tre props passate ai figli è noioso. È ovvio. Un nuovo sviluppatore può leggerlo in 30 secondi. È esattamente quello che vuoi. La riscrittura del carrello Next.js che ho descritto in Dimentica le best practice seguiva lo stesso principio — abbiamo sostituito la complessità del rendering lato server con semplice stato lato client, e tutto è migliorato.
// The component tree IS your state architecture.
// Place state in the lowest common ancestor that needs it.
// ❌ Over-engineered: global store for page-local state
// Redux store → useSelector in Header, Sidebar, Content
// Now every component is coupled to a global store shape
// and re-renders on any store change unless you memoize everything.
// ✅ Simple: smart component owns the state, passes it down
function DashboardPage() {
const [period, setPeriod] = useState<'week' | 'month' | 'year'>('month');
const [data, setData] = useState<DashboardData | null>(null);
useEffect(() => {
fetchDashboard(period).then(setData);
}, [period]);
return (
<main>
{/* 1 level deep — just pass it */}
<PeriodSelector period={period} onChange={setPeriod} />
<StatsGrid data={data} />
<ChartSection data={data} period={period} />
</main>
);
}
// PeriodSelector, StatsGrid, ChartSection are all dumb.
// They receive data, render UI, report interactions.
// No store subscription. No selector. No dispatch.
// Change the data shape? Update one smart component.Props, Context o entrambi
Impongo una regola semplice nei miei team: il props drilling va bene fino a 3 livelli di profondità. Oltre, usa Context. Non è arbitrario — è il punto in cui passare la stessa prop attraverso componenti intermedi che non la usano inizia a danneggiare la leggibilità e crea accoppiamento inutile.
Le props sono il default. Sono esplicite, tracciabili e type-safe. Quando vedi le props di un componente, sai esattamente di cosa ha bisogno. Quella trasparenza è preziosa — non rinunciarci per comodità.
Context è per quando le props creano catene. Quando un componente Layout deve accettare una prop user solo per passarla a un componente Navigation che la passa a un UserMenu — è rumore. Nessuno di quei componenti intermedi si interessa dell'utente. Context ti permette di saltare la catena e leggere il valore direttamente dove serve.
Il punto ottimale è mescolare entrambi. Usa Context per le preoccupazioni trasversali di cui molti componenti hanno bisogno (utente, tema, carrello, locale). Usa le props per i dati che fluiscono attraverso una chiara relazione genitore-figlio. E non usare nessuno dei due per lo stato che appartiene a un singolo componente — per quello c'è useState.
// Props drilling is fine — up to a point.
// My rule: max 3 levels deep. Beyond that, use Context.
// ✅ Fine — 2 levels
<Page>
<Section filters={filters}>
<FilterBar filters={filters} onChange={setFilters} />
</Section>
</Page>
// ❌ Too deep — 5 levels of threading the same prop
<Page>
<Layout user={user}>
<Sidebar user={user}>
<Navigation user={user}>
<UserMenu user={user}> {/* enough. */}
<Avatar user={user} />
</UserMenu>
</Navigation>
</Sidebar>
</Layout>
</Page>
// ✅ Context cuts the chain where it matters
<UserProvider user={user}>
<Layout>
<Sidebar>
<Navigation>
<UserMenu /> {/* useUser() — reads from context */}
</Navigation>
</Sidebar>
</Layout>
</UserProvider>
// The middle components (Layout, Sidebar, Navigation)
// no longer need to know about 'user' at all.
// They just render their children. Clean.Ridondanza anziché astrazione
Questo articolo è il compagno pratico di Perché YAGNI batte DRY. Lo stesso principio si applica alla gestione dello stato: un po' di ridondanza è meno costosa dell'astrazione sbagliata. Due componenti che gestiscono ciascuno il proprio stato locale con chiamate useState simili sono più facili da mantenere di due componenti che condividono una slice di store globale progettata per «evitare la duplicazione».
La duplicazione nello stato è visibile e locale. Puoi vederla, e se i due stati devono divergere (di solito lo fanno), cambi semplicemente uno senza toccare l'altro. Uno store condiviso crea accoppiamento invisibile — cambia la forma per un consumer e rompi l'altro. Ho visto team passare sprint interi a districare slice Redux condivise che «risparmiavano codice» ma creavano dipendenze tra feature non correlate.
I migliori team con cui ho lavorato scelgono il semplice come default. Prima useState. Se più componenti hanno bisogno dello stesso stato, sollevalo a un genitore comune e passalo in basso. Se la catena di props diventa troppo profonda, avvolgilo in un Context. Se — e solo se — hai genuinamente bisogno di time-travel debugging, state machine complesse o aggiornamenti ottimistici su molte entità, allora considera una libreria dedicata. Ma la maggior parte delle applicazioni non ci arriva mai. E quelle che ci arrivano di solito ne hanno bisogno in una parte dell'app, non ovunque.
Smettila di risolvere problemi che non hai. La soluzione più semplice che funziona è la migliore soluzione — e resta quasi sempre la migliore soluzione molto dopo che quella ingegnosa è diventata un incubo di manutenzione.

