Sviluppare applicazioni con React e Flux

Sviluppare applicazioni con React e Flux

Nel mio precedente post ho introdotto React: ultimo framework front-end creato da Facebook. In questo nuovo post tratterò Flux: un pattern architetturale che si pone come sostituto del ben più noto Model View Controller. Flux è il pattern che regge il front-end web di Facebook, ed è quindi il pattern di riferimento per applicazioni enterprise sviluppate tramite React. Nulla vieta però di utilizzarlo con altre tecnologie. Lo schema di un’applicazione React è il seguente:

Flux

Fonte: https://facebook.github.io/

Come vedete la più grande differenza rispetto al MVC è che le frecce vanno in un’unica direzione, mentre nel MVC la comunicazione è bidirezionale e questo ha portato ad una serie di variazioni più o meno ufficiali. Basti pensare che AngularJS si proclama come framework MVW (Model View Whatever).

MVC

MVC nelle sue varie “forme”

Elementi di un’applicazione Flux

Analizziamo ora insieme gli elementi che compongono un’applicazione Flux:

Action: Un evento scatenato nel sistema che ha lo scopo di modificarne lo stato. Escluse le action di bootstrap, sono principalmente azioni scatenate dall’utente.

Dispatcher: Un Event Bus che fa girare gli eventi scatenati dalle Action all’interno dell’applicazione.

Store: Gli oggetti che contengono la business logic della vostra applicazione. Sono gli oggetti che in un classico MVC formerebbero la parte Model.

View: In questo caso la View è rappresentata dai componenti React.

Volendo quindi descrivere a parole un ciclo completo di Flux potremmo dire che:

Una Action scatena un evento che il Dispatcher rende disponibile nel sistema, uno o più Store sono in ascolto di questi eventi cambiando opportunamente il loro stato. La View infine sincronizza il suo stato con quello degli Store e rimane quindi in ascolto degli input dell’utente per poter scatenare delle nuove Action.

Let’s code

Analizziamo ora il codice di una todo-list scritta usando Flux, React ed Ecmascript 6, iniziamo subito con il componente più semplice, il Dispatcher:

import Flux from "flux";

var Dispatcher = Flux.Dispatcher;

export default new Dispatcher();

Come vedete possiamo semplicemente utilizzare il Dispatcher di default di Flux. Passiamo ora alla Action:

import Dispatcher from "src/Dispatcher";

var add = function(text) {
	Dispatcher.dispatch({
		actionType: "addTodo",
		text: text
	});
};

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

var deleteTodo = function(index) {
	Dispatcher.dispatch({
		actionType: "deleteTodo",
		index: index
	});
};

var deleteAll = function(){
	Dispatcher.dispatch({
		actionType: "deleteAll"
	});
};

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

In questo semplice caso le Action sono semplicemente dei semplici wrapper per il metodo dispatch del Dispatcher. In un’applicazione reale la situazione potrebbe complicarsi. Passiamo ora allo Store:

import Dispatcher from "src/Dispatcher";
import Events from "events";
import _ from "underscore";

var EventEmitter = Events.EventEmitter;

var data = [];

Dispatcher.register(function(action) {
	var text;

	switch (action.actionType) {
		case "addTodo":
			TodoStore.add(action.text);
			break;
		case "updateTodo":
			TodoStore.update(action.index,action.text);
			break;
		case "deleteTodo":
			TodoStore.delete(action.index);
			break;
		case "deleteAll":
			TodoStore.deleteAll();
			break;
	};
});

var TodoStore = _.extend({
	add: function(text) {
		data.push(text);
		this.emit("ListChanged");
	},
	update:function(index,text){
		data[index] = text;
		this.emit("ListChanged");
	},
	delete:function(index){
		data.splice(index, 1);
		this.emit("ListChanged");
	},
	deleteAll:function(){
		data = [];
		this.emit("ListChanged");
	},
	get:function(){
		return data;
	},
	addChangeListener: function(callback) {
		this.on("ListChanged", callback);
	},
	removeChangeListener: function(callback) {
		this.removeListener("ListChanged", callback);
	}
},EventEmitter.prototype);

