da Flux a Redux

da Flux a Redux

In uno dei miei ultimi post ho parlato di Flux: un pattern alternativo a MVC sviluppato dagli ingegneri di Facebook. In questo nuovo articolo analizzerò Redux, nuovo pattern ideato da Dan Abramov che da molti è considerato un’evoluzione di Flux. Per cogliere meglio le differenze tra i due pattern affronteremo in Redux lo stesso problema affrontato in Flux: una todo-list in un contesto React.

Dan Abramov: il creatore di Redux

Dan Abramov: il creatore di Redux

Why (not) Redux

Nello scorso post abbiamo identificato i problemi del Model View Controller prima di capire quali vantaggi poteva portare Flux. Ripetiamo lo stesso esercizio, questa volta però mettiamo sotto la lente Flux, prima di passare ai vantaggi di Redux. Il primo problema di Flux è sicuramente la verbosità. Ogni progetto Flux comporta un numero di righe molto più alto rispetto alla stessa applicazione scritta in un classico AngularJS. L’altro problema che ho notato è il non essere abbastanza stringente: Flux lascia allo sviluppatore piena libertà sul numero di dispatcher e store da implementare: questa cosa se non tenuta sotto controllo può portare a risultati disastrosi.

Durante il post vedremo come Redux risolva entrambi i problemi riscontrati con Flux.

I tre principi

Redux si basa su tre principi cardine. Analizziamoli insieme nel dettaglio:

Singola sorgente di verità

Questo primo principio è il più disruptive, in pratica afferma che lo stato della vostra applicazione debba essere memorizzato in unico oggetto centralizzato gestito da un unico store. Di solito lo stato di un’applicazione web è la somma delle sue parti e vale la regola dividi et impera: ogni parte gestisce in maniera indipendente il suo stato non tenendo conto dello stato delle altre parti.

Lo stato è in sola lettura

Proprio come in Flux lo stato può essere modificato solo tramite action, degli oggetti che descrivono al sistema cosa è appena accaduto.

I cambiamenti sono fatti solo attraverso funzioni pure

Una funzione pura è in pratica una funzione che non modifica i parametri che riceve in ingresso. In Redux ad ogni action corrisponde un reducer: una funzione (pura per l’appunto) che prende in ingresso lo stato dell’applicazione e una action e ritorna una copia dello stato modificato rispetto a quello precedente.

Let’s code

Passiamo ora al codice: verranno mostrate solo le parti di codice modificate alla precedente versione React+Flux. Iniziamo con il codice delle action:

var add = function(text) {
	return {
		actionType: "addTodo",
		text: text
	};
};

var update = function(index, text) {
	return {
		actionType: "updateTodo",
		text: text,
		index: index
	};
};

var deleteTodo = function(index) {
	return {
		actionType: "deleteTodo",
		index: index
	};
};

var deleteAll = function(){
	return {
		actionType: "deleteAll"
	};
};

export default {
	add: add,
	update: update,
	delete:deleteTodo,
	deleteAll:deleteAll
};

La grande differenza rispetto a Flux è l’assenza del dispatcher, le action in questo caso esportano semplicemente dei literal Javascript. Passiamo ora aireducer, le funzioni che conterranno la business logic della nostra applicazione.

const initialState = {
	todos: []
};

function addTodo(state,text){
	var toReturn = Object.assign({},state,{
		todos:[...state.todos]
	});

	toReturn.todos.push(text);

	return toReturn;
};

function updateTodo(state,index,text){
	var toReturn = Object.assign({},state,{
		todos:[...state.todos]
	});

	toReturn.todos[index] = text;

	return toReturn;	
};

function deleteTodo(state,index){
	var toReturn = Object.assign({},state,{
		todos:[...state.todos]
	});

	toReturn.todos.splice(index, 1);

	return toReturn;
};

export default function todoApp(state = initialState, action) {
	switch (action.actionType) {
		case "addTodo":
			return addTodo(state,action.text);
		case "updateTodo":
			return updateTodo(state,action.index,action.text)
		case "deleteTodo":
			return deleteTodo(state,action.index);
		case "deleteAll":
			return initialState;
		default:
			return state;
	};
}

