ezakto code

Los contenidos de este blog están desactualizados.

Pero estoy pensando en actualizarlos y escribir más.

Si te interesa, dejame tu email (no spam, no newsletters). Si hay suficiente interés me pondré manos a la obra y te lo haré saber!


Tutorial React: Controller views

27 de junio 2015

Interacción entre componentes a través de un componente controlador

Si bien nuestro administrador de notas ya es modestamente funcional, hay un aspecto crucial para cualquier aplicación moderna que no está cubierto: la actualización instantánea.

React está diseñado para realizar operaciones complejas en el DOM automáticamente, además de eficientemente. Entonces, ¿por qué recargar la página cada vez que agrego una nota?

Con una simple actualización del state, el componente Grid puede mostrar las notas que añadimos instantáneamente. Pero es necesario notificarle cada vez que una nota es creada para que esto ocurra. Para hacer esto, necesitamos una capa superior que notifique a Grid cada vez que Form realiza un cambio. Construiremos esta capa superior como un nuevo gran componente que encapsule tanto a Grid como a Form y se encargue de administrar el flujo de información. Esto se conoce a grandes rasgos como Controller view o vista controladora, ya que su función es controlar a las demás vistas.

Este nuevo componente, al que llamaremos App, será el único montado en DOM inicial, y se encargará de montar los otros dos en su método render(). Además, albergará los métodos que modifiquen los datos de la aplicación (en este caso, el método save() del componente From).

Lo primero que haremos será modificar nuestro index.html. Quitaremos el div #wrapper y su contenido, y lo reemplazaremos con un único y vacío div #app.

<body>
    <div id="app"></div>
    <script src="js/main.js"></script>
</body>

Ahora crearemos un nuevo archivo, App.js, en la carpeta react/components. Contendrá la estructura del nuevo componente. En su método render(), este componente devolverá el div #wrapper que habíamos quitado, y en su interior contendrá los componentes Form y Grid como antes. Para poder ser incluidos, es necesario cargar ambos módulos al inicio del archivo. Entonces, lucirá así:

var React = require('react');
var Form = require('./Form');
var Grid = require('./Grid');

var App = React.createClass({

    render: function() {
        return (
            <div id="wrapper">
                <Form />
                <Grid />
            </div>
        );
    }

});

module.exports = App;

Ahora editamos el glue file, app.js de la carpeta react. Reemplazamos las declaraciones var Form y var Grid por un var App, y montamos este nuevo componente en vez de los otros dos. Quedará, entonces, así:

var React = require('react');
var App = require('./components/App');

React.render(<App />, document.getElementById('app'));

Si compilamos ahora, veremos que la aplicación luce exactamente igual. La diferencia es que ahora sólo estamos montando un único componente, y es éste el encargado de montar el resto.

Vamos a hacer modificaciones cruciales en la aplicación:

La idea es mantener el uso del state al mínimo para no perder la predictibilidad de la aplicación, y a su vez ejercer un control jerárquico sobre los otros componentes, que permita relacionar sus comportamientos.

Comencemos con los cambios:

Grid.js

Eliminamos el método getInitialState().

En el método render(), reemplazaremos this.state.notes por this.props.notes, ya que el componente controlador enviará las listas via props.

La declaración del array notes quedará así:

        var notes = this.props.notes.map(function(note, idx){
Form.js

En el método save(), eliminaremos las líneas que acceden a localStorage y manipulan la lista de notas. Las reemplazaremos por una única llamada: this.props.onSave(note).

Además, evitaremos la página se recargue cancelando el comportamiento por defecto del evento submit con preventDefault().

Quedará así:

    save: function(|||e|||) {
        |||e.preventDefault();|||

        // Obtenemos los valores del formulario
        var note = {
            id: new Date().getTime(), // Generamos una id rápida
            title: React.findDOMNode(this.refs.title).value,
            text: React.findDOMNode(this.refs.text).value
        };

        // Enviamos la nota al controller view
        |||this.props.onSave(note);|||

        // Vaciamos el formulario
        React.findDOMNode(this.refs.title).value = '';
        React.findDOMNode(this.refs.text).value = '';

        // Y finalmente lo cerramos
        this.close();
    },

Como se ve, estamos ejecutando una función (onSave() almacenada en this.props). Esto quiere decir que deberemos pasar esta función desde el componente padre a través de los atributos.

App.js

Modificamos el método render() ligeramente. Vamos a añadir un atributo a cada componente hijo: notes para Grid y onSave para Form, acorde a los cambios que realizamos recién.

El valor de notes será una referencia al state de App, que almacenará la lista de notas, mientras que el valor de onSave será una referencia a un método propio también de App (especificamos ambos a continuación).

    render: function() {
        return (
            <div id="wrapper">
                <Form onSave={this.onSave} />
                <Grid notes={this.state.notes} />
            </div>
        );
    }

Creamos el método getInitialState(), donde cargaremos la lista de notas desde localStorage, o crearemos una nueva vacía en caso de no encontrar nada (tal como hacíamos antes en los otros componentes).

    getInitialState: function() {
        // Leemos la lista de notas guardadas o creamos una vacía
        var notes = window.localStorage.getItem('notes');

        if (notes === null) {
            notes = []; // Creamos una nueva lista vacía
        } else {
            notes = JSON.parse(notes); // Decodificamos la cadena
        }

        return {
            notes: notes
        };
    },

Ahora creamos el método onSave(note). Será el encargado de guardar las notas que se envíen desde Form. El código será similar al que usamos antes, con sutiles diferencias:

    onSave: function(note) {
        // Copiamos la lista de notas almacenada en el state
        var notes = this.state.notes.slice();

        // Insertamos la nueva nota al principio de la lista
        notes.unshift(note);

        // Actualizamos el state
        this.setState({
            notes: notes
        });

        // Codificamos la lista como cadena de texto
        notes = JSON.stringify(notes);

        // Guardamos en localStorage
        window.localStorage.setItem('notes', notes);
    },

Como vemos, producimos una actualización del state en esta función. Cada vez que una nota sea guardada, el state de App se actualizará, disparando su función render(), que actualizará las props pasadas a Grid, por lo que éste también ejecutará su render(), mostrando la nueva nota instantáneamente. Se produce un flujo unidireccional de datos, o one-way data flow.

onewayflow

Compilamos los archivos y probamos las nuevas actualizaciones instantáneas.