export default TodoStore;

Come accennato in precedenza, nello Store è presente la business logic della nostra applicazione. Da notare che lo Store stesso, oltre che essere in ascolto del Dispatcher, è a sua volta un EventEmitter. Come vedremo però lo scope degli eventi dello Store è più piccolo di quello del Dispatcher: l’unico elemento in ascolto di questi eventi sarà la View. Nel prossimo snippet possiamo vedere uno degli elementi della View, il componente List che conterrà un elenco dei todo gestiti dallo Store:

import React from "react";
import Actions from "src/Actions";
import Store from "src/Store";

export default class List extends React.Component{
	constructor(props) {
	    super(props);
	    this.state = {
	      data:Store.get()
	    };

	    this.listener = this._listener.bind(this);
	}

	_listener(){
		this.setState({
      		data:Store.get()
	    });
	}

	componentDidMount() {
	    Store.addChangeListener(this.listener);
	}

  	componentWillUnmount() {
    	Store.removeChangeListener(this.listener);
  	}

  	delete(index) {
  		Actions.delete(index);
	}

	render() {

		var items = this.state.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.delete.bind(this,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.state.data.length ? items : emptyRow}
					</tbody>
					<tfoot>
					    <tr className="text-center">
					      <td colSpan="2">Total Todos : {this.state.data.length}</td>
					    </tr>
  					</tfoot>
				</table>
			</div>
	    );
	}
}

La List si iscrive al change dello Store sincronizzando il suo stato tramite il metodo setState. Questo causerà un render dell’intera pagina ad ogni cambio di stato dello Store, tenendo quindi allineata la table con l’elenco presente nello Store. Inoltre il click sui pulsanti “delete” presenti su ognuna delle riga, scatenerà una nuova Action, che a sua volta farà sì che lo Store cambi il suo stato (tramite il Dispatcher) e questo infine provocherà un nuovo render. et voilà.

Conclusioni

React non ha molto senso senza Flux. D’altro canto Flux si porta dietro una verbosità non indifferente. La stessa applicazione in AngularJS avrebbe necessitato di meno della metà del codice per fare lo stesso lavoro. Ma il fatto che Flux sia così stringente nella forma ha due grossi vantaggi dal mio punto di vista:

  • C’è solo un modo di far comunicare tra loro i componenti: questo non accade in quasi nessun framework Javascript front-end. Basti pensare ad AngularJS, che proprio per il passaggio di valori tra pagine/componenti prevede moltissime modalità alternative. Al crescere del progetto (e quindi del team) rimane molto difficile mantenere uno standard, il che porta di solito a codice che ogni volta assume una forma differente.
  • La complessità del codice rimane pressoché costante: dato che il flusso di ogni operazione è sempre la stessa, la difficoltà di aggiungere nuove feature rimane costante. Infatti bisognerà sempre creare prima la Action, poi lo Store ed infine la View, con pochissimi margini di manovra.

Detto questo però mi sento di consigliare React+Flux solo a progetti web di complessità e dimensioni medio/grandi. Solo su larga scala infatti la verbosità di Flux ripaga in termini di manutenibilità. I più curiosi potranno trovare sul repository GitHub di cosenonjaviste il progetto completo. In questo progetto oltre a quello che avete visto potrete vedere l’utilizzo di jspm: la nuova moda in fatto di package managing per il front-end, e react-router che ci permette di configurare le rotte della nostra applicazione in maniera del tutto simile a quello che avviene con AngularJS ed altri framework simili. Infine sul mio blog trovate un articolo sul testing dei componenti React, l’esempio preso in considerazione è la stessa todo-list appena analizzata insieme. 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.