Come accennato in precedenza queste funzioni non modificano mai lo stato che arriva in ingresso ma lavorano sempre su una copia (notate l’utilizzo di Object.assign()).

L’unico store presente all’interno del sistema è un semplice wrapper dei reducer che abbiamo appena visto. Questa istanza viene generata al boot dell’applicazione e resa disponibile al resto dell’applicazione tramite un componente chiamato Provider.

import React from "react";
import { Provider } from 'react-redux';
import Reducers from 'src/Reducers';
import { createStore } from 'redux';

let store = createStore(Reducers);

React.render(<Provider store={store}>{() => <App/>}</Provider>,  document.getElementById('wrapper'));

Il dispatcher in Redux viene fuso con lo store, esso diventa di fatto inutile in un pattern che prevede un unico store. Quando utilizzato con React il dispatching delle action viene delegato ai componenti. Questi ultimi sono divisi divisi in smart, cioè che conoscono la presenza di Redux, e dumb che invece sono agnostici rispetto all’utilizzo del framework.

Vediamo come configurare un componente smart prendendo come esempio la lista dei todo:

import React from "react";
import Actions from "src/Actions";
import { connect } from 'react-redux';

class List extends React.Component{
	
	render() {

		var items = this.props.data.map(function(todo,i) {
			return (
				<tr key={i}>
					<td>
						<a className="btn btn-success" href={'#/detail/' + i}>Edit</a>
						<button type="button" 
							className="btn btn-danger" 
							onClick={this.props.dispatch.bind(this,Actions.delete(i))}>
								Elimina
						</button>
					</td>
					<td>{todo}</td>
				</tr>
			);
		},this);

		var emptyRow = (<tr><td colSpan="2">Niente da fare</td></tr>);

	    return (
	    	<div>
		    	<a className="btn btn-info" href="#/detail">Add</a>
		    	<table className="table table-striped">
					<thead>
						<tr>
							<th colSpan="2">Todo</th>
						</tr>
					</thead>
					<tbody>
						{this.props.data.length ? items : emptyRow}
					</tbody>
					<tfoot>
					    <tr className="text-center">
					      <td colSpan="2">Total Todos : {this.props.data.length}</td>
					    </tr>
  					</tfoot>
				</table>
			</div>
	    );
	}
}

var select = (state) => {
	return {
		data:state.todos
	};
};

export default connect(select)(List);

Per rendere un componente smart dobbiamo utilizzare la funzione connect. Questa funzione oltre al componente stesso accetta in ingresso una funzione, di solito chiamata select.
Lo scopo di questa funzione è prendere lo stato dell’applicazione e ne restituirne una parte di esso, che sarà disponibile al componente sotto forma di props non modificabili. È importante notare che anche se lo stato dell’applicazione è unico ogni componente vede solo ed esclusivamente le parti di cui ha bisogno. A questo punto i componenti smart possono invocare una action semplicemente utilizzando il metodo dispatch, il quale non è disponibile per i componenti dumb.

Conclusioni

Come abbiamo visto Redux risolve alcuni dei problemi legati a Flux, indubbiamente è meno verboso e lascia meno margine di manovra agli sviluppatori (abbiamo un unico store). Questa ultima feature fa sì che la manutenibilità del codice sia a livelli altissimi. Credo però che il vero valore di questo pattern sia quello di portare ad un abbassamento del debito tecnico: se guardate le actions e i reducers, che rappresentano il cuore della business logic della nostra applicazione, sono assolutamente privi di dipendenze. Potreste passare da React ad Angular2 al prossimo framework frontend senza modificare una virgola del codice scritto. I più curiosi possono trovare il codice del progetto su GitHub, sul branch chiamato “redux”. Alla prossima.

Francesco Strazzullo

Faccio il Front-end developer per e-xtrategy dove mi occupo di applicazioni AngularJS e mobile. In passato ho lavorato principalmente con applicazioni con stack Spring+Hibernate+JSF 2.X+Primefaces. Sono tra i collaboratori del progetto Primefaces Extensions: suite di componenti aggiuntivi ufficialmente riconosciuta da Primefaces. Sono anche uno dei fondatori del progetto MaterialPrime: una libreria JSF che segue le direttive del Material Design di Google.