Semplificare Redux con Redux Toolkit

Maico Orazio
weBeetle
Published in
7 min readApr 27, 2021
Semplificare Redux con Redux Toolkit
Semplificare Redux con Redux Toolkit

Negli ultimi mesi del 2018 il team di Redux ha introdotto un set di strumenti ufficiale, successivamente ribattezzato come Redux Toolkit. Redux Toolkit mira a semplificare Redux con una comoda astrazione della quantità di codice boilerplate di cui tanti sviluppatori si sono lamentati.

Una delle critiche, più popolari e durature, che ha ricevuto Redux nel tempo è la quantità di codice che aggiunge alle applicazioni che lo utilizzano, che molti ritengono non necessaria.

In questo articolo esploreremo Redux Toolkit fianco a fianco con il “classico” codice Redux, effettuando il refactoring dell’app mostrata nel precedente articolo e capiremo come risolve molti degli argomenti relativi al boilerplate e al codice non necessario.

Prima di iniziare a descrivere le API esposte da Redux Toolkit, è necessario installare lo strumento tramite npm o yarn:

npm install @reduxjs/toolkit
yarn add @reduxjs/toolkit

configureStore

Iniziamo con la creazione dello store. Abbiamo visto come creare lo store di Redux che richiede il reducer e i middleware e contiene tutto lo stato dell’applicazione:

import { createStore, applyMiddleware } from "redux";const middleware = [
/*YOUR CUSTOM MIDDLEWARES HERE*/
];
const initialState = {
todos: []
}
function rootReducer(state = initialState, action) {
return state;
}
const store = createStore(rootReducer, applyMiddleware(...middleware));

Possiamo anche creare più reducers per poi inglobare in unico root reducer da passare alla funzione createStore:

import { createStore, combineReducers, applyMiddleware } from "redux";const middleware = [
/*YOUR CUSTOM MIDDLEWARES HERE*/
];
const initialState = {
todos: []
}
function todoReducer(state = initialState, action) {
return state;
}
function loggedReducer(state = false, action) {
return state;
}
const rootReducer = combineReducers({
todos_data: todoReducer,
user_data: loggedReducer
});
const store = createStore(rootReducer, applyMiddleware(...middleware));

Con Redux Toolkit possiamo semplificare tutto ciò utilizzando la funzione configureStore:

import { configureStore } from '@reduxjs/toolkit';const initialState = {
todos: []
}
function loggedReducer(state = false, action) {
return state;
}
function todoReducer(state = initialState, action) {
return state;
}
const store = configureStore({
reducer: {
user_data: loggedReducer,
todos_data: todoReducer
}
});

La funzione configureStore è un’astrazione della funzione createStore che aggiunge alcune impostazioni predefinite alla configurazione dello store per una migliore esperienza di sviluppo: accetta un oggetto di configurazione attraverso il quale possiamo definire il root reducer e i middleware. Tra le impostazioni predefinite troviamo abilitato l’uso dell’estensione Redux DevTools , attivo di default, e redux-thunk, di cui parleremo in un successivo articolo.

Per impostazione predefinita, quando non viene definita alcuna proprietà middleware dell’oggetto di configurazione, configureStore aggiunge automaticamente alcuni middleware alla configurazione dello store di Redux; è necessario includerli tramite la funzione getDefaultMiddleware quando, invece, vogliamo fornire un array di middleware custom:

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';const middlewareCustom = [
// funzioni di middleware personalizzati
];
const initialState = {
todos: []
}
function todoReducer(state = initialState, action) {
return state;
}
const store = configureStore({
reducer: {
todos_data: todoReducer
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middlewareCustom),
devTools: true // estensione ReduxDevTools abilitata di default
});

Come riportato sulla documentazione, è preferibile utilizzare i metodi .concat(...)e .prepend(...) dell’array restituito MiddlewareArray invece dell'operatore spread, poiché quest'ultimo può perdere preziose informazioni sulla tipologia.

createAction

La successiva funzione implementata in Redux Toolkit è createAction. Come abbiamo visto nel primo articolo, è buona pratica in Redux avere gli action creators e le named constants per definire tutti i comportamenti dell’applicazione.

Con createAction definiamo entrambi con un’unica istruzione:

import { createAction } from '@reduxjs/toolkit'

const addTodo = createAction("ADD_TODO");

Ogni chiamata a createAction definisce una action creator pronta per essere chiamata dal metodo dispatch dello store di Redux. La funzione definisce anche un metodo toString() utilizzato per ritornare il tipo della action (action.type).

// store.dispatch(addTodo({id:1, title: "new todo"}))
// addTodo() -> { type: 'ADD_TODO' }
// addTodo({id:1, title: "new todo"}) -> { type: 'ADD_TODO', payload: {id:1, title: "new todo"} }
// addTodo.toString() -> 'ADD_TODO'
// console.log(addTodo) -> ADD_TODO

createReducer

Dopo gli action creators e le named constants, i reducers sono un altro punto dove viene riscritto la maggior parte del codice per l’utilizzo di Redux nelle nostre applicazioni.

Per convenzione, utilizziamo uno switch per la gestione dei tipi di actions:

// named constants
const ADD_TODO = "ADD_TODO";

// action creators
export function addTodo(payload) {
return {
type: ADD_TODO,
payload
}
}

const initialState = {
todos: []
}
function rootReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO:

//return {...state, todos: state.todos.concat(action.payload)};
return Object.assign({}, state, {
todos: state.todos.concat(action.payload.title)
});
default:
return state;

}
}

Come approfondito nel primo articolo, Redux richiede che il reducer sia una funzione pura, ovvero non debba infrangere il principio fondamentale di Redux: l’immutabilità. In sostanza, deve produrre lo stato successivo senza alterare quello iniziale. Avevamo risolto questo problema tramite due soluzioni:

