Introduzione
Redux è un sistema di gestione dello state per dati cross-component o app-wide. Funziona come alternativa a React Context, offrendo un approccio strutturato per gestire state condiviso attraverso un unico store centrale. Questo articolo esplora come funziona Redux, il suo flusso di dati e come utilizzarlo con Redux Toolkit per semplificare la configurazione.
Tipi di State
Lo state in React può essere classificato in tre categorie principali: local state, cross-component state e app-wide state.
Il local state è gestito all’interno di un singolo componente con useState o useReducer. Esempi includono input utente o toggle di visibilità che interessano solo quel componente.
Lo state cross-component interessa più componenti. Ad esempio, uno stato che controlla la visibilità di un modal può essere gestito passando props attraverso la gerarchia dei componenti (prop drilling) o usando Context/Redux.
Lo app-wide state interessa l’intera applicazione. L’autenticazione utente è un esempio tipico: quando l’utente si autentica, molti componenti devono reagire a questo cambiamento.
Come Funziona Redux
Redux si basa su tre concetti fondamentali: store centrale, reducer e actions.
Store Centrale
Redux utilizza un unico store centrale per tutta l’applicazione. Questo store contiene tutto lo state cross-component e app-wide. Non si creano mai più store: c’è esattamente un store per applicazione.
Reducer
Il reducer è una funzione pura responsabile di aggiornare lo state. Riceve due parametri:
- Lo state corrente
- L’action dispatchata
Il reducer deve sempre restituire un nuovo oggetto state, mai mutare lo state esistente. È una funzione pura: stessi input producono sempre lo stesso output, senza side effects.
Actions
Le actions sono oggetti JavaScript semplici che descrivono quale operazione deve essere eseguita. Hanno sempre una proprietà type che identifica l’azione. I componenti dispatchano actions, che vengono inoltrate al reducer, che esegue l’operazione e produce un nuovo state.
Flusso di Dati
Il flusso in Redux segue questo pattern:
- I componenti si sottoscrivono allo store per ricevere aggiornamenti quando lo state cambia
- I componenti dispatchano actions per modificare lo state
- Le actions vengono inoltrate al reducer
- Il reducer produce un nuovo state basato sull’action
- Lo store viene aggiornato con il nuovo state
- I componenti sottoscritti vengono notificati e si ri-renderizzano
Esempio base di Redux (senza React)
// Import di Reduxconst redux = require('redux');
// Reducer functionconst counterReducer = (state = { counter: 0 }, action) => { if (action.type === 'increment') { return { counter: state.counter + 1 }; }
if (action.type === 'decrement') { return { counter: state.counter - 1 }; }
return state;};
// Creazione dello storeconst store = redux.createStore(counterReducer);
// Subscriber functionconst counterSubscriber = () => { const latestState = store.getState(); console.log(latestState);};
// Sottoscrizione ai cambiamentistore.subscribe(counterSubscriber);
// Dispatch di actionsstore.dispatch({ type: 'increment' });store.dispatch({ type: 'decrement' });In questo esempio:
counterReducergestisce lo state del countercreateStorecrea lo store passando il reducersubscriberegistra una funzione che viene eseguita ad ogni cambio di statedispatchinvia actions al reducer
Redux in React
Per utilizzare Redux in React, si installano due package: redux e react-redux. Il secondo semplifica l’integrazione tra Redux e React.
Provider
Il componente Provider di react-redux avvolge l’applicazione e fornisce lo store a tutti i componenti figli:
import { Provider } from 'react-redux';import store from './store/index';
function App() { return ( <Provider store={store}> <Counter /> </Provider> );}Solo i componenti dentro Provider (e i loro discendenti) possono accedere allo store.
useSelector
L’hook useSelector permette ai componenti di leggere dati dallo store. Accetta una funzione che riceve lo state e restituisce la porzione di state desiderata:
import { useSelector } from 'react-redux';
function Counter() { const counter = useSelector((state) => state.counter);
return <div>{counter}</div>;}useSelector configura automaticamente una sottoscrizione: quando lo state cambia, il componente si ri-renderizza. La sottoscrizione viene rimossa automaticamente quando il componente viene smontato.
useDispatch
L’hook useDispatch restituisce la funzione dispatch per inviare actions allo store:
import { useDispatch } from 'react-redux';
function Counter() { const dispatch = useDispatch();
const incrementHandler = () => { dispatch({ type: 'increment' }); };
return <button onClick={incrementHandler}>Increment</button>;}Esempio completo: Counter con Redux
import { createStore } from 'redux';
const initialState = { counter: 0 };
const counterReducer = (state = initialState, action) => { if (action.type === 'increment') { return { counter: state.counter + 1 }; } if (action.type === 'decrement') { return { counter: state.counter - 1 }; } return state;};
const store = createStore(counterReducer);
export default store;import { useSelector, useDispatch } from 'react-redux';
function Counter() { const counter = useSelector((state) => state.counter); const dispatch = useDispatch();
const incrementHandler = () => { dispatch({ type: 'increment' }); };
const decrementHandler = () => { dispatch({ type: 'decrement' }); };
return ( <div> <div>{counter}</div> <button onClick={incrementHandler}>Increment</button> <button onClick={decrementHandler}>Decrement</button> </div> );}Actions con Payload
Le actions possono trasportare dati aggiuntivi oltre al type. Questi dati vengono chiamati payload:
// Dispatch di action con payloaddispatch({ type: 'increase', amount: 5});
// Nel reducerconst counterReducer = (state = initialState, action) => { if (action.type === 'increase') { return { counter: state.counter + action.amount }; } // ...};Il nome della proprietà del payload è arbitrario (amount, value, data, ecc.), ma deve corrispondere tra il dispatch e l’accesso nel reducer.
State Immutabile
Il reducer non deve mai mutare lo state esistente. Deve sempre restituire un nuovo oggetto:
// ❌ SBAGLIATO: muta lo state esistenteconst counterReducer = (state = initialState, action) => { if (action.type === 'increment') { state.counter++; // Mutazione! return state; } return state;};
// ✅ CORRETTO: restituisce un nuovo oggettoconst counterReducer = (state = initialState, action) => { if (action.type === 'increment') { return { counter: state.counter + 1 }; } return state;};Quando si aggiorna una parte dello state, è necessario copiare tutte le altre proprietà:
const initialState = { counter: 0, showCounter: true};
const counterReducer = (state = initialState, action) => { if (action.type === 'increment') { return { counter: state.counter + 1, showCounter: state.showCounter // Copia le altre proprietà! }; } if (action.type === 'toggle') { return { counter: state.counter, // Copia le altre proprietà! showCounter: !state.showCounter }; } return state;};Redux sostituisce lo state, non lo merge. Se si omette una proprietà, quella proprietà viene persa.
Redux Toolkit
Redux Toolkit semplifica la configurazione e l’utilizzo di Redux, riducendo il boilerplate e prevenendo errori comuni.
createSlice
createSlice combina la definizione del reducer, delle actions e degli action creators in un’unico oggetto:
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({ name: 'counter', initialState: { counter: 0, showCounter: true }, reducers: { increment(state) { state.counter++; }, decrement(state) { state.counter--; }, increase(state, action) { state.counter = state.counter + action.payload; }, toggleCounter(state) { state.showCounter = !state.showCounter; } }});Con Redux Toolkit, è permesso “mutare” lo state nei reducer perché internamente viene usato Immer, che traduce il codice in operazioni immutabili. Il codice risulta più semplice da scrivere e leggere.
Action Creators Automatici
createSlice genera automaticamente gli action creators:
// Le actions sono disponibili su counterSlice.actionsexport const counterActions = counterSlice.actions;
// Utilizzo nei componentiimport { counterActions } from './store/index';
function Counter() { const dispatch = useDispatch();
const incrementHandler = () => { dispatch(counterActions.increment()); };
const increaseHandler = () => { dispatch(counterActions.increase(5)); // 5 viene messo in action.payload };}Gli action creators generati automaticamente evitano errori di typo negli identificatori delle actions.
configureStore
configureStore semplifica la creazione dello store e la combinazione di più reducer:
import { configureStore } from '@reduxjs/toolkit';import counterSlice from './counter-slice';import authSlice from './auth-slice';
const store = configureStore({ reducer: { counter: counterSlice.reducer, auth: authSlice.reducer }});Quando si usano più slice, lo state viene organizzato per chiave:
// Accesso allo stateconst counter = useSelector((state) => state.counter.counter);const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);Esempio completo con Redux Toolkit
import { createSlice } from '@reduxjs/toolkit';
const initialCounterState = { counter: 0, showCounter: true };
const counterSlice = createSlice({ name: 'counter', initialState: initialCounterState, reducers: { increment(state) { state.counter++; }, decrement(state) { state.counter--; }, increase(state, action) { state.counter = state.counter + action.payload; }, toggleCounter(state) { state.showCounter = !state.showCounter; } }});
export const counterActions = counterSlice.actions;export default counterSlice;import { createSlice } from '@reduxjs/toolkit';
const initialAuthState = { isAuthenticated: false };
const authSlice = createSlice({ name: 'auth', initialState: initialAuthState, reducers: { login(state) { state.isAuthenticated = true; }, logout(state) { state.isAuthenticated = false; } }});
export const authActions = authSlice.actions;export default authSlice;import { configureStore } from '@reduxjs/toolkit';import counterSlice from './counter-slice';import authSlice from './auth-slice';
const store = configureStore({ reducer: { counter: counterSlice.reducer, auth: authSlice.reducer }});
export default store;import { useSelector, useDispatch } from 'react-redux';import { counterActions } from './store/counter-slice';
function Counter() { const counter = useSelector((state) => state.counter.counter); const showCounter = useSelector((state) => state.counter.showCounter); const dispatch = useDispatch();
const incrementHandler = () => { dispatch(counterActions.increment()); };
const increaseHandler = () => { dispatch(counterActions.increase(5)); };
return ( <div> {showCounter && <div>{counter}</div>} <button onClick={incrementHandler}>Increment</button> <button onClick={increaseHandler}>Increase by 5</button> </div> );}Class-Based Components
Per i class-based components, react-redux fornisce la funzione connect invece degli hooks:
import { connect } from 'react-redux';import { counterActions } from './store/counter-slice';
class Counter extends Component { incrementHandler() { this.props.increment(); }
render() { return ( <div> <div>{this.props.counter}</div> <button onClick={this.incrementHandler.bind(this)}>Increment</button> </div> ); }}
// Mappa lo state Redux alle propsconst mapStateToProps = (state) => { return { counter: state.counter.counter };};
// Mappa le funzioni dispatch alle propsconst mapDispatchToProps = (dispatch) => { return { increment: () => dispatch(counterActions.increment()) };};
export default connect(mapStateToProps, mapDispatchToProps)(Counter);connect è un higher-order component che avvolge il componente e fornisce le props mappate dallo store.
Organizzazione del Codice
In applicazioni più grandi, è utile organizzare i slice in file separati:
store/ ├── index.js # Configurazione dello store ├── counter-slice.js # Slice per il counter └── auth-slice.js # Slice per l'autenticazioneOgni slice esporta le proprie actions e il reducer. Lo store principale importa e combina tutti i reducer.
Riepilogo
Redux gestisce state cross-component attraverso un store centrale unico. I componenti dispatchano actions che vengono processate da reducer che producono nuovo state. I componenti si sottoscrivono allo store per ricevere aggiornamenti.
Redux Toolkit semplifica l’uso di Redux con createSlice (combina reducer e action creators), configureStore (crea lo store combinando più reducer) e supporto per “mutazioni” sicure grazie a Immer.
Gli hook useSelector e useDispatch permettono ai componenti funzionali di accedere allo store. Per i class-based components si usa connect.
Il reducer deve sempre restituire un nuovo oggetto state, mai mutare lo state esistente. Con Redux Toolkit, le “mutazioni” sono permesse perché vengono tradotte automaticamente in operazioni immutabili.