Diseño de interfaces web con React.js
En este artículo explicaremos cómo diseñar interfaces web con React.js mediante la implementación de una aplicación de lista de tareas.
React.js es un framework JavaScript para el navegador creado por Facebook y que sirve para diseñar e implementar interfaces web con funcionalidad completa.
React.js no solo permite definir y posicionar los elementos de la interfaz de un sitio o aplicación web, sino que también permite implementar las interacciones entre dichos elementos.
Sin embargo, con React.js no es posible definir un modelo de datos para acceder a una base de datos de forma directa. Los datos deben ser suministrados a través de una fuente externa, como por ejemplo, un servicio o aplicación web.
Creación de un proyecto React.js
Para poder utilizar React.js, debemos tener instalado Node.js en nuestra computadora.
Con Node.js debidamente instalado en nuestra máquina, necesitamos ahora un lugar en dónde almacenar los archivos necesarios para implementar la aplicación de lista de tareas.
Para esto, creamos un directorio llamado todo_react/
y dentro de él creamos
un directorio llamado todo/
con la siguiente estructura:
todo_react/
todo/
js/
jsx/
Dentro del directorio todo/
, creamos el archivo index.html
con las
siguientes líneas de código:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Todo list</title>
</head>
<body>
<div id="main"></div>
</body>
</html>
Este es el único archivo HTML necesario en un proyecto React.js. El concepto de plantilla, muy común al trabajar con frameworks MVC como Django, no existe en React.js. En su lugar existe el concepto de componente. Explicaremos este aspecto en detalle más adelante.
El código fuente del proyecto se puede encontrar en Github y Bitbucket.
Instalación de React.js
Ahora debemos instalar React.js, descargando la última versión del Starter Kit desde la página de descargas de React.js.
A continuación, extraemos los archivos react.min.js
y react-dom.min.js
a la
carpeta todo/js/
.
todo_react/
todo/
js/
react.min.js
react-dom.min.js
Finalmente, modificamos el archivo index.html
para incluir los archivos
JavaScript recién extraídos.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Todo list</title>
<script src="js/react.min.js" charset="utf-8"></script>
<script src="js/react-dom.min.js" charset="utf-8"></script>
</head>
</html>
Instalación de Babel
En una aplicación React.js, es recomendable implementar los componentes en un lenguaje llamado JSX, el cual es una extensión al lenguaje JavaScript que permite agregar etiquetas XML en un fragmento de código JavaScript. Esto nos permite definir el código HTML de nuestro componente directamente en el archivo JavaScript.
No es obligatorio utilizar JSX para implementar los componentes. Se pueden implementar usando código JavaScript regular, con la desventaja que el código resultante es más difícil de entender para los desarrolladores.
Debido a que los navegadores web no entienden el código escrito en JSX, se hace necesario usar un traductor para convertirlo a código JavaScript regular. Babel es un compilador para la nueva generación de JavaScript que precisamente puede realizar dicha traducción.
Para hacer uso de Babel, instalamos las utilidades de línea de comandos de forma global en el sistema.
$ npm install --global babel-cli
A continuación debemos instalar el paquete babel-preset-react
en el
directorio del proyecto.
$ cd todo_react/
$ npm install babel-preset-react
Luego, creamos un archivo llamado todo.jsx
dentro de la carpeta todo/jsx/
.
Es en este archivo donde vamos a implementar la interfaz web más adelante.
todo_react/
todo/
jsx/
todo.jsx
El archivo todo.jsx
debe contener las siguientes líneas de código:
ReactDOM.render(
<h1>It works!</h1>,
document.getElementById('main')
);
Como se puede apreciar, la función render()
de React.js nos permite
definir el código HTML inicial de nuestra aplicación. En este caso estamos
definiendo un heading y enseguida le indicamos a React.js que lo incluya
dentro del <div id="main">
que definimos en el archivo index.html
.
Para compilar el archivo todo.jsx
a JavaScript necesitamos ejecutar el
siguiente comando:
$ babel --presets react todo/jsx --watch --out-dir todo/js
Este comando va escanear los archivos en el directorio todo/jsx
indefinidamente. Cualquier cambio que se detecte en algún archivo dentro del
directorio, activará la compilación automáticamente. Los archivos
JavaScript resultantes se guardarán en el directorio todo/js
.
todo_react/
todo/
js/
todo.js
Finalmente, incluimos el archivo todo.js
en el archivo HTML index.html
de
la siguiente forma:
<!DOCTYPE html>
<html>
<body>
<div id="main"></div>
<script src="js/todo.js" charset="utf-8"></script>
</body>
</html>
Instalación y configuración de un servidor web
Para visualizar la interfaz web a medida que la vamos desarrollando, necesitamos instalar y configurar un servidor web. También necesitamos que en el servidor exista una aplicación web sencilla que nos permita consultar y guardar tareas.
Afortunadamente para nuestros lectores, hemos decidido implementar dicha aplicación web con anticipación. El código fuente se puede descargar desde el repositorio del proyecto en Github o Bitbucket.
En primer lugar, debemos instalar Express y BodyParser en el directorio del proyecto.
$ cd todo_react/
$ npm install express
$ npm install body-parser
En seguida, debemos descargar el archivo server.js
desde el repositorio
y colocarlo en el directorio del proyecto.
todo_react/
server.js
Para iniciar el servidor, ejecutamos el siguiente comando:
$ node server.js
De esta forma es posible visualizar nuestro proyecto al abrir la URL http://localhost:8080 con nuestro navegador favorito.
Finalmente, necesitamos crear un archivo llamado todos.json
en el directorio
del proyecto. Este archivo va a actuar como una base de datos en donde
vamos a guardar las tareas creadas por la aplicación de lista de tareas.
todo_react/
todos.json
El archivo todos.json
debe contener las siguientes líneas de texto:
[
{"id": 1, "description": "Buy milk", "done": true},
{"id": 2, "description": "Do dishes", "done": false}
]
Instalación de jQuery
Debido a que React.js no incluye un módulo AJAX, vamos a instalar la librería jQuery que más adelante vamos a utilizar para realizar llamadas AJAX a nuestro servidor web recién configurado.
Simplemente vamos a la página de descargas de jQuery y descargamos la última versión de producción, que al momento de escribir este artículo es la versión 1.12.3.
Luego, copiamos el archivo jquery-1.12.X.min.js
al directorio todo/js
.
todo_react/
todo/
js/
jquery-1.12.X.min.js
Por último, incluimos jQuery al final del archivo index.html
, antes del
cierre de la etiqueta <body>
, de la siguiente forma.
<!DOCTYPE html>
<html>
<body>
<div id="main"></div>
<script src="js/jquery-1.12.X.min.js" charset="utf-8"></script>
<script src="js/todo.js" charset="utf-8"></script>
</body>
</html>
Diseñando la interfaz web
Como se mencionó anteriormente, cuando se trabaja con React.js no se implementan plantillas HTML sino componentes en JavaScript.
La interfaz web se debe diseñar como un rompecabezas Lego. Para nuestro caso, la aplicación de lista de tareas estará constituida por los siguientes componentes:
- TodoBox
- TodoList
- Todo
- TodoForm
Como se puede apreciar, la estructura de componentes está organizada de forma
jerárquica, siendo TodoBox
el componente raíz o padre de todos los demás.
El componente TodoBox
es simplemente un <div>
que va a contener un
sub-componente TodoList
y un sub-componente TodoForm
, es decir, toda la
interfaz de la aplicación.
El sub-componente TodoList
es una lista <ul>
que estará conformada por uno
o más sub-componentes Todo
, los cuales representan las tareas que vamos a ir
agregando a medida que usamos la aplicación.
El sub-componente Todo
es un ítem de lista <li>
en donde colocaremos los
datos de la tarea como un código único, la descripción y un valor que indica si
ha sido completada o no.
El sub-componente TodoForm
es un formulario <form>
que nos permitirá
agregar más tareas a la lista.
Para implementar los sub-componentes TodoList
, Todo
y TodoForm
,
modificamos por completo el archivo todo.jsx
, de la siguiente manera:
var Todo = React.createClass({
render: function() {
return (
<li className="todo" id="{this.props.id}">
{this.props.children}
-
{this.props.done ? <span>Done!</span> : <a href="#">Mark as done</a>}
</li>
);
}
});
var TodoList = React.createClass({
render: function() {
return (
<ul className="todo-list">
<Todo id={1} done>Buy milk</Todo>
<Todo id={2}>Do dishes</Todo>
</ul>
);
}
});
var TodoForm = React.createClass({
render: function() {
return (
<form className="todo-form">
<input type="text" />
<input type="submit" value="Add todo" />
</form>
);
}
});
Como pueden ver, la sintaxis JSX nos permite definir el código HTML directamente dentro de las funciones JavaScript responsables de crear cada componente.
Los componentes se definen llamando a la función createClass()
de React.js
y asignando su resultado a una variable regular de JavaScript.
Para incluir un componente dentro de otro, usamos una etiqueta XML cuyo nombre es el mismo que el de la variable que contiene al componente definido, así:
// Definicion del componente Todo
var Todo = React.createClass({...});
var TodoList = React.createClass({
render: function() {
return (
<ul className="todo-list">
// Incluyendo el componente Todo definido anteriormente
<Todo id={1} done>Buy milk</Todo>
</ul>
)
}
});
El código HTML se escribe dentro del método render()
de cada componente, el
cual es responsable de incluirlo en documento HTML principal al momento de
ejecutar la aplicación.
Para implementar el componente TodoBox
, agregamos las siguientes líneas de
código al final del archivo todo.jsx
:
var TodoBox = React.createClass({
render: function() {
return (
<div className="todo-box">
<h1>Todo List</h1>
<TodoList />
<TodoForm />
</div>
);
}
});
ReactDOM.render(
<TodoBox />,
document.getElementById('main')
);
Para visualizar nuestro progreso, vamos a recargar la URL http://localhost:8080 en nuestro navegador favorito. Debemos ver algo como esto:
Propiedades y estado de una aplicación React.js
Antes de seguir adelante, es preciso explicar la forma como los datos fluyen dentro de una aplicación React.js.
En React.js existen dos formas de controlar el flujo de datos:
- A través de las propiedades de cada componente
- A través del estado de cada componente
Los datos que pasamos a través de los atributos de los componentes se consideran como propiedades.
<Todo id={1} done>Buy milk</Todo>
En este ejemplo, podemos usar las propiedades id
, done
y children
en la
implementación del componente Todo
de la siguiente manera:
<!-- En el metodo render() del componente Todo -->
<li className="todo" id="{this.props.id}">
{this.props.children}
-
{this.props.done ? <span>Done!</span> : <a href="#">Mark as done</a>}
</li>
La variable {this.props.id}
será igual a 1
, la variable
{this.props.children}
corresponderá al contenido del cuerpo de la etiqueta,
en este caso Buy milk
, y la variable {this.props.done}
será igual a true
.
Las propiedades son inmutables, lo que significa que no podemos cambiar el valor que se les ha asignado inicialmente y son utilizadas para compartir datos hacia los sub-componentes. React.js no permite compartir propiedades hacia un componente de mayor jerarquía.
Por otro lado, los componentes en React.js también poseen un estado. El
estado es privado para cada componente y se puede modificar al llamar al método
setState()
.
Cuando el estado del componente cambia, los elementos de la interfaz son refrescados automáticamente para mostrarle al usuario el estado actualizado.
Actualizando el estado de la interfaz
La interfaz web que hemos implementado hasta el momento es estática, es decir, que las tareas que podemos ver son las que hemos agregado al archivo JSX de forma manual.
Sería mucho mas conveniente que la interfaz pudiera mostrar las tareas obtenidas desde algún servicio o aplicación web.
Para hacer esto posible, vamos a hacer uso de la aplicación web que configuramos con anterioridad. Además de poder visualizar nuestro progreso, la aplicación nos permite obtener una lista de tareas a través de la URL http://localhost:8080/api/todos/.
La pregunta que nos debemos hacer ahora es: ¿Cual es el componente responsable de obtener los datos del servidor?
Debido a que los datos solo fluyen de los componentes de mayor a los de menor
jerarquía, el componente TodoBox
debe recibir los datos del servidor y
mediante propiedades, compartir los datos con los componentes descendientes en
la jerarquía.
Para hacer esto posible, modificamos la función render()
en el archivo
todo.jsx
de la siguiente manera:
ReactDOM.render(
<TodoBox url="/api/todos" />,
document.getElementById('main')
);
Solo hemos agregado una atributo llamado url
a la etiqueta del componente
TodoBox
, cuyo valor es la URL que nos permite obtener la lista de tareas
del servidor.
Ahora, vamos a modificar la implementación del componente TodoBox
para
implementar el código responsable de obtener los datos del servidor.
var TodoBox = React.createClass({
loadTodoList: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadTodoList();
setInterval(this.loadTodoList, 2000);
},
render: function() {
return (
<div className="todo-box">
<h1>Todo List</h1>
<TodoList data={this.state.data} />
<TodoForm />
</div>
);
}
});
La función getInitialState()
solo se ejecuta una vez y sirve para
inicializar el estado del componente. En este caso, el estado es un objeto
JavaScript que va a contener el listado de tareas en la variable data
.
En la función loadTodoList()
, usamos la función ajax()
de jQuery para
obtener el listado de tareas desde la URL definida por la variable
this.props.url
. El estado del componente se actualiza cuando la llamada
AJAX ha sido exitosa, mediante el llamado a la función this.setState()
.
La función componentDidMount()
se ejecuta una sola vez cuando el componente es
incluido dentro del documento HTML principal. Para seguir obteniendo los
datos, hacemos polling al servidor para obtener una copia de la lista de
tareas cada dos segundos mediante la función setInterval()
.
En la función render()
, compartimos el estado con el sub-componente TodoList
al agregarle un atributo llamado data
.
Para finalizar, debemos modificar el sub-componente TodoList
para que muestre
las tareas obtenidas del servidor en lugar de las que habíamos agregado
manualmente.
var TodoList = React.createClass({
render: function() {
var todoNodes = this.props.data.map(function(todo) {
return (
<Todo id={todo.id} done={todo.done ? true : false}>
{todo.description}
</Todo>
);
});
return (
<ul className="todo-list">
{todoNodes}
</ul>
);
}
});
La función map()
recorre cada una de las tareas almacenadas en la variable
this.props.data
y retorna el código HTML para cada una.
Agregando nuevas tareas
Para agregar nuevas tareas al listado, agregamos la función createTodo()
y
modificamos la función render()
en el componente TodoBox
:
var TodoBox = React.createClass({
createTodo: function(description) {
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: description,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
...
render: function() {
return (
<div className="todo-box">
<h1>Todo List</h1>
<TodoList data={this.state.data} />
<TodoForm createTodo={this.createTodo} />
</div>
);
}
});
Como se puede apreciar en la función render()
, estamos pasando la función
createTodo()
como una propiedad en el atributo del sub-componente TodoForm
.
Esto nos permite organizar todas las operaciones de envío y recepción de
datos en el componente TodoBox
pero con la posibilidad de poder llamar a
createTodo()
desde el sub-componente TodoForm
.
Ahora vamos a modificar el componente TodoForm
para que podamos crear una
nueva tarea cuando pulsemos el botón Add todo
:
var TodoForm = React.createClass({
onDescriptionChange: function(event) {
this.setState({description: event.target.value})
},
onSubmit: function(event) {
event.preventDefault();
var description = this.state.description.trim();
if (!description) return;
this.props.createTodo({description: description});
this.setState({description: ''});
},
getInitialState: function() {
return {description: ''};
},
render: function() {
return (
<form className="todo-form" onSubmit={this.onSubmit}>
<input type="text" value={this.state.description} onChange={this.onDescriptionChange} />
<input type="submit" value="Add todo" />
</form>
);
}
});
En la función getInitialState()
estamos inicializando el estado del
sub-componente, tal y como hicimos con el componente TodoBox
anteriormente.
En la función onDescriptionChange()
actualizamos el estado del sub-componente
cada vez que el usuario escribe en la casilla de texto. Este funcionamiento es
posible debido a que estamos enlazando la función al evento onChange
de la
casilla de texto en render()
.
En la función onSubmit()
estamos creando una nueva tarea al llamar al método
createTodo()
del componente TodoBox
, pasándole como argumento la cadena de
texto que el usuario escribió en la casilla de texto. Al final de la operación,
borramos el contenido de la casilla de texto.
Marcando las tareas como completadas
Para finalizar, vamos a hacer que las tareas se marquen como completadas cuando
hacemos click en el enlace Mark as done
de cada tarea.
Para marcar las tareas como completadas, vamos a incluir la función
completeTodo()
en el componente TodoBox
:
var TodoBox = React.createClass({
completeTodo: function(todoId) {
$.ajax({
url: this.props.url + '/done',
dataType: 'json',
type: 'POST',
data: todoId,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
...
render: function() {
return (
<div className="todo-box">
<h1>Todo List</h1>
<TodoList data={this.state.data} completeTodo={this.completeTodo} />
<TodoForm createTodo={this.createTodo} />
</div>
);
}
});
Como el método completeTodo()
va a ser utilizado por el sub-componente
Todo
, primero lo compartimos con el sub-componente TodoList
, como se
aprecia arriba; y luego con el sub-componente Todo
, como se muestra a
continuación:
var TodoList = React.createClass({
render: function() {
var todoNodes = this.props.data.map(function(todo) {
return (
<Todo id={todo.id} done={todo.done ? true : false} completeTodo={this.props.completeTodo}>{todo.description}</Todo>
);
}.bind(this));
return (
<ul className="todo-list">
{todoNodes}
</ul>
);
}
});
¡Felicitaciones por haber llegado hasta el final del artículo! En este punto ya tenemos el diseño básico de la interfaz web para una aplicación de lista de tareas con React.js. Dejamos al lector el ejercicio de implementar la funcionalidad para borrar tareas.
Recuerden que el código fuente completo explicado en este artículo se puede descargar desde Github o Bitbucket.