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:
- El Controller view,
App
, será el único con acceso al localStorage. Por lo tanto, la lectura y decodificación que hacíamos enForm
yGrid
serán removidas y trasladadas aApp
. - Así mismo, el método
save()
deForm
ya no tendrá acceso a localStorage. En cambio, invocará un método deApp
que se le será pasado vía props. App
será, además, el único componente con state. Guardará una copia de la lista de notas en él para controlar las actualizaciones. La información requerida por los otros componentes se les será enviada vía props.
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.
Compilamos los archivos y probamos las nuevas actualizaciones instantáneas.