// prima soluzione, spread sintax
return {...state, todos: state.todos.concat(action.payload)};
// seconda soluzione, Object.assign
return Object.assign({}, state, {
todos: state.todos.concat(action.payload.title)
});

Come semplifichiamo utilizzando la funzione createReducer di Redux Toolkit? createReducer utilizza internamente immer, che consente di scrivere una logica “mutativa”, in realtà il reducer riceve una copia dello stato corrente e traduce tutte le mutazioni con operazioni di copia, non alterando lo stato originale.

import { createAction, createReducer } from '@reduxjs/toolkit';const addTodo = createAction("ADD_TODO");const initialState = {
todos: []
}
const todoReducer = createReducer(initialState, {
addTodo: (state, action) => {
state.todos = state.todos.concat(action.payload.title)
}
});

Questa tipologia di notazione, oltre allo stato iniziale, accetta un oggetto in cui le chiavi sono i tipi di actions e i valori le funzioni che rappresentano i case del reducer nella sua versione “classica” di Redux. Si noti che non è necessario disporre di costanti per identificare la action o utilizzare alcun type.

createSlice

createSlice è una funzione di Redux Toolkit ideata per semplificare ancora di più quanto scritto finora,

import { configureStore, createAction, createReducer } from '@reduxjs/toolkit';const addTodo = createAction("ADD_TODO")const initialState = {
todos: []
}
const todoReducer = createReducer(initialState, {
[addTodo]: (state, action) => {
//return {...state, todos: state.todos.concat(action.payload)};
return Object.assign({}, state, {
todos: state.todos.concat(action.payload.title)
});
}
});

const middleware = [
// funzioni di middleware personalizzati
];
const store = configureStore({
reducer: {
user_data: loggedReducer,
todos_data: todoReducer
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware)
});

convogliando tutto in un unico posto: action creators, reducer e store:

import { configureStore, createSlice } from '@reduxjs/toolkit';const initialState = {
todos: []
}
const todoReducerSlice = createSlice({
name: "todo",
initialState: initialState,
reducers: {
addTodo: (state, action) => {
state.todos = state.todos.concat(action.payload.title)
}
}
});
const middleware = [
// funzioni di middleware personalizzati
];

// actions per il dispatch
const { addTodo } = todoReducerSlice.actions;

// reducer
const todoReducer = todoReducerSlice.reducer;

const store = configureStore({
reducer: {
todo: todoReducerSlice
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware)
});

Internamente utilizza createAction e createReducer, accetta un oggetto javascript di configurazione:

  • nome “slice”, utilizzato come identificativo della slice e anche come prefisso dei tipi di actions generati (es.: todos/ADD_TODO);
  • stato iniziale del reducer;
  • oggetto reducers , dove le chiavi sono usati come type delle actions e le funzioni sono i case per la gestione dei relativi tipi di actions.

Fornisce, generandoli automaticamente, gli action creators, i tipi di actions e i reducers:

// actions per il dispatch
const { addTodo } = todoReducerSlice.actions;

// reducer
const todoReducer = todoReducerSlice.reducer;

useSelector e useDispatch

Tutto ciò che resta da fare è utilizzare i nostri hooks React-Redux sia per visualizzare che per inviare actions allo store di Redux.

Importiamo le actions del reducer che abbiamo creato tramite createSlice, e utilizziamo gli hooks useSelector per acquisire lo stato e useDispatch per inviare le actions.

Nel componente List utilizziamo useSelector per acquisire lo stato che esiste nello store di Redux sotto il nome di “todo” e visualizzare l’elenco delle attività.

import { useSelector } from "react-redux";const List = () => {
const todos = useSelector(state => state.todo.todos);
return (
<ul id="list-todos">
{todos.map((title,i) => (
<li key={i}><strong>{title}</strong></li>
))}
</ul>
)
}
export default List;

Nel componente Form, importiamo le actions del reducer, che abbiamo creato tramite createSlice, e utilizziamo useDispatch per inviare l’action addTodo che aggiunge una nuova attività allo store di Redux:

import { useState } from "react";
import { useDispatch } from "react-redux";
import { addTodo } from "../store/todoSlice";
const Form = () => {
const dispatch = useDispatch();
const [value, setValue] = useState("");
const onChangeText = (e) => {
setValue(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (value.trim() === "") return;
dispatch(addTodo({ title: value }));
setValue("");
}
return (
<form onSubmit={handleSubmit}>
<input type="text" name="todo-title" value={value} placeholder="titolo attività" id="todo-title" onChange={onChangeText} />
<button type="submit">aggiungi</button>
</form>
)
}
export default Form;

Conclusioni

Nella sua forma “classica”, utilizzare Redux significa scrivere tanto codice in diversi blocchi ognuno con la propria funzionalità. Questa è la principale motivazione per cui molte persone sono contrarie al suo utilizzo ma, con Redux Toolkit ora è più facile scrivere la logica Redux; la giusta direzione per una delle librerie di gestione dello stato lato client più mature in circolazione.

Infine, proprio come Redux, Redux Toolkit non è stato creato solo per React e possiamo utilizzarlo con qualsiasi altro framework JavaScript.

Quanto scritto in questo articolo rappresenta solo un punto di partenza. Per sapere di più sulle altre configurazioni di Redux Toolkit è possibile fare riferimento alla loro documentazione.

Puoi trovare tutto il codice di questo tutorial qui sul mio repository GitHub.

--

--

Maico Orazio
weBeetle

Senior Web Application Developer. I'm a software engineer, a passionate coder, and a web developer. I am a fan of technology. #php #symfony #javascript #reactjs