Diseño de interfaces web con React.js

React week IMGP4198 por Marcus Bernales. CC BY. Imagen con calidad y tamaño reducidos.

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.

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.

Aplicación React.js

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.

Página de descargas de jQuery

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}
        &nbsp; - &nbsp;
        {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:

Aplicación React.js

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}
  &nbsp; - &nbsp;
  {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.