100% encontró este documento útil (9 votos)
2K vistas

Programación Reactiva Con React-NodeJS-MongoDB-rev005

Cargado por

Jeguel Cortez
Derechos de autor
© © All Rights Reserved
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
100% encontró este documento útil (9 votos)
2K vistas

Programación Reactiva Con React-NodeJS-MongoDB-rev005

Cargado por

Jeguel Cortez
Derechos de autor
© © All Rights Reserved
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 540

Sold to

[email protected]
Programación
Reactiva con React,
NodeJS & MongoDB

❝Se proactivo ante los cambios, pues una vez que llegan no tendrás tiempo para
reaccionar❞

─ Oscar Blancarte (2018)

Página | 2
Datos del autor:

Ciudad de México

e-mail: [email protected]

Autor y Editor

© Oscar Javier Blancarte Iturralde

Queda expresamente prohibida la reproducción o transmisión, total o parcial, de


este libro por cualquier forma o medio; ya sea impreso, electrónico o mecánico,
incluso la grabación o almacenamiento informáticos sin la previa autorización
escrita del autor.

Composición y redacción:

Oscar Javier Blancarte Iturralde

Edición:

Oscar Javier Blancarte Iturralde

Portada

Arq. Jaime Alberto Blancarte Iturralde

Primera edición

Se publicó por primera vez en Enero del 2018

3 | Página
Acerca del autor
Oscar Blancarte es originario de Sinaloa, México donde estudió la carrera de
Ingeniería en Sistemas Computacionales y rápidamente se mudó a la Ciudad de
México donde actualmente radica.

Oscar Blancarte es un Arquitecto de software con más de 12 años de experiencia


en el desarrollo y arquitectura de software. Certificado como Java Programmer
(Sun microsystems), Análisis y Diseño Orientado a Objetos (IBM) y Oracle IT
Architect (Oracle).

A lo largo de su carrera ha trabajado para diversas empresas del sector de TI,


entre las que destacan su participación en diseños de arquitectura de software y
consultoría para clientes de los sectores de Retail, Telco y Health Care.

Oscar Blancarte es además autor de su propio blog


https://www.oscarblancarteblog.com desde el cual está activamente publicando
temas interesantes sobre Arquitectura de software y temas relacionados con la
Ingeniería de Software en general. Desde su blog ayuda a la comunidad a
resolver dudas y es por este medio que se puede tener una interacción más
directa con el autor.

Página | 4
Otras obras del autor

Introducción a los patrones de diseño – Un enfoque práctico

Hoy en día aprender patrones de diseño no es


una cualidad más, si no una obligación. Y es
que estudiar y comprender los patrones de
diseño nos convierte en un mejor
programador/arquitecto y es clave para
conseguir una mejor posición en el mundo
laboral.

Este libro fue creado con la intención de


enseñar a sus lectores cómo utilizar los
patrones de diseño de una forma clara y
simple desde un enfoque práctico y con
escenarios del mundo real.

Tengo que aceptar que este no es un libro


convencional de patrones de diseño debido,
principalmente, a que no sigue la misma
estructura de las primordiales obras
relacionadas con este tema. En su lugar, me
quise enfocar en ofrecer una perspectiva del
mundo real, en donde el lector pueda aprender a utilizar los patrones de diseño en
entornos reales y que puedan ser aplicados a proyectos reales.

Cuando empecé a estudiar sobre patrones de diseño, me di cuenta que siempre se


explicaban en escenarios irracionales que poco o ninguna vez podrías utilizar, como
por ejemplo para aprender a crear figuras geométricas, hacer una pizza o crear una
serie de clases de animales que ladren o maúllen; esos eran los ejemplos que siempre
encontraba, que, si bien, explicaban el concepto, se complicaba entender cómo
llevarlos a escenarios reales.
En este libro trato de ir un poco más allá de los ejemplos típicos para crear cosas
realmente increíbles. Por ejemplo:

 Crear tu propia consola de línea de comandos.


 Crear tu propio lenguaje para realizar consultas SQL sobre un archivo de
Excel.
 Crear aplicaciones que puedan cambiar entre más de una base de datos, por
ejemplo, Oracle y MySQL según las necesidades del usuario.
 Administrar la configuración global de tu aplicación.
 Crear un Pool de ejecuciones para controlar el número de hilos ejecutándose
simultáneamente, protegiendo nuestra aplicación para no agotar los recursos.
 Utilizar proxis para controlar la seguridad de tu aplicación.
 Utilizar estrategias para cambiar la forma en que los usuarios son autenticados
en la aplicación; como podría ser por Base de datos, Webservices, etcétera.

5 | Página
 Crear tu propia máquina de estados para administrar el ciclo de vida de tu
servidor.

Éstos son sólo algunos de los 25 ejemplos que abordaremos en este libro, los
cuales están acompañados, en su totalidad, con el código fuente para que seas
capaz de descargarlos, ejecutarlos y analizarlos desde tu propia computadora.
Adquiérelo en https://patronesdediseño.com

Página | 6
Agradecimientos
Este libro tiene una especial dedicación a mi esposa Liliana y mi hijo Oscar,
quienes son la principal motivación y fuerza para seguir adelante todos los días,
por su cariño y comprensión, pero sobre todo por apoyarme y darme esperanzas
para escribir este libro.

A mis padres, quien con esfuerzo lograron sacarnos adelante, darnos una
educación y hacerme la persona que hoy soy.

A todos mis lectores anónimos de mi blog y todas aquellas personas que, de


buena fe, compraron y recomendaron mi primer libro (Introducción a los patrones
de diseño) y fueron en gran medida lo que me inspiro en escribir este segundo
libro.

Finalmente, quiero agradecerte a ti, por confiar en mí y ser tu copiloto en esta


gran aventura para aprender React, NodeJS, MongoDB y muchas cosas más. :)

7 | Página
Prefacio
Cada día, nacen nuevas tecnologías que ayudan a construir aplicaciones web más
complejas y elaboras, con relativamente menos esfuerzo, ayudando a que casi
cualquiera persona con conocimientos básicos de computación puede realizar una
página web. Sin embargo, no todo es felicidad, pues realizar un trabajo
profesional, requiere de combinar muchas tecnologías para poder entregar un
producto terminado de calidad.

Puede que aprender React o NodeJS no sea un reto para las personas que ya
tiene un tiempo en la industria, pues ya están familiarizados con HTML,
JavaScript, CSS y JSON, por lo que solo deberá complementar sus conocimientos
con una o dos tecnologías adicionales, sin embargo, para las nuevas
generaciones de programadores o futuros programadores, aprender React o
NodeJS puede implicar un reto aun mayor, pues se necesita aprender primero
las bases, antes de poder programar en una capa más arriba.

Cuando yo empecé a estudiar el Stack completo de React + NodeJS + MongoDB


fue necesario tener que leer 3 libros completos, pues cada uno enseñaba de
forma separada una parte de la historia. Cada libro me enseñó a utilizar las
tecnologías de forma individual, lo cual era fantástico, sin embargo, cuando llego
el momento de trabajar con todas las tecnologías juntas empezó el problema,
pues nadie te enseñaba como unir todo en un solo proyecto, optimizar y pasar a
producción como una aplicación completa. Todo esto, sin mencionar los cientos
o quizás miles de foros y blogs que tuve que analizar para aprender los trucos
más avanzados.

Este libro pretende evitarte ese gran dolor de cabeza que yo tuve por mucho
tiempo, pues a lo largo de este libro aprenderemos a utilizar React + NodeJS con
Express + MongoDB y aderezaremos todo esto con Redux, uno de los módulos
más populares y avanzados para el desarrollo de aplicaciones Web profesionales.
Finalmente aprenderemos a crear un API REST completo con NodeJS y utilizando
el Estándar de Autenticación JSON Web Tokens.

El objetivo final de este libro es que aprendas a crear aplicaciones Reactivas con
React, apoyado de las mejores tecnologías disponibles. Es por este motivo que,
durante la lectura de este libro, trabajaremos en un único proyecto que irá
evolucionando hasta terminarlo por completo. Este proyecto será, una réplica de
la red social Twitter, en la cual podremos crear usuarios, autenticarnos, publicar
Tweets, seguir a otros usuarios y ver las publicaciones de los demás en nuestro
feed.

Página | 8
Cómo utilizar este libro
Este libro es en lo general fácil de leer y digerir, pues tratamos de enseñar todos
los conceptos de la forma más simple y asumiendo que el lector tiene poco o
nada de conocimiento del tema, así, sin importar quien lo lea, todos podamos
aprender fácilmente.

Como parte de la dinámica de este libro, hemos agregado una serie de tipos de
letras que hará más fácil distinguir entre los conceptos importantes, código,
referencias a código y citas. También hemos agregado pequeñas secciones de
tips, nuevos conceptos, advertencias y peligros, los cuales mostramos mediante
una serie de íconos agradables para resaltar a la vista.

Texto normal:

Es el texto que utilizaremos durante todo el libro, el cual no enfatiza nada en


particular.

Negritas:

El texto en negritas es utilizado para enfatizar un texto, de tal forma que


buscamos atraer tu atención, que es algo importante.

Cursiva:
Es texto lo utilizamos para hacer referencia a fragmentos de código como
una variable, método, objeto o instrucciones de líneas de comandos. Pero
también es utilizada para resaltar ciertas palabras técnicas.

Código

Para el código utilizamos un formato especial, el cual permite colorear ciertas


palabras especiales, crear el efecto entre líneas y agregar el número de línea que
ayude a referencia el texto con el código.

1. ReactDOM.render(
2. <h1>Hello, world!</h1>,
3. document.getElementById('root')
4. );

El texto con fondo verdes, lo utilizaremos para indicar líneas que se agregan al
código existente.

Mientras que el rojo y tachado, es para indicar código que se elimina de un


archivo existente.

9 | Página
Por otra parte, tenemos los íconos, que nos ayudan para resaltar algunas cosas:

Nuevo concepto: <concepto>


Cuando mencionamos un nuevo concepto o termino
que vale la pena resaltar, es explicado mediante una
caja como esta.

Tip
Esta caja la utilizamos para dar un tip o sugerencia
que nos puede ser de gran utilidad.

Importante
Esta caja se utiliza para mencionar algo muy
importante.

Error común
Esta caja se utiliza para mencionar errores muy
comunes que pueden ser verdadero dolor de cabeza o
para mencionar algo que puede prevenir futuros
problemas.

Documentación
Esta caja se muestra cada vez que utilizamos un nuevo
servicio del API REST, la cual tiene la intención de
indicar al lector donde puede encontrar la
documentación del servicio, como es la URL,
parámetros de entrada/salida y restricciones de
seguridad.

Página | 10
Código fuente
Todo el código fuente de este libro está disponible en GitHub y dividido de tal
forma que, cada capítulo tenga un Branch independiente. El código fuente lo
puedes encontrar en:

https://github.com/oscarjb1/books-reactiveprogramming.git

Para ver el código de cada capítulo, solo es necesario dirigirse al listado de


branches y seleccionar el capitulo deseado:

La segunda y más recomendable opción es, utilizar un cliente de Git, como lo es


Source Tree y clonar el repositorio, lo que te permitirá tener acceso a todos los
branches desde tu disco duro.

11 | Página
Requisitos previos
Este libro está diseñado para que cualquier persona con conocimientos básicos
de programación web, puedo entender la totalidad de este libro, sin embargo,
debido a la naturaleza de React y NodeJS, es necesario conocer los fundamentos
de JavaScript, pues será el lenguaje que utilizaremos a lo largo de todo este libro.

También será interesante contar con conocimientos básicos de CSS3, al menos


para entender como declarar selectores y aplicarlos a los elementos de HTML.

Página | 12
INTRODUCCIÓN
Muy lejos han quedado los tiempos en los que Tim Berners Lee, conocido como
el padre de la WEB; estableció la primera comunicación entre un cliente y un
servidor, utilizando el protocolo HTTP (noviembre de 1989). Desde entonces, se
ha iniciado una guerra entre las principales compañías tecnológicas por dominar
el internet. El caso más claro, es la llamada guerra de los navegadores,
protagonizado por Netscape Communicator y Microsoft, los cuales buscaban que
sus respectivos navegadores (Internet Explorer y Netscape) fueran el principal
software para navegar en internet.

Durante esta guerra, las dos compañías lucharon ferozmente, Microsoft luchaba
por alcanzar a Netscape, quien entonces le llevaba la delantera y Netscape
luchaba para no ser alcanzados por Microsoft. Como hemos de imaginar,
Microsoft intento comprar a Netscape y terminar de una vez por todas con esta
guerra, sin embargo, Netscape se negó reiteradamente a la venta, por lo que
Microsoft inicio una de las más descaradas estrategias; amenazo con copiar
absolutamente todo lo que hiciera Netscape si no accedían a la venta.

Económicamente hablando, Microsoft era un gigante luchando contra una


hormiga, el cual pudo contratar y poner a su plantilla a cuento ingeniero fuera
necesarios para derrotar a Netscape.

En esta carrera por alcanzar a Netscape, ambas empresas empezaron a lanzar


releaces prácticamente cada semana, y semana con semana, los navegadores
iban agregando más y más funcionalidad, lo cual podría resultar interesante, pero
era todo lo contrario. Debido a la guerra sucia, ninguna empresa se apegaba a
estándares, que además eran prácticamente inexistentes en aquel entonces. Lo
que provocaba que los programadores tuvieran que escribir dos versiones de una
misma página, una que funcionara en Netscape y otra que funcionara para
Internet Explorer, pues las diferencia que existían entre ambos navegadores era
tan grande, que era imposible hacer una misma página compatible para los dos
navegadores.

Para no hacer muy larga esta historia, y como ya sabrás, Microsoft termino
ganando la guerra de los navegadores, al proporcionar Internet Explorer
totalmente gratis y preinstalado en el sistema operativo Windows.

Hasta este punto te preguntarás, ¿qué tiene que ver toda esta historia con React
y NodeJS?, pues la verdad es que mucho, pues durante la guerra de los
navegadores Netscape invento JavaScript. Aunque en aquel momento, no era un
lenguaje de programación completo, sino más bien un lenguaje de utilidad, que
permitía realizar cosas muy simples, como validar formularios, lanzar alertas y
realizar algunos cálculos. Es por este motivo que debemos recordar a Netscape,
pues fue el gran legado que nos dejó.

Finalmente, Netscape antes de terminar de morir, abre su código fuente y es


cuando nace Mozilla Firefox, pero esa es otra historia…

13 | Página
Retornando a JavaScript, este lenguaje ha venido evolucionando de una manera
impresionante, de tal forma que hoy en día es un lenguaje de programación
completo, como Java o C#. Tan fuerte ha sido su evolución y aceptación que hoy
en día podemos encontrar a JavaScript del lado del servidor, como es el caso de
NodeJS, creado por Ryan Dahl en 2009. De la misma forma, Facebook desarrollo
la librería React basada en JavaScript.

Tanto React como NodeJS funcionan en su totalidad con JavaScript, React se


ejecuta desde el navegador, mientras que NodeJS se ejecuta desde un servidor
remoto, el cual ejecuta código JavaScript.

JavaScript es un lenguaje de programación interpretado, lo que quieres decir que


el navegador va ejecutando el código a medida que es analizado, evitando
procesos de compilación. Además, JavaScript es un lenguaje regido por el
Estándar ECMAScript el cual, al momento de escribir estas líneas, se encuentra
en la versión 6 estable y se está desarrollando la versión 7.

Nuevo concepto: ECMAScript


ECMAScript es una especificación de lenguaje de
programación propuesta como estándar por NetScape
para el desarrollo de JavaScript.

Comprender la historia y la funcionalidad de JavaScript es fundamental para


aprender React y NodeJS, pues ambos trabajan 100% con este lenguaje. Aunque
en el caso de React, existe un lenguaje propio llamado JSX, que al final se
convierte en JavaScript. Pero eso lo veremos en su momento.

Página | 14
Índice

Acerca del autor..................................................................................................................................... 4

Otras obras del autor ............................................................................................................................. 5

Agradecimientos .................................................................................................................................... 7

Prefacio ................................................................................................................................................. 8

Cómo utilizar este libro .......................................................................................................................... 9

Código fuente ...................................................................................................................................... 11

Requisitos previos................................................................................................................................ 12

INTRODUCCIÓN ................................................................................................................................... 13

Índice ................................................................................................................................................... 15

Por dónde empezar.............................................................................................................................. 22


Introducción a React ........................................................................................................................ 24
Server Side Apps vs Single Page Apps........................................................................................... 25
Introducción a NodeJS ...................................................................................................................... 26
NodeJS y la arquitectura de Micro Servicios ................................................................................. 28
Introducción a MongoDB.................................................................................................................. 29
Bases de datos Relacionales VS MongoDB ................................................................................... 29
La relación entre React, NodeJs & MongoDB .................................................................................... 30
Resumen .......................................................................................................................................... 31

Preparando el ambiente de desarrollo................................................................................................. 33


Instalando Atom y algunos plugIn interesantes................................................................................. 33
Instalación de Atom..................................................................................................................... 33
Instalar PlugIns ............................................................................................................................ 34
Instalando NodeJS & npm ................................................................................................................ 36
Instalando MongoDB ....................................................................................................................... 37
Habilitando el acceso por IP......................................................................................................... 39
Instalando Compass, el cliente de MongoDB................................................................................ 40
Creando mi primer proyecto de React ............................................................................................... 43
Estructura base de un proyecto ................................................................................................... 43
Creación un proyecto paso a paso ............................................................................................... 44
Creación del proyecto con utilerías .............................................................................................. 52
Descargar el proyecto desde el repositorio .................................................................................. 54
Gestión de dependencias con npm .............................................................................................. 55
Micro modularización .................................................................................................................. 59
Introducción a WebPack................................................................................................................... 59
Instalando Webpack .................................................................................................................... 60
El archivo webpack.config.js ........................................................................................................ 61

15 | Página
React Developer Tools ...................................................................................................................... 63
Resumen .......................................................................................................................................... 65

Introducción al desarrollo con React .................................................................................................... 66


Programación con JavaScript XML (JSX) ............................................................................................ 66
Diferencia entre JSX, HTML y XML................................................................................................ 67
Contenido dinámico y condicional ............................................................................................... 73
JSX Control Statements................................................................................................................ 76
Transpilación ............................................................................................................................... 80
Programación con JavaScript puro.................................................................................................... 81
Element Factorys......................................................................................................................... 82
Element Factory Personalizados .................................................................................................. 83
Resumen .......................................................................................................................................... 84

Introducción a los Componentes.......................................................................................................... 85


La relación entre Components y Web Component ............................................................................. 85
Componentes con estado y sin estado .............................................................................................. 87
Componentes sin estado ............................................................................................................. 88
Componentes con estado ............................................................................................................ 90
Jerarquía de componentes ............................................................................................................... 92
Propiedades (Props) ......................................................................................................................... 94
PropTypes ........................................................................................................................................ 97
Validaciones avanzadas ............................................................................................................... 99
DefaultProps .................................................................................................................................. 101
Refs ............................................................................................................................................... 101
Alcance los Refs......................................................................................................................... 103
Keys ............................................................................................................................................... 103
Las 4 formas de crear un Componente ............................................................................................ 104
ECMAScript 5 – createClass ....................................................................................................... 105
ECMAScript 6 - React.Component .............................................................................................. 105
ECMAScript 6 - Function Component ......................................................................................... 106
ECMAScript 7 - React.Component .............................................................................................. 106
Resumen ........................................................................................................................................ 107

Introducción al proyecto Mini Twitter ............................................................................................... 108


Un vistazo rápido al proyecto ......................................................................................................... 108
Página de inicio ......................................................................................................................... 108
Perfil de usuario ........................................................................................................................ 109
Editar perfil de usuario .............................................................................................................. 109
Página de seguidores ................................................................................................................. 110
Detalle del Tweet ...................................................................................................................... 110
Inicio de sesión (Login) .............................................................................................................. 111
Registro de usuarios (Signup)..................................................................................................... 111
Análisis al prototipo del proyecto ................................................................................................... 112
Componente TwitterDashboard ................................................................................................ 112
Componente TweetsContainer .................................................................................................. 113
Componente UserPage .............................................................................................................. 114
Componente Signup .................................................................................................................. 115
Componente Login .................................................................................................................... 116

Página | 16
Jerarquía de los componentes del proyecto .................................................................................... 117
El enfoque Top-down & Bottom-up................................................................................................. 117
Top-down.................................................................................................................................. 118
Bottom-up................................................................................................................................. 119
El enfoque utilizado y porque .................................................................................................... 119
Preparando el entorno del proyecto ............................................................................................... 120
Instalar API REST ....................................................................................................................... 120
Probando nuestra API................................................................................................................ 123
Invocando el API REST desde React ................................................................................................. 126
Mejorando la clase APIInvoker................................................................................................... 128
El componente TweetsContainer .................................................................................................... 131
El componente Tweet..................................................................................................................... 135
Resumen ........................................................................................................................................ 141

Introducción al Shadow DOM y los Estados ....................................................................................... 143


Introducción a los Estados .............................................................................................................. 143
Establecer el estado de Componente .............................................................................................. 144
Actualizando el estado de un Componente ..................................................................................... 145
La librería react-addons-update ................................................................................................. 148
El Shadow DOM de React ............................................................................................................... 150
Atributos de los elementos: ....................................................................................................... 150
Eventos ..................................................................................................................................... 151
Resumen ........................................................................................................................................ 153

Trabajando con Formularios .............................................................................................................. 154


Controlled Components .................................................................................................................. 154
Uncontrolled Components .............................................................................................................. 157
Enviar el formulario........................................................................................................................ 158
Mini Twitter (Continuación 1) ......................................................................................................... 159
El componente Signup ............................................................................................................... 159
El componente login .................................................................................................................. 169
Resumen ........................................................................................................................................ 174

Ciclo de vida de los componentes ...................................................................................................... 175


Function componentWillMount ...................................................................................................... 176
Function render.............................................................................................................................. 176
Function componentDidMount ....................................................................................................... 176
Function componentWillReceiveProps ............................................................................................ 177
Function shouldComponentUpdate ................................................................................................ 177
Function componentWillUpdate ..................................................................................................... 178
Function componentDidUpdate ...................................................................................................... 178
Function componentWillUnmount .................................................................................................. 178
Flujos de montado de un componente ............................................................................................ 179
Flujos de actualización del estado................................................................................................... 180

17 | Página
Flujos de actualización de las propiedades...................................................................................... 181
Flujos de desmontaje de un componente ........................................................................................ 182
Mini Twitter (Continuación 2) ......................................................................................................... 182
El componente TwitterApp ........................................................................................................ 183
El componente TwitterDashboard ............................................................................................. 187
El componente Profile ............................................................................................................... 189
El componente SuggestedUsers ................................................................................................. 193
El componente Reply ................................................................................................................. 196
Resumen ........................................................................................................................................ 213

React Routing .................................................................................................................................... 214


Single page App ............................................................................................................................. 215
Router & Route .............................................................................................................................. 215
IndexRoute .................................................................................................................................... 216
History ........................................................................................................................................... 217
browserHistory.......................................................................................................................... 218
hashHistory ............................................................................................................................... 220
MemoryHistory ......................................................................................................................... 221
Link................................................................................................................................................ 221
Props ............................................................................................................................................. 222
URL params ................................................................................................................................... 223
Mini Twitter (Continuación 3) ......................................................................................................... 224
Preparando el servidor para BrowserHistory .............................................................................. 224
Implementando Routing en nuestro proyecto............................................................................ 227
El componente Toolbar ............................................................................................................. 227
Toques finales al componente Login .......................................................................................... 232
Toques finales al componente Signup ........................................................................................ 233
El componente UserPage........................................................................................................... 235
El componente MyTweets ......................................................................................................... 254
Resumen ........................................................................................................................................ 259

Interfaces interactivas ....................................................................................................................... 260


Qué son las transiciones ................................................................................................................. 260
Qué son las animaciones ................................................................................................................ 261
Introducción a CSSTranstionGroup ................................................................................................. 263
Mini Twitter (Continuación 4) ......................................................................................................... 265
El componente UserCard ........................................................................................................... 265
El componente Followings ......................................................................................................... 267
El componente Followers .......................................................................................................... 271
Resumen ........................................................................................................................................ 274

Componentes modales ...................................................................................................................... 275


Algunas librerías existentes ............................................................................................................ 275
Implementando modal de forma nativa.......................................................................................... 276
Mini Twitter (Continuación 5) ......................................................................................................... 277
El componente TweetReply ....................................................................................................... 277
El componente TweetDetail....................................................................................................... 283

Página | 18
Últimos retoques al proyecto .................................................................................................... 287
Resumen ........................................................................................................................................ 289

Redux................................................................................................................................................. 290
Introducción a Redux...................................................................................................................... 291
Componentes de Redux............................................................................................................. 292
Los tres principios de Redux ...................................................................................................... 294
Como funciona Redux................................................................................................................ 296
Implementando Redux Middleware ................................................................................................ 300
Debugging Redux ........................................................................................................................... 301
Implementando Redux con React ................................................................................................... 303
Estrategia de migración ............................................................................................................. 303
Instalar las dependencias necesarias.......................................................................................... 306
Estructura del proyecto con Redux ............................................................................................ 306
Creando nuestro archivo de acciones ........................................................................................ 307
Creando el primer reducer......................................................................................................... 308
Funciones de dispatcher ............................................................................................................ 310
Implementar la connect en TwitterApp...................................................................................... 311
Crear el Store de la aplicación .................................................................................................... 313
Comprobando la funcionalidad de Redux. .................................................................................. 315
Migrando el proyecto Mini Twitter a Redux .................................................................................... 316
Refactorizando el componente Login ......................................................................................... 316
Refactorizando el componente Signup ....................................................................................... 321
Refactorizando el componente TweetContainer ........................................................................ 327
Refactorizando el componente Tweet ....................................................................................... 331
Refactorizando el componente Reply......................................................................................... 337
Refactorizando el componente Profile ....................................................................................... 341
Refactorizando el componente SuggestedUsers ......................................................................... 343
Refactorizando el componente TwitterDashboard ..................................................................... 346
Refactorizando el componente Toolbar ..................................................................................... 347
Refactorizando el componente Followers & Followings.............................................................. 349
Refactorizando el componente UserCard ................................................................................... 354
Refactorizando el componente MyTweets ................................................................................. 354
Refactorizando el componente UserPage................................................................................... 355
Refactorizando el componente TweetReply ............................................................................... 365
Refactorizando el componente TweetDetails ............................................................................. 366
Últimas observaciones............................................................................................................... 371
Resumen ........................................................................................................................................ 372

Introducción a NodeJS ....................................................................................................................... 373


Porque es importante aprender NodeJS .......................................................................................... 373
El Rol de NodeJS en una aplicación ................................................................................................. 374
NodeJS es un mundo ...................................................................................................................... 375
Introducción a Express ................................................................................................................... 375
Instalando Express..................................................................................................................... 376
El archivo package.json.............................................................................................................. 376
Node Mudules........................................................................................................................... 379
Creando un servidor de Express ................................................................................................. 381
Express Verbs ................................................................................................................................. 383
Método GET .............................................................................................................................. 383
Método POST ............................................................................................................................ 384

19 | Página
Método PUT .............................................................................................................................. 384
Método DELETE ......................................................................................................................... 384
Consideraciones adicionales. ..................................................................................................... 384
Implementemos algunos métodos. ............................................................................................ 385
Trabajando con parámetros ........................................................................................................... 388
Query params............................................................................................................................ 388
URL params ............................................................................................................................... 389
Body params ............................................................................................................................. 390
Middleware ................................................................................................................................... 392
Middleware de nivel de aplicación ............................................................................................. 394
Middleware de nivel de direccionador ....................................................................................... 394
Middleware de terceros ............................................................................................................ 395
Middleware incorporado ........................................................................................................... 395
Error Handler ................................................................................................................................. 396
Resumen ........................................................................................................................................ 397

Introducción a MongoDB ................................................................................................................... 398


Porque es importante aprender MongoDB...................................................................................... 398
El rol de MongoDB en una aplicación .............................................................................................. 400
Como funciona MongoDB............................................................................................................... 401
Que son las Colecciones ............................................................................................................ 401
Que son los Documentos ........................................................................................................... 402
Operaciones básicas con Compass ............................................................................................. 403
Aprender a realizar consultas ......................................................................................................... 408
Filter ......................................................................................................................................... 408
Project ...................................................................................................................................... 419
Sort ........................................................................................................................................... 422
Paginación con Skip y Limit ........................................................................................................ 423
NodeJS y MongoDB ........................................................................................................................ 425
Estableciendo conexión desde NodeJS ....................................................................................... 425
Que son los Schemas de Mongoose ........................................................................................... 428
Schemas avanzados ................................................................................................................... 429
Schemas del proyecto Mini Twitter ................................................................................................. 432
Tweet Scheme ........................................................................................................................... 432
Profile Scheme .......................................................................................................................... 433
Ejecutar operaciones básicas ..................................................................................................... 434
Resumen ........................................................................................................................................ 441

Desarrollo de API REST con NodeJS .................................................................................................... 442


¿Qué es REST y RESTful?................................................................................................................. 442
REST vs SOA ................................................................................................................................... 443
Preparar nuestro servidor REST ...................................................................................................... 444
Configuración inicial del servidor ............................................................................................... 444
Establecer conexión con MongoDB ............................................................................................ 446
Creando un subdominio para nuestra API .................................................................................. 447
Desarrollo del API REST .................................................................................................................. 455
Mejores prácticas para crear URI’s ............................................................................................. 455
Códigos de respuesta ................................................................................................................ 457
Migrando a nuestra API REST..................................................................................................... 457

Página | 20
Implementar los servicios REST....................................................................................................... 458
Servicio - usernameValidate ...................................................................................................... 458
Servicio - Signup ........................................................................................................................ 461
Autenticación con JSON Web Token (JWT) ................................................................................. 463
Servicio - Login .......................................................................................................................... 466
Servicio - Relogin ....................................................................................................................... 469
Servicio - Consultar los últimos Tweets ...................................................................................... 471
Servicio - Consultar se usuarios sugeridos .................................................................................. 474
Servicio – Consulta de perfiles de usuario .................................................................................. 477
Servicio – Consulta de Tweets por usuario ................................................................................. 480
Servicio – Actualización del perfil de usuario .............................................................................. 483
Servicio – Consulta de personas que sigo ................................................................................... 485
Servicio – Consulta de seguidores .............................................................................................. 487
Servicio – Seguir ........................................................................................................................ 489
Servicio – Crear un nuevo Tweet................................................................................................ 491
Servicio – Like............................................................................................................................ 495
Servicio – Consultar el detalle de un Tweet ................................................................................ 497
Documentando el API REST ............................................................................................................ 501
Introducción al motor de plantillas Pug ...................................................................................... 501
API home .................................................................................................................................. 506
Service catalog .......................................................................................................................... 508
Service documentation .............................................................................................................. 513
Algunas observaciones o mejoras al API ......................................................................................... 516
Aprovisionamiento de imágenes................................................................................................ 516
Guardar la configuración en base de datos ................................................................................ 518
Documentar el API por base de datos ........................................................................................ 518
Resumen ........................................................................................................................................ 519

Producción ......................................................................................................................................... 520


Producción vs desarrollo................................................................................................................. 520
Habilitar el modo producción ......................................................................................................... 521
Empaquetar la aplicación para producción................................................................................. 522
Puertos .......................................................................................................................................... 524
Comunicación segura ..................................................................................................................... 526
Certificados comprados ............................................................................................................. 526
Certificados auto firmados......................................................................................................... 527
Instalando un certificado en nuestro servidor ............................................................................ 529
Alta disponibilidad ......................................................................................................................... 532
Cluster ...................................................................................................................................... 532
Hosting y dominios......................................................................................................................... 537
Resumen ........................................................................................................................................ 539

CONCLUSIONES .................................................................................................................................. 540

21 | Página
Por dónde empezar
Capítulo 1

Hoy en día existe una gran cantidad de propuestas para desarrollar aplicaciones
web, y cada lenguaje ofrece sus propios frameworks que prometen ser los
mejores, aunque la verdad es que nada de esto está cercas de la realidad, pues
el mejor framework dependerá de lo que buscas construir y la habilidad que ya
tengas sobre un lenguaje determinado.

Algunas de las propuestas más interesantes para el desarrollo web son, Angular,
Laravel, Vue.JS, Ember.js, Polymer, React.js entre un gran número de etcéteras.
Lo que puede complicar la decisión sobre que lenguaje, librería o framework
debemos utilizar.

Desde luego, en este libro no tratamos de convencerte de utilizar React, sino más
bien, buscamos enseñarte su potencial para que seas tú mismo quien pueda
tomar esa decisión.

Solo como referencia, me gustaría listarte algunas de las empresas que


actualmente utilizan React, para que puedas ver que no se trata de una librería
para hacer solo trabajos caseros o de baja carga:

 Udemy
 Bitbucket
 Anypoint (Mule Soft)
 Facebook
 Courcera
 Airbnb
 American Express
 Atlassian
 Docker
 Dropbox
 Instagram
 Reddit

Son solo una parte de una inmensa lista de empresas y páginas que utilizan React
como parte medular de sus desarrollos. Puedes ver la lista completa de páginas

Página | 22
que usan React aquí: https://github.com/facebook/react/wiki/sites-using-react.
Esta lista debería ser una evidencia tangible de que React es sin duda una librería
madura y probada.

Un dato que debemos de tener en cuenta, es que React es desarrollado por


Facebook, pero no solo eso, sino que es utilizado realmente por ellos, algo muy
diferente con Angular, que, a pesar de ser mantenido por Google, no es utilizando
por ellos para ningún sitio crítico. Esto demuestra la fe que tiene Facebook sobre
React, pero sobre todo garantiza la constante evolución de esta gran librería.

23 | Página
Introducción a React

React es sin duda una de las tecnologías web más revolucionarias de la


actualidad, pues proporciona todo un mecanismo de desarrollo de aplicaciones
totalmente desacoplado del backend y ejecutado 100% del lado del cliente.

React fue lanzado por primera vez en 2013 por Facebook y es actualmente
mantenido por ellos mismo y la comunidad de código abierto, la cual se extiende
alrededor del mundo. React, a diferencia de muchas tecnologías del desarrollo
web, es una librería, lo que lo hace mucho más fácil de implementar en muchos
desarrollos, ya que se encarga exclusivamente de la interface gráfica del
usuario y consume los datos a través de API que por lo general son REST.
El nombre de React proviene de su capacidad de crear interfaces de usuario
reactivas, la cual es la capacidad de una aplicación para actualizar toda la
interface gráfica en cadena, como si se tratara de una formula en Excel, donde
al cambiar el valor de una celda automáticamente actualiza todas las celdas que
depende del valor actualizado y esto se repite con las celdas que a la vez
dependía de estas últimas. De esta misma forma, React reacciona a los cambios
y actualiza en cascada toda la interface gráfica.

Uno de los datos interesantes de React es que es ejecutado del lado del
cliente (navegador), y no requiere de peticiones GET para cambiar de una
página a otra, pues toda la aplicación es empaquetada en un solo archivo
JavaScript (bundle.js) que es descargado por el cliente cuando entra por primera
vez a la página. De esta forma, la aplicación solo requerirá del backend para
recuperar y actualizar los datos.

Fig. 1 - Comunicación cliente-servidor

React suele ser llamado React.js o ReactJS dado que es una librería de
JavaScript, por lo tanto, el archivo descargable tiene la extensión .js, sin
embargo, el nombre real es simplemente React.

Página | 24
Server Side Apps vs Single Page Apps

Para comprender como trabaja React es necesario entender dos conceptos


claves, los cuales son Server side app (Aplicaciones del lado del servidor) y Single
page app (Aplicaciones de una sola página)

Server side app

Las aplicaciones del lado del servidor, son aquellas en las que el código fuente
de la aplicación está en un servidor y cuando un cliente accede a la aplicación, el
servidor solo le manda el HTML de la página a mostrar en pantalla, de esta
manera, cada vez que el usuario navega hacia una nueva sección de la página,
el navegador lanza una petición GET al servidor y este le regresa la nueva página.

Esto implica que cada vez que el usuario de click en una sección se tendrá que
comunicar con el servidor para que le regresa la nueva página, creado N
solicitudes GET para N cambios de página. En una página del lado del servidor,
cada petición retorna tanto el HTML para mostrar la página, como los datos que
va a mostrar.

Fig. 2 - Server side apps Architecture

Como vemos en la imagen, el cliente lanza un GET para obtener la nueva página,
el servidor tiene que hacer un procesamiento para generar la nueva página y
tiene que ir a la base de datos para obtener la información asociada a la página
de respuesta. La nueva página es enviada al cliente y este solo la muestra en
pantalla. En esta arquitectura todo el trabajo lo hace el servidor y el cliente
solo se limita a mostrar las páginas que el server le envía.

25 | Página
Single page app

Las aplicaciones de una sola página se diferencian de las aplicaciones del lado del
servidor debido a que gran parte del procesamiento y la generación de las
vistas las realiza directamente el cliente (navegador). Por otro lado, el
servidor solo expone un API mediante el cual, la aplicación puede consumir datos
y realizar operaciones transaccionales.

En este tipo de arquitectura, se libera al servidor de una gran carga, pues no


requiere tener que estar generando vistas para todos los usuarios conectados.

Fig. 3 - Single page apps Architecture.

Como vemos en esta nueva imagen, el cliente es el encargado de realizar


las vistas y realizar algunos procesamientos, mientras que el servidor solo
expone un API para acceder a los datos.

React es Single page app


Es muy importante resalta que React sigue la
arquitectura de Single page app, por lo que las vistas
son generadas del lado del cliente y solo se comunica
al backend para obtener y guardar datos.

Introducción a NodeJS

NodeJS es sin duda una de las tecnologías que más rápido está creciendo, y que
ya hoy en día es indispensable para cubrir posiciones de trabajo. NodeJS ha sido
revolucionario en todos los aspectos, desde la forma de trabajar hasta que
ejecuta JavaScript del lado del servidor.

Página | 26
NodeJS es básicamente un entorno de ejecución JavaScript del lado del
servidor. Puede sonar extraño, ¿JavaScript del lado del servidor? ¿Pero que no
JavaScript se ejecuta en el navegador del lado del cliente? Como ya lo platicamos,
JavaScript nace inicialmente en la década de los noventas por los desarrolladores
de Netscape, el cual fue creado para resolver problemas simples como validación
de formularios, alertas y alguna que otra pequeña lógica de programación, nada
complicado, sin embargo, JavaScript ha venido evolucionando hasta convertirse
en un lenguaje de programación completo. NodeJS es creado por Ryan Lienhart
Dahl, quien tuvo la gran idea de tomar el motor de JavaScript de Google Chrome
llamado V8, y montarlo como el Core de NodeJS.

Fig. 4 - Arquitectura de NodeJS

Como puedes ver en la imagen, NodeJS es en realidad el motor de JavaScript V8


con una capa superior de librerías de NodeJS, las cuales se encargan de la
comunicación entre el API de NodeJS y el Motor V8. Adicionalmente, se apoya de
la librería Libuv la cual es utilizada para el procesamiento de entradas y salidas
asíncronas.

Cabe mencionar que NodeJS es Open Source y multiplataforma, lo que le ha


ayudado a ganar terrenos rápidamente.

Que es NodeJS (Según su creador):

Node.js® es un entorno de ejecución para JavaScript construido con el motor de


JavaScript V8 de Chrome. Node.js usa un modelo de operaciones E/S sin bloqueo
y orientado a eventos, que lo hace liviano y eficiente. El ecosistema de paquetes
de Node.js, npm, es el ecosistema más grande de librerías de código abierto en
el mundo.

27 | Página
NodeJS y la arquitectura de Micro Servicios

Cuando hablamos de NodeJS es imposible no hablar de la arquitectura de


Microservicios, ya que es una de las arquitecturas más utilizadas y que están de
moda en la actualidad. La cual consiste en separar una aplicación en pequeños
módulos que corren en contenedores totalmente aislados, en lugar de tener
aplicaciones monolíticas que conglomeran toda la funcionalidad un mismo
proceso.

Fig. 5 - Arquitectura monolítica vs Microservices

En una arquitectura monolítica todas las aplicaciones escalan de forma


monolítica, es decir, todos los módulos son replicados en cada nodo, aunque no
se requiera de todo ese poder de procesamiento para un determinado módulo,
en cambio, en una arquitectura de microservicios, los módulos se escalan a como
sea requerido.

Todo esto viene al caso, debido a que NodeJS se ha convertido en unos de los
servidores por excelencia para implementar microservicios, ya que es muy ligero
y puede ser montado en servidores virtuales con muy pocos recursos, algo que
es imposible con servidores de aplicaciones tradiciones como Wildfy, Websphere,
Glashfish, IIS, etc.

Hoy en día es posible rentar un servidor virtual por 5 USD al mes con 1GB de
RAM y montar una aplicación con NodeJS, algo realmente increíble y es por eso
mi insistencia en que NodeJS es una de las tecnologías más prometedoras
actualmente. Por ejemplo, yo suelo utilizar Digital Ocean, pues me permite rentar
servidores desde 5 usd al mes.

Página | 28
Introducción a MongoDB

MongoDB es la base de datos NoSQL líder del marcado, ya que ha demostrado


ser lo bastante madura para dar vida a aplicaciones completas. MongoDB nace
con la idea de crear aplicaciones agiles, que permita realizar cambios a los
modelos de datos si realizar grandes cambios en la aplicación. Esto es gracias a
que permite almacenar su información en documentos en formato JSON.

Además, MongoDB es escalable, tiene buen rendimiento y permite alta


disponibilidad, escalando de un servidor único a arquitecturas complejas de
centros de datos. MongoDB es mucho más rápido de lo que podrías imaginas,
pues potencia la computación en memoria.

Uno de los principales retos al trabajar con MongoDB es entender cómo funciona
el paradigma NoSQL y abrir la mente para dejar a un lado las tablas y las
columnas para pasar un nuevo modelo de datos de Colecciones y documentos,
los cuales no son más que estructuras de datos en formato JSON.

Actualmente existe una satanización contra MongoDB y de todas las bases de


datos NoSQL en general, ya que los programadores habituales temen salir de su
zona de confort y experimentar con nuevos paradigmas. Esta satanización se
debe en parte a dos grandes causas: el desconocimiento y la incorrecta
implementación. Me explico, el desconocimiento se debe a que los
programadores no logran salir del concepto de tablas y columnas, por lo que no
encuentra la forma de realizar las operaciones que normalmente hacen con una
base de datos tradicional, como es hacer un Join, crear procedimientos
almacenados y crear transacciones. Y Finalmente la incorrecta implementación
se debe a que los programadores inexpertos o ignorantes utilizan MongoDB para
solucionar problemas para los que no fue diseñado, llevando el proyecto directo
al fracaso.

Es probable que hallas notado que dije que MongoDB no soporta transacciones,
y es verdad, pero eso no significa que MongoDB no sirva, si no que no fue
diseñado para aplicaciones que requieren de transacciones y bloque de registro,
un que es posible simularlos con un poco de programación. Más adelante
analizaremos más a detalle estos puntos.

Bases de datos Relacionales VS MongoDB

Probablemente lo que más nos cueste entender cuando trabajamos con


MongoDB, es que no utiliza tablas ni columnas, y en su lugar, la información se
almacena en objetos completos en formato JSON, a los que llamamos
documentos. Un documento se utiliza para representar mucha información
contenida en un solo objeto, que, en una base de datos relacional, probablemente
guardaríamos en más de una tabla. Un documento MongoDB suele ser un objeto
muy grande, que se asemejan a un árbol, y dicho árbol suele tener varios niveles
de profundidad, debido a esto, MongoDB requiere de realizar joins para armar

29 | Página
toda la información, pues un documento por sí solo, contiene toda la información
requerida

Otros de los aspectos importantes de utilizar documentos, es que no existe una


estructura rígida, que nos obligue a crear objetos de una determinada estructura,
a diferencia una base de datos SQL, en la cual, las columnas determinan la
estructura de la información. En MongoDB no existe el concepto de columnas,
por lo que los dos siguientes objetos podrían ser guardados sin restricción alguna:

1. { 6. {
2. "name": "Juan Perez", 7. "name": "Juan Perez",
3. "age": 20, 8. "age": 20,
4. "tel": "1234567890" 9. "tels": [
5. } 10. "1234567890",
11. "0987654321"
12. ]
13. }

Observemos que los dos objetos son relativamente similares, y tiene los mismos
campos, sin embargo, uno tiene un campo para el teléfono, mientras que el
segundo objeto, tiene una lista de teléfonos. Es evidente que aquí tenemos dos
incongruencias con un modelo de bases de datos relacional, el primero, es que
tenemos campos diferentes para el teléfono, ya que uno se llama tel y el otro
tels. La segunda incongruencia, es que la propiedad tels, del segundo objeto,
es en realidad un arreglo, lo cual en una DB relacional, sería una tabla secundaria
unida con un Foreign Key.

El segundo objeto se vería de la siguiente manera modelado en una DB


relacional:

Como ya nos podemos dar una idea, en un DB relacional, es necesario tener


estructuras definidas y relacionadas entre sí, mientras que en MongoDB, se
guardan documentos completos, los cuales contiene en sí mismo todas las
relaciones requeridas. Puede sonar algo loco todo esto, pero cuando entremos a
desarrollar nuestra API REST veremos cómo utilizar correctamente MongoDB.

La relación entre React, NodeJs & MongoDB

Página | 30
Hasta este momento, ya hemos hablado de estas tres tecnologías de forma
individual, sin embargo, no hemos analizado como es que estas se combinan
para crear proyectos profesionales.

Resumiendo un poco lo ya platicado, React se encargará de la interfaz


gráfica de usuario (UI) y será la encargada de solicitar información al Backend
a medida que el usuario navega por la página. Por otra parte, tenemos NodeJS,
al cual utilizaremos para programar la lógica de negocio y la comunicación con
la base de datos, mediante NodeJS expondremos un API REST que luego React
consumirá. Finalmente, MongoDB es la base de datos en donde almacenamos
la información.

Fig. 6 - MERN Stack

Como vemos en la imagen, React está del lado del FrontEnd, lo que significa que
su único rol es la representación de los datos y la apariencia gráfica. En el
BackEnd tenemos a NodeJS, quien es el intermediario entre React y MongoDB.
MongoDB también está en el Backend, pero este no suele ser accedido de forma
directa por temas de seguridad.

Cuando una serie de tecnologías es utilizada en conjunto, se le llama Stack, el


cual hace referencia a todas las tecnologías implementadas. A lo largo de este
libro, utilizaremos el Stack MERN, el cual es el acrónimo de MongoDB, Express,
React & NodeJS, estas tecnologías suelen acompañarse con Webpack y Redux.
Puedes encontrar más información de MERN en la página web: http://mern.io/

Resumen

En este capítulo hemos dado un vistazo rápido a la historia de JavaScript y como


es que ha evolucionado hasta convertirse en un lenguaje de programación
completo. De la misma forma, hemos analizado como es que JavaScript es la
base para tecnologías tan importantes como React y NodeJS.

31 | Página
También hemos analizado de forma rápida a React, NodeJS y MongoDB para
dejar claro los conceptos y como es que estas 3 tecnologías se acoplan para dar
soluciones de software robustas.

Por ahora no te preocupes si algún concepto no quedo claro o no entiendes como


es que estas 3 tecnologías se combinan, ya que más adelante entraremos mucho
más a detalle.

Página | 32
Preparando el ambiente de
desarrollo
Capítulo 2

Este capítulo lo reservaremos exclusivamente para la instalación de todas las


herramientas que utilizaremos a lo largo de este libro. También lo
aprovecharemos para analizar algunas de sus características y crearemos
nuestro primer proyecto con React.

Instalando Atom y algunos plugIn interesantes

Atom es un editor relativamente nuevo, lanzado en el 2014 por GitHub, el


repositorio de código más grande del mundo. A pesar de su corta edad, se ha
logrado posicionar entre los editores de código más populares de la actualidad.
Sobresaliendo su utilización en proyectos basados en JavaScript, como es el caso
de React y NodeJS.

Cabe mencionar que Atom, no es un IDE como tal, sino más bien, un editor de
texto, lo cual lo hace una herramienta robusta pero no tan sofisticada como un
IDE de programación completo, como sería Webstorm, Eclipse o Visual Studio.
En este libro nos hemos inclinado por Atom debido a que es una herramienta
ampliamente utilizada y es open source, lo que te permitirá descargarlo e
instalarlos sin pagar una licencia. Sin embargo, si tú te sientes cómodo en otro
editor o IDE, eres libre de utilizarlo.

Instalación de Atom

Instalar Atom esta simple como descargarlo de la página oficial. Atom es


compatible con Windows, Linux y Mac, por lo que no deberías de tener problemas
con tu sistema operativo.

Instalar Atom es muy simple, tan solo es necesario seguir los clásicos pasos de
siguiente, siguiente y finalizar, por lo que no te aburriré haciendo un tutorial de
como instalarlo.

33 | Página
En las siguientes ligas encontrarás el procedimiento de instalación actualizado
por sistema operativo:

 Windows: http://flight-manual.atom.io/getting-
started/sections/installing-atom/#platform-windows
 Mac: http://flight-manual.atom.io/getting-started/sections/installing-
atom/#platform-mac
 Linux: http://flight-manual.atom.io/getting-started/sections/installing-
atom/#platform-linux

Fig. 7 - Vista previa del editor ATOM

Una vez finalizada la instalación, aparecerá el ícono de Atom en el escritorio o


lanzadores. Lo ejecutamos y nos deberá aparecer una pantalla como la siguiente:

Fig. 8 - Pantalla de bienvenida de Atom

Instalar PlugIns

Página | 34
Ya en este punto, solo procederemos a instalar algunos plugins interesantes. Para
esto, nos dirigiremos al menú superior y presionaremos en File > Settings, una
vez allí, veremos una pestaña como la siguiente.

Fig. 9 - Atom Settings

Ya en settings, nos vamos a la pestaña de packages, y buscaremos los


siguientes plugin:

 language-babel: Nos ayudará con la sintaxis de React


 file-icons: Nos proporciona una serie de iconos para representar mejor
los archivos del proyecto, este plugin es meramente visual y no es
obligatorio

Si todo salió bien, deberás de poder ver los plugins de la siguiente manera:

Fig. 10 - Atom plugins

Estos son los dos plugins que, recomiendo para empezar, pero puedes navegar
un poco para ver toda la gran lista de plugins disponibles, y la gran mayoría
escritos por contribuidores de código libre.

35 | Página
Existen otros plugins interesantes para el desarrollo, que, si bien no los
aprovecharemos en este libro, si te que recomiendo que los análisis, estos son:

 color-picker: remplaza los caracteres de color #fafafa de HTML y CSS3


por el color real, así mismo, permite definir un color por medio de una
paleta de colores.
 emmet: el plugin estrella de los desarrolladores web, permite agilizar la
escritura de código por medio de atajos y sintaxis simplificada, puedes
ver la documentación en https://emmet.io/

Dejo para el final el plugin active-power-mode, el cual te hace sentir el


verdadero poder de programar, verás cómo tu código se extrémese a medida
que escribir, la experiencia es indescriptible, tienes que verlo para creer. Lo
instalas y escribe código lo más rápido que puedas para verlo en acción .

Finalmente, puedes aprovechar que estas en settings para ver los temas
disponibles, incluso descargar algunos de la comunidad.

Instalando NodeJS & npm

Instalar NodeJS es también realmente simple, pues tan solo será necesario
descargarlo de su página oficial https://nodejs.org, asegurándonos de tomar la
versión correcta para nuestro sistema operativo. En la siguiente página
encontraras la guía de instalación para los diferentes sistemas operativos:
https://nodejs.org/es/download/package-manager/

Una vez descargado, nuevamente seguimos el procedimiento habitual, siguiente,


siguiente, finalizar. Con esto, habremos instalado el Runtime de NodeJS y el
Gestor de Paquetes de Node o NPM por sus siglas en inglés.

Para asegurarnos de que todo salió bien, deberemos entrar a la terminal de


comandos y ejecutar el comando “npm -v”, esto nos deberá arrojar la versión de
NodeJS instalada.

Página | 36
Fig. 11 - Comprobando la instalación de NodeJS.

Esto será todo lo que tendremos que hacer por el momento. Más adelante
veremos cómo ejecutarlo y descargar módulos con NPM.

Instalando MongoDB

En el caso de MongoDB no necesitaremos hacer una instalación como tal, pues


aprovecharemos la infraestructura de MongoDB para crear nuestra propia base
de datos en la nube y totalmente gratis.

Lo primero que deberemos hacer será crea una cuenta en MongoDB Atlas, al
entrar verás una pantalla como la siguiente:

Fig. 12 - Crear cuenta en MongoDB

Presionaremos el botón que dice “Get started free” para iniciar el registro. Nos
llevará a un pequeño formulario en donde tendremos que capturar los datos que
nos solicite.

Una vez creada la cuenta, nos redirigirá a la consola de administración. Si es la


primera vez que entramos nos abrirá otro formulario para crear nuestra primera
base de datos. El formulario se ve como a continuación:

37 | Página
Fig. 13 - Crear una base de datos de MongoDB.

Lo primero que debemos de capturar es el Cluster Name, que es este caso le


he puesto Mini-Twitter y luego debemos de seleccionar la Opción Free que
sale en la parte de abajo. Vamos a dejar por default los demás valores, pues
cambiar la configuración nos moverá a un plan de paga y no queremos eso.

Seguido de eso, nos vamos a ir hasta debajo de la pantalla para capturar un


nombre de usuario y password para nuestra base de datos:

Fig. 14 - Captura de usuario y password para crear la base de datos.

Como usuario ponemos twitter y para el password podemos poner cualquier


password que queramos o autogenerarlo con el botón de arriba. Finalmente
presionamos el botón que dice Confirm & Deploy.

Una vez que finalicemos este paso, nos mandará nuevamente al dashboard, en
donde vemos todas las bases de datos. Normalmente el proceso de creación de
la base de datos tarde unos minutos, por lo que desde esta pantalla podrás ver
el avance. Cuando el proceso termine, podremos apreciar nuestra base de datos
operativa:

Página | 38
Fig. 15 - Cluster de MongoDB creado.

NOTA: Si ya habías entrado antes una base de datos y no te sale esta pantalla,
busca el botón que dice Build a New Cluster.

Quiero aclarar que he estado utilizando erróneamente el término “Base de datos”,


pues en realidad lo que hemos creado, es un cluster, es decir, un conjunto de 3
bases de datos funcionando en paralelo. Sin embargo, eso ahora no nos tiene
que preocupar.

Habilitando el acceso por IP

En esta misma pantalla deberemos ir a la pestaña de “Security” y luego “IP


Whitelist” como vemos en la siguiente imagen:

Fig. 16 - Cluster Security

Una vez allí, daremos click en el botón que dice “+ ADD IP ADDRESS” y saldrá
una pantalla como la siguiente:

39 | Página
Fig. 17 - Autorizar todas las IP’s

En esta sección, podemos determinar desde que IP’s nos podemos conectar, lo
cual es muy importante en ambiente productivo, sin embargo, dado que estamos
desde una PC con IP cambiante, debemos definir que nos permite todas las IP’s
mediante el botón “ALLOW ACCESS FROM ANYWHERE” y presionamos el
botón de guardar.

Hasta este punto hemos terminado de configurar nuestra base de datos en la


Nube. Realmente fácil, ¿no es así?

Te recomiendo te des un tour por la aplicación y que conozcas cada detalle que
nos ofrece. Entre las características más interesantes son las métricas, pues no
dice el número de transacciones, conexiones activas, número de operaciones,
ancho de banda utilizado, etc. También cuenta con alertas, Respaldos, etc.
Desafortunadamente algunas características son de pago, pero con las cosas que
nos da free es más que suficiente para empezar.

Otra de las opciones es instalar MongoDB local en tu equipo, sin embargo,


recomiendo ampliamente utilizarlo desde la nube, pues nos ahorramos la
instalación, iniciar y detener la base de datos para que no consuma recursos.
Como sea, si tu deseas instalarlo local, te dejo la siguiente liga de la
documentación de MongoDB.

https://docs.mongodb.com/manual/administration/install-community/

Instalando Compass, el cliente de MongoDB

Una vez que la base de datos esta activa y funcionando, nos queda solo un paso,
instalar un cliente para conectarnos a la base de datos de MongoDB.

Página | 40
En el mercado existen varios productos que nos permiten conectarnos a una base
de datos Mongo, como lo son:

 Compass
 Mongobooster
 Estudio 3T
 Robomongo
 MongoVue
 MongoHub
 RockMongo

Y seguramente hay más. hay algunos muy simples, otros muy completos, hay
gratis otros de paga, es cuestión de que te des una vuelta a la página de cada
uno para que veas qué características tiene.

En la práctica, a mí me gusta mucho Estudio 3T, pues es uno de los más robustos
y completos, pero tiene el inconveniente que es de paga, por esa razón, nos
iremos por la segunda mejor opción que es Compass y que además es Open
Source.

Para descargarlo, regresaremos al Dashboard de MongoDB y presionamos la


última opción que dice “Connect with MongoDB Compass” como se ve en la
siguiente imagen:

Fig. 18 - Download MongoDB Compass

Seguido de esto, una nueva ventana aparecerá, desde la cual podremos


descargar la versión adecuada para nuestro sistema operativo. Existe una versión
para Windows, Mac y Linux, por lo que no hay pretexto para no instalarla.

41 | Página
Fig. 19 - Seleccionado versión de MongoDB Compass.

Indicamos que estamos utilizando una versión superior a la 1.12 presionamos el


botón que dice “COPY”, el cual nos copia el String de conexión para conectarnos
a la base de datos. El único campo que tendríamos que capturar por separado es
el password.

Una vez terminado de instalar, Compass nos reconocerá el String de conexión


directamente desde el portapapeles y solo tendremos que poner el password. Si
no reconoce el String de conexión, podemos ir al menú principal y presionar
Connect  Connecto To. Una vez conectado, tendremos una pantalla como la
siguiente:

Página | 42
Fig. 20 - Conexión exitosa a MongoDB.

Del lado derecho izquierdo podemos ver las opciones “admin”, “local” y “test”,
de esas tres, la que nos interesa es “test”, sin embargo, como todavía no vamos
a hacer nada con ella. Más adelante a medida que vallamos trabajando con el
proyecto Mini-Twitter, vamos a ir viendo cómo se van creando las colecciones (lo
que conocemos como tablas en SQL).

En este punto ya nos deberíamos sentir muy orgullosos, pues ya hemos creado
un clúster en la nube y nos hemos logro conectar.

NOTA: Si no vez la base de datos “test” no te preocupes, más adelante cuando


nos conectemos por NodeJS aparecerá.

Creando mi primer proyecto de React

Estas es sin duda la sección más esperada para todos, pues por fin empezaremos
a programar con React, puede que de momento hagamos algo muy simple, pero
a medida que avancemos en el libro iremos avanzando en un proyecto final, el
cual, contemplará todo el conocimiento de este libro. Entonces, sin más
preámbulos, comencemos.

Estructura base de un proyecto

Existen básicamente dos formas para crear un proyecto en React, crear


manualmente todos los archivos necesarios o utilizar utilerías que nos ayudan a
inicializar el proyecto de forma automática.

43 | Página
Puede que resulte obvio que la mejor forma es mediante las utilerías, pero como
en este punto queremos aprender a utilizar React, entonces tendremos que
iniciar de la forma difícil, es decir, crea a mano cada archivo del proyecto.

Creación un proyecto paso a paso

Los que viene de trabajar de entornos de IDE’s, es muy probable que estén
acostumbrados a crear proyectos mediante Wizzards, los cuales ya nos crean
todo el proyecto y sus archivos, pero en esta sección aprenderemos a crearlo de
forma manual.

Lo primero que tendremos que hace es crear una carpeta sobre la que estaremos
trabajando, puede estar en cualquier dirección del disco duro, en este caso,
crearemos una carpeta llamada TwitterApp y nos ubicaremos en esta carpeta por
medio de la consola:

Fig. 21 - Creando el directorio del proyecto

En mi caso, yo estoy creando mi proyecto en el Path


C://Libros/React/TwitterApp y me dirijo a la carpeta mediante el comando cd.

Una vez ubicados en la carpeta, deberemos ejecutar el comando, npm init, lo


que iniciará un proceso de inicialización de un proyecto, en el cual nos pedirá los
siguientes datos:

Página | 44
 name: nombre del proyecto, no permite camel case, por lo que tendrá
que ser todo en minúsculas. En este caso ponemos twitter-app.
 version: versión actual del proyecto, por default es 1.0.0, por lo que
solo presionamos enter sin escribir nada.
 description: una breve descripción del proyecto, en nuestro caso
podemos poner Aplicación de redes sociales, o cualquiera otra
descripción, al final, es meramente descriptiva.
 entry point: indica el archivo principal del proyecto, en nuestro caso no
nos servirá de nada, así que presionamos enter para tomar el valore por
default.
 test command: nos permite definir un comando de prueba,
generalmente se imprime algo en pantalla para ver que todo anda bien.
En nuestro caso, solo presionamos enter y dejamos el valor por default.
 git repository: si nuestro proyecto está asociado a un repositorio de git,
aquí podemos poner la URL, por el momento, solo presionamos enter.
 keywords: como su nombre lo dice, palabras clave que describen el
proyecto. Nuevamente tomamos el valor por defecto.
 author: permite establecer el nombre del autor del proyecto, puede ser
el nombre de una persona o empresa. En mi caso pongo mi nombre
Oscar Blancarte, pero tú puedes poner tu nombre.
 license: se utiliza en proyectos que tienen una determinada licencia,
para prevenir a los que utilicen el código de tu proyecto. En nuestro
caso, dejamos los valores por defecto.

Finalmente, el asistente nos arrojara un resumen de los datos capturados, así


como una vista previa del archivo package.json, el cual analizaremos más
adelante. Terminamos el asistente capturando yes.

45 | Página
Fig. 22 - Inicialización del proyecto

Una vez finalizado todos los pasos, podremos ver que, en la carpeta del proyecto,
se habrá creado un nuevo archivo llamado package.json, el cual se verá de la
siguiente manera:

1. {
2. "name": "twitter-app",
3. "version": "1.0.0",
4. "description": "Aplicación de redes sociales",
5. "main": "index.js",
6. "scripts": {
7. "test": "echo \"Error: no test specified\" && exit 1"
8. },
9. "author": "Oscar Blancarte",
10. "license": "ISC"
11. }

Como puedes ver, el contenido del archivo, es exactamente lo que escribimos


por consola, por lo que tu podrías haber creado a mano el archivo y el resultado
hubiera sido el mismo.

El archivo package.json es muy importante, ya que desde aquí se administrarán


las dependencias del proyecto. Pero tampoco es un archivo muy complejo al que
debamos prestar especial atención, al menos por el momento.

Página | 46
Index.html

El siguiente paso será crear una página de inicio, la cual por lo general solo
importa un script de JavaScript y una hoja de estilos. Este archivo lo llamaremos
index.html y lo crearemos en la raíz del proyecto, el cual se verá de la siguiente
manera:

1. <!DOCTYPE html>
2. <html>
3. <head>
4. <title>Mini Twitter</title>
5. <link rel="stylesheet" href="/public/resources/css/styles.css">
6. </head>
7. <body>
8. <div id="root"></div>
9. <script type="text/javascript" src="/public/bundle.js"></script>
10. </body>
11. </html>

Un dato curioso de React, es que este será la única página HTML que tendremos
en todo el proyecto, pues como ya lo hablamos, las aplicaciones en React se
empaquetan en un solo archivo JavaScript, el cual denominaremos bundle.js.
Cuando el usuario accede a la página, iniciará la descarga del Script, una vez
descargado se ejecutará y remplazará el div con id=root, por la aplicación
contenida en bundle.js.

webpack.config.js

El siguiente paso será crear el archivo de configuración de Webpack. Este archivo


lo analizaremos más adelante, pero por lo pronto, lo crearemos en la raíz del
proyecto con el nombre webpack.config.js y nos limitaremos a copiar el archivo
para que quede de la siguiente manera:

1. module.exports = {
2. entry: [
3. __dirname + "/app/App.js",
4. ],
5. output: {
6. path: __dirname + "/public",
7. filename: "bundle.js",
8. publicPath: "/public"
9. },
10.
11. module: {
12. loaders: [{
13. test: /\.jsx?$/,
14. exclude: /node_modules/,
15. loader: 'babel-loader',
16. query:{
17. presets: ['env','react']
18. }
19. }]
20. }
21. }

47 | Página
Recuerda que no importa si no entiendas nada en este archivo, más adelante lo
retomaremos en la sección de Webpack.

Dependencias del proyecto

Ahora regresaremos al archivo package.json y agregaremos las dependencias


necesarias para poder ejecutar Webpack y React. Nuevamente, no te preocupes
por entender esta parte, ya que las dependencias las analizaremos más adelante,
en este mismo capítulo.

1. {
2. "name": "twitter-app",
3. "version": "1.0.0",
4. "main": "index.js",
5. "description": "Aplicación de redes sociales",
6. "author": "Oscar Blancarte",
7. "license": "ISC",
8. "devDependencies": {
9. "webpack": "^1.12.*",
10. "webpack-dev-server": "^1.10.*",
11. "babel-preset-react": "^6.24.1"
12. },
13. "dependencies": {
14. "react": "^15.0.0",
15. "react-dom": "^15.0.0",
16. "babel-core": "^6.24.1",
17. "babel-loader": "^6.4.1",
18. "babel-preset-env": "^1.7.0"
19. }
20. }

Para finalizar la configuración del archivo package.json, deberemos agregar las


secciones devDependencies y dependencies, los cuales son secciones especiales
para referenciar las dependencias del proyecto.

Una vez aplicados estos últimos cambios, será necesario ejecutar le comando npm
install sobre la carpeta del proyecto para descargar las dependencias.

Fig. 23 - Instalando dependencias con npm install

Página | 48
Instalar actualizaciones
Cada vez que se agregue o modifique una dependencia
del archivo package.json, será necesario instalar los
nuevos módulos mediante el comando npm install, el
cual se deberá ejecutar en la raíz del proyecto. De lo
contrario, los módulos no estarán disponibles en
tiempo de ejecución.

styles.css

Dado que todas las páginas web deben de verse atractivas, es necesario crear al
menos un archivo de estilos, en el cual iremos declarando las clases de estilo que
utilizaremos a lo largo del libro. Para esto, será necesario crear la siguiente
estructura de carpetas iniciando desde la raíz del proyecto.
/public/resources/css, es importante respetar correctamente el path, ya que de
lo contrario, nuestra página no cargara los estilos. Dentro de la carpeta css,
crearemos un archivo llamado styles.css, el cual se verá de la siguiente manera:

1. body{
2. background-color: #F5F8FA;
3. }

Por el momento, solo estableceremos el color de fondo del body en un gris suave,
y más adelante iremos complementando los estilos.

Observemos que el path del archivo de estilos, corresponde con el path definido
en el archivo index.html

1. <link rel="stylesheet" href="/public/resources/css/styles.css">

Un punto importante, es que podemos cambiar el path del archivo styles.css


dentro del proyecto, sin embargo, hay dos puntos a considerar, el archivo debe
de estar dentro de la carpeta public, pues hemos configurado webpack para
exponer los archivos en ese path, lo segundo a considerar, es que debemos
actualizar el archivo index.html para apuntar a la nueva URL.

App.js

Ha llegado el momento de entrar en React. App,js es el archivo principal del


proyecto, pues es el punto de entrada a la aplicación. De tal forma, que cuando
el usuario acceda a la página, este será el archivo que se ejecute.

Lo primero que debemos de hacer es crear una carpeta llamada app en la raíz del
proyecto. Dentro de esta carpeta, crearemos el archivo App.js el cual se verá de
la siguiente manera:

1. import React from 'react'


2. import { render } from 'react-dom'
3.
4. class App extends React.Component{
5.

49 | Página
6. render(){
7. return(
8. <h1>Hello World</h1>
9. )
10. }
11. }
12. render(<App/>, document.getElementById('root'));

Por ahora no entraremos en los detalles de React, pues el objetivo de este


capítulo es crear nuestra primera aplicación funcional. Sin embargo, nos vamos
a adelantar un poco para tratar de entender que está pasando.

En las primeras líneas del archivo, importamos las librerías de React (Líneas 1 y
2), por un lado, importamos React del módulo ‘react’ y después importamos la
función render del módulo ‘react-dom’.

Lo que sigues, es la declaración de una nueva clase llamada App, la cual extiende
de React.Component. La clase App tiene un método llamado render, el cual es el
encargado de generar la vista del componente, en este caso, está retornando el
elemento <h1>Hello Word</h1>. Finalmente, utilizamos la función render, para
remplazar el elemento root por el nuevo componente.

NOTA: recordemos que el elemento root está definido en el archivo index.html,


y su única función es servir como punto de montaje para React.

Configurando Script de ejecución

Ya estamos casi listo para ejecutar nuestra primera aplicación con React, solo
nos queda un paso más. Regresamos al archivo package.json y agregamos la
sección scripts, tal como lo vemos a continuación:

1. {
2. "name": "twitter-app",
3. "version": "1.0.0",
4. "main": "index.js",
5. "description": "Aplicación de redes sociales",
6. "scripts": {
7. "start": "node_modules/.bin/webpack-dev-server --progress"
8. },
9. "author": "Oscar Blancarte",
10. "license": "ISC",
11. "devDependencies": {
12. "webpack": "^1.12.*",
13. "webpack-dev-server": "^1.10.*",
14. "babel-preset-react": "^6.24.1"
15. },
16. "dependencies": {
17. "react": "^15.0.0",
18. "react-dom": "^15.0.0",
19. "babel-core": "^6.24.1",
20. "babel-loader": "^7.0.0",
21. "babel-preset-env": "^1.7.0"
22. }
23. }

La sección Script nos permitirá definir una serie de comandos pre definidos, para
compilar, ejecutar pruebas, empaquetar y desplegar/ejecutar la aplicación. Estos
scripts son ejecutados con ayuda de npm.

Página | 50
Hello Word!!

Finalmente ha llegado el momento de ejecutar nuestra primera aplicación con


React, por lo que nos dirigiremos a la consola y ejecutaremos el comando npm
start, el cual corresponde con el nombre del script definido en package.json.

Fig. 24 - Ejecutando nuestra primera aplicación.

La ejecución de este comando lanza una gran cantidad de texto en la consola,


pero ahora nos centraremos en ver el mensaje “webpack: Compiled
successfully”. Si vemos este mensaje, es que todo salió bien y la aplicación ya
debería de esta disponible en la URL http://localhost:8080/.

Fig. 25 - Hello World con React

Si al entrar a la URL puedes ver “Helllo World” quieres decir que has hecho
perfectamente bien todos los pasos, de lo contrario, será necesario que regreses
a los pasos anteriores y análisis donde está el problema. Muchas veces una coma,
un punto o una letra de más puede hacer que no funcione, así que no te
desanimes, ya que es raro que a alguien le salga bien a la primera.

Estructura del proyecto:

51 | Página
Tras haber realizados todos los pasos, el proyecto debería de quedar con la
siguiente estructura:

Fig. 26 - Estructura de un proyecto React

NOTA: es posible que adicional veas la carpeta node_modules, pero no te


preocupes simplemente deja como esta y más adelante la analizaremos.

Creación del proyecto con utilerías

Como verás, configurar un proyecto a mano, puede ser bastante complicado,


sobre todo, si es que no conoces todos los pasos y archivos necesarios para el
correcto funcionamiento. Es por esta razón que la comunidad desarrollo una
utilidad que nos ayuda a crear proyectos mucho más rápido, mediante unos
simples comandos de npm.

Esta utilidad es un módulo de npm llamado create-react-app. Para utilizarla


tendremos que instalarla utilizando el siguiente comando, npm install -g
create-react-app.

Fig. 27 - Instalando create-react-app

Página | 52
Una vez instalado el módulo, nos dirigiremos a la carpeta en donde queremos
crear el proyecto, en mi caso me dirigiré a C:\create-react-app y desde allí
ejecutare el comando “create-react-app twitter-app”. Este comando demorará
un tiempo considerable así que seamos pacientes.

Fig. 28 - Creando una app con create-react-app

Tras finalizar el comando, veremos un resultado como el anterior, y ya solo


restará acceder a la carpeta del proyecto cd twitter-app y ejecutar el comando
npm start.

Fig. 29 - Hello word con create-react-app

53 | Página
En este caso, la aplicación se iniciará en el puerto 3000, por lo que la URL será
http://localhost:3000/.

Esta es una herramienta que nos ayuda a iniciar un proyecto para React de una
forma muy rápida y simple, pero tiene el inconveniente que no prepara el
proyecto para usar Webpack. En la siguiente sección hablaremos acerca de
Webpack y veremos las ventajas de utilizarlo.

La estructura del proyecto se vería de la siguiente manera:

Fig. 30 - Estructura de un proyecto con create-react-app

Esta alternativa es muy buena si solo vas a crear una aplicación sin el BackEnd,
pero si ya requerimos utilizar NodeJS + Express, entonces sería bueno crear el
proyecto paso a paso.

En este libro trabajaremos con la estructura del proyecto paso a paso, ya que
necesitaremos más control sobre el proyecto y para eso utilizaremos Webpack.

Descargar el proyecto desde el repositorio

Si no quieres batallar creando un proyecto de React y quieres avanzar más


rápido, puedes simplemente descargar el proyecto ya listo para ser ejecutado
desde el repositorio del libro.

Página | 54
https://github.com/oscarjb1/books-reactiveprogramming.git

Y navegamos al branch “Capitulo-02-Preparando_ambiente_desarrollo” del


repositorio.

Gestión de dependencias con npm

En este punto ya hemos configurado un proyecto React con todo y sus


dependencias, pero hasta el momento no nos habíamos detenido a analizar cómo
es que npm administra las dependencias, y este es precisamente el objetivo de
esta sección.

Primero que nada, npm es la abreviatura de Node Package Manager o


Administrador de paquetes de Node, el cual sirve precisamente para gestionar
los paquetes o módulos en NodeJS. npm nace como una necesidad de
estandarizar la forma en que los desarrolladores gestionaban las dependencias,
pues antes de npm había herramientas como bower, browserify, gulp, grunt, etc.
que, entre sus funciones, estaba la de gestionar paquetes, lo que provocaba que
cada proyecto, y según su desarrollador, configurara de forma diferente las
dependencias. Esto desde luego provoca el gran problema de que no existe un
repositorio central, en el cual, se almacenen todos los módulos. Npm, además de
ser una herramienta, proporciona un repositorio en la nube para que cualquier
persona puede subir sus módulos y hacerlos disponibles para toda la comunidad.
Puedes dar un vistazo a https://www.npmjs.com/ para obtener más información.

Todas las dependencias de npm se almacenan en el archivo package.json, el cual


ya tuvimos la oportunidad de revisar. Sin este archivo, npm no reconocerá el
proyecto y no podremos agregar dependencias.

Inicializar un proyecto con npm


Recuerda que puedes utilizar el comando npm init
para inicializar un proyecto con npm, el cual creará el
archivo package.json.

Con npm es posible agregar 3 tipos de librerías, las cuales utilizaremos en


diferentes contextos, los tipos son:

1. Librerías locales: son librerías indispensables para la ejecución de un


proyecto, sin las cuales sería imposible ejecutar la aplicación. Estas están
disponibles únicamente para el proyecto al que fueron definidas
2. Librerías globales: son librerías que se instalan a nivel global, es decir,
no se requiere declarar la librería en cada proyecto, sino que es accesible
para todos los proyectos sin necesidad de importarlas.
3. Librerías de desarrollo: son librerías que se utilizan solamente en la
fase de desarrollo, las cuales son utilizadas para compilar, minificar o

55 | Página
comprimir archivos, también proporcionan utilidades en tiempo de
ejecución para debuger y muchas cosas más.

Instalando librerías locales

Solo será necesario introducir el comando:

npm install --save <module_name>

El comando install lo utilizaremos en los 3 tipos de dependencias y solo para


indicar que queremos instalar una dependencia. --save le dice a npm que
queremos la librería se guarde en el archivo package.json, si no lo ponemos, la
librería si se descargará, pero no quedará registra en archivo. <module_name> es
el nombre del módulo, se divide en dos secciones <name>@<version> name es el
nombre del módulo y versión, la versión del módulo que requerimos, en caso
de no declarar la versión, se instalará la última versión estable.

Cuando una dependencia es instalada con el parámetro --save, esta se


almacenará en la sección "dependencies" del archivo package.json.

Instalando librerías globales

Instalar librerías globales es exactamente lo mismo que las locales, con la


diferencia de que debemos de agregar el parámetro -g y el parámetro --save no
es requerido.

Npm install -g <module_name>

Como ya lo hablamos, las librerías globales están disponibles para todos los
proyectos si la necesidad de instarlos en cada proyecto.

Un ejemplo de esta librería es la de create-react-app que utilizamos para crear


un proyecto de forma automática con npm, si recordamos el comando create-
react-app twitter-app, veremos que no requerimos poner npm, ya que creaete-
creact-app se ha convertido en un comando a nivel global.

Librerías globales
Hay que tener cuidado con las librerías que instalamos
de forma global, pues podrían entrar en conflicto con
las librerías locales. Por lo que solo hay que instalar
de forma global las que tienen un motivo justificable.

Instalando librerías de desarrollo

Página | 56
El tercer tipo de librerías son las de desarrollo, las cuales son utilizadas para la
fase de desarrollo. La importancia de separar estas las librerías de desarrollo, es
no llevárnoslas a un ambiente de producción, pues puede hacer mucho más
pesada la página de lo que debería.

La instalación es muy similar a las locales, con la diferencia que le agregamos -


D como parámetro. La otra diferencia es que las dependencias se guardan en la
sección “devDependencies” del archivo package.json.

npm install --save -D <module_name>

Librerías de desarrollo
Es muy importante identificar que librerías será
utilizadas únicamente durante el desarrollo, pues
exportarlas a producción puede dar problemas de
rendimiento, además de una experiencia desagradable
para el usuario, pues tendrá que descargar un archivo
mucho más grande.

Análisis de las dependencias actuales

Una vez que ya hemos visto como npm gestiona las dependencias, ha llegado el
momento de retomar el archivo package.json para analizar las dependencias que
ya hemos instalado. Empecemos con las dependencias de desarrollo:

1. "devDependencies": {
2. "webpack": "^1.12.*",
3. "webpack-dev-server": "^1.10.*",
4. "babel-preset-react": "^6.24.1",
5. "babel-core": "^6.24.1",
6. "babel-loader": "^6.4.1",
7. "babel-preset-env": "^1.7.0"
8. }

57 | Página
 webpack: esta es la dependencia a Webpack, el cual analizaremos en la
siguiente sección.
 webpack-dev-server: dependencia al servidor de desarrollo que
proporciona Webpack, permite compilar todos los archivos y
empaquetarlos para generar el bundle.js.
 babel-preset-react: este es un módulo para trabajar con React. El cual
nos ayuda debido a que empaqueta todo lo necesario y no hace falta
buscar las dependencias de forma individual. Este paquete está
orientado a la Transpilación.
 babel-core: no hay mucho que decir, solo es la librería principal de
babel, el módulo encargado de la transpilación.
 babel-loader: este módulo es para utilizar babel como loader en
Webpack.
 babel-preset-env: paquete de librerías para la transpilación de
JavaScript en formato ECMAScript 6 (2015)

Continuamos con las dependencias locales:

1. "dependencies": {
2. "react": "^15.0.0",
3. "react-dom": "^15.0.0"
4. }

 react: obviamente es la librería de principal de Runtime de React.


 react-dom: librería para trabajar con el DOM, originalmente era parte
del módulo React, pero se decidió separar.

Desinstalando librerías

Para desinstalar un paquete ya instalado se realiza un procedimiento muy similar


al de instalación, sin embargo, en lugar de usar el comando install, utilizaremos
uninstall.

npm uninstall <options> <module_name>

mediante el comando uninstall le indicamos a npm nuestra intención de


desinstalar el paquete con el nombre <module_name>. Adicional, podemos
definir algunas opciones adicionales, como son:

-S o --save: borra la dependencia de la sección dependencies

-D o --save-dev: borra la dependencia de la sección devDependencies

--no-save: no borra la dependencia en el archivo package.json

Página | 58
Micro modularización

Algo a tomar en cuenta, es que NodeJS al igual que todos los módulos disponibles
en NPM (incluyendo React) son micro modulares, lo que quiere decir que son
librerías muy pequeñas, diseñadas para realizar una tarea muy específica, a
diferencia de los lenguajes de programación tradiciones, como el JDK de Java o
framework de .NET, los cuales pasan años antes de liberar una nueva versión, y
las versiones suelen tener grandes cambios.

Con NodeJS, cada proyecto es independiente, lo que permite que cada módulo
evolucione a su propio ritmo, lo cual es muy bueno, pues en el caso de Java o
NET, tenemos que esperar años, antes de tener mejoras en el lenguaje o las
librerías proporcionadas.

Esta micromodularización tiene grandes ventajas, pero también puede ser una
trampa para programadores inexpertos, pues la gran mayoría de los
programadores siempre buscan la última versión de un módulo, sin importar que
agrega o que compatibilidad rompe.

Instalar siempre la última versión de un módulo, no siempre es la mejor opción,


pues los paquetes de NPM evolucionan tan rápido, que con frecuencia rompen
compatibilidad con los demás paquetes que estamos utilizando. Es por eso que,
mi recomendación, es siempre instalar las versiones que aquí mencionamos,
incluso si hay nuevas versiones y tiene cambios interesantes. Lo mejor será que
entiendas los ejemplos tal cual se plantean y una vez que te sientas seguro,
puedes migrar a versiones más recientes.

Incluso, es muy probable que varios de los módulos que utilizamos en este libro,
liberen nuevas versiones mientras escribimos este libro, pero eso no quiere decir
que estamos desactualizados, ya que muchos de los features que agregan los
módulos, ni siquiera los utilizamos. Es por eso que mi recomendación es siempre
evaluar que nuevas cosas trae un módulo antes de actualizarlo. Como regla, las
versiones que corrigen bug siempre es bueno actualizarlas, las versiones
menores es importante investigar que nuevas cosas tiene y las mayores hay que
tratarlas con mucho cuidado, pues con frecuencia rompen compatibilidad.

Introducción a WebPack

Como su nombre lo dice, Webpack es un empaquetador, eso quiere decir que su


trabajo es obtener todos los archivos de nuestro proyecto, aplicar un
procesamiento y arrojarnos una versión mucho más compacta. Puedo no sonar
la gran cosa, pero la realidad es que facilita el trabajo como no tienes una idea.

59 | Página
Fig. 31 - Funcionamiento de Webpack

Como puedes ver en la imagen, Webpack tomará todos los archivos de nuestro
proyecto, los compilará, empaquetará, comprimirá y finalmente los minificara,
todo esto sin que nosotros tengamos que hacer prácticamente nada.

Uno de los aspectos más interesantes de Webpack, son los cargadores o loaders,
los cuales nos permite procesar diferentes tipos de archivos y arrojarnos un
resultado, un ejemplo de estos son los procesadores de SASS y LESS, que nos
permite compilar los archivos y arrojarnos CSS, también está el loader Babel,
que permite que el código JavaScript en formato ECMAScript 6 sea compatible
con todos los navegadores. También podemos configurar a Webpack para que
aplique compresiones a las imágenes y minificar el código (compactar).

Nuevo concepto: Minificar


Proceso por el cual un archivo como JavaScript, CSS,
HTML, etc. es compactado al máximo, con la finalidad
de reducir su tamaño y ser descargado más rápido por
los usuarios.

Por si esto fuera poco, Webpack nos proporciona un servidor de desarrollo


integrado, el cual es listo para ser utilizado. Webpack se podría ver como la
evolución de herramientas como Grunt, browserify, Gulp, etc, pues permite hacer
lo que ya hacían estas herramientas y algunas cosas extras.

Instalando Webpack

Instalar Webpack es mucho más simple de lo que creeríamos, pues solo falta
instalar la dependencia con npm como ya lo hemos visto antes.

npm install --save webpack

Página | 60
Este comando habilitará el uso de Webpack para el proyecto en cuestión. Pero
también es posible instalar Webpack a nivel global agregando el parámetro -g
como ya vimos.

Otra de las herramientas que nos proporciona Webpack es el servidor de


desarrollo, el cual podemos instalar de la siguiente manera:

npm install --save webpack-dev-server

Si prestaste atención, seguramente te habrás dado cuenta que el comando start


definido en package.json, utiliza el módulo webpack-dev-server. Veamos
nuevamente el comando para refrescar la memoria:

1. "scripts": {
2. "start": "node_modules/.bin/webpack-dev-server --progress"
3. }

Este script permite que cuando ejecutemos npm start, inicie una nueva instancia
del server webpack-dev-server, y el parámetro --progress es solo para ver el
avance a medida que compila los archivos.

Webpack-dev-server es solo para desarrollo


Este módulo es de gran ayuda únicamente en la fase
de desarrollo, pues permite levantar un servidor
fácilmente y automatizar la transpilación de los
archivos. Sin embargo, no es apropiado para un
ambiente productivo, más adelante veremos cómo
crear nuestro propio servidor para producción.

Sin este módulo, tendríamos que crear un servidor con NodeJS antes de poder
ejecutar un Hello World en React.

Puedes buscar más información de Webpack en su página oficial:


https://webpack.js.org/

El archivo webpack.config.js

Como ya hemos visto, webpack se configura a través del archivo


webpack.config.js y desde el cual podremos definir la forma en que empaquetará
nuestra aplicación.

Retomemos el archivo webpack.config.js que ya tenemos para analizarlo:

1. module.exports = {
2. entry: [
3. __dirname + "/app/App.js",

61 | Página
4. ],
5. output: {
6. path: __dirname + "/public",
7. filename: "bundle.js",
8. publicPath: "/public"
9. },
10.
11. module: {
12. loaders: [{
13. test: /\.jsx?$/,
14. exclude: /node_modules/,
15. loader: 'babel-loader',
16. query:{
17. presets: ['env','react']
18. }
19. }]
20. }
21. }

Este archivo simplemente exporta un objeto, el cual contiene toda la


configuración necesaria para el funcionamiento de Webpack. Las secciones del
archivo son:

 Entry: permite definir los puntos de entrada de la aplicación, es decir, la


página de inicio o cuantas páginas sea requeridas. En el caso de React,
solo es necesario tener un punto de montaje, por lo que hemos
referencia al archivo App.js.
 Output: en esta sección se define como es que los archivos procesados
serán guardados y finalmente, en que URL estarán disponibles. las
propiedades utilizadas son:
o Path: path o URL en la que los archivos procesados serán guardados.
o Filename: indica el nombre del archivo empaquetado, es decir, todos los
archivos JavaScript que encuentre los empaquetará en un solo archivo
llamado bundle.js.
o publicPath: con esta propiedad le indicamos a webpack-dev-server la URL
en la que estarán disponibles los archivos empaquetados. Recuerda que
en el archivo index.html hacemos referencia al archivo bundles.js y
styles.css iniciando con /public en el path.
 Module: esta es una sección que tiene muchos usos, por lo que nos
centraremos por lo pronto en los loaders, los cuales nos permitirán
“Transpilar” los archivos de React.
o Test: se define la expresión regular que se utilizará validar si un archivo
debe de ser procesado por el loader, en este caso, indicamos que tome los
archivos *.js | *.jsx.
o Exclude: indicamos archivos o path que deben de ser ignorados por el
loader. En este caso, le decimos que ignore todos los módulos de Node
(node_modules).
o Loader: simplemente es el nombre del loader.
o Query: se utiliza para pasar parámetros al loader, en este caso, le
indicamos que estamos trabajando con ECMAScript 6 (2015) y React.

Página | 62
Nuevo concepto: Transpilación
Hasta este momento, hemos utilizado incorrectamente
la palabra compilar, para referirnos al proceso por el
cual, convertimos los archivos de React a JavaScript
puro, sin embargo, el proceso por el cual se lleva a
cabo esto es Transpilación, que no es precisamente
una compilación, si no la conversión del código de
React a JavaScript compatible con todos los
navegadores.

Webpack puede parecer simple, pero es mucho más complejo de lo que parece,
tiene una gran cantidad de plugins y configuraciones que pueden ser requeridas
en cualquier momento. Puedes darle una revisada a la documentación oficial para
que te des una idea: https://webpack.github.io/docs/

React Developer Tools

Otra de las herramientas indispensables para debugear una aplicación


desarrollada en React, es el plugin React Developer Tools para Google Chrome
(también disponible para Firefox). El cual nos permitirá ver la estructura de la
aplicación, así como su estado y propiedades (veremos los estados y propiedades
más adelante).

A lo largo de este libro utilizaremos Chrome, pues es el navegador más utilizado


en la actualidad y con el mejor soporte. Por ello, accedemos a la siguiente URL
desde Chrome https://chrome.google.com/webstore y en la barra de búsqueda
poner ‘react developer tools’, he instalamos el siguiente plugin:

Fig. 32 - React developer tools

Una vez instalado probablemente tengas que reiniciar el navegador. Una vez
hecho esto, deberá aparecer el ícono de React a un lado de la barra de búsqueda.
Por default este ícono se ve gris, lo cual indica que la página en la que estamos,
no utiliza React. Para probar el funcionamiento del plugin, nos iremos a Facebook
y veremos que el ícono pasa a tener color. Esto nos indica que la página utiliza
React y es posible debugearla con el plugin.

63 | Página
Una vez que estemos en Facebook, daremos click derecho en cualquier parte de
la página y presionaremos la opción Inspeccionar. Una vez allí, nos vamos al tab
React:

Fig. 33 - Probando el plugin React developer tools

Desde esta sección, es posible ver los componentes React y saber en tiempo real,
el valor de su estado y propiedades. Por ahora no entraremos en los detalles,
pues primero necesitamos aprender los conceptos básicos como estados y
propiedades, antes de poder entender lo que nos arroja el plugin. Más adelante
retomaremos el plugin para analizar las páginas.

Página | 64
Resumen

Hemos concluido uno de los capítulos más complicados, pues nos tuvimos que
enfrentar a varias tecnologías, aprendimos nuevos conceptos y echamos a andar
nuestra primera aplicación con React, lo cual es un enorme avance.

Hasta este punto hemos aprendido a instalar React, NodeJS, y configurar una
base de datos Mongo en la nube, también aprendimos a gestionar dependencias
con npm, para finalmente introducirnos en Webpack.

De aquí en adelante, esto se pondrá mucho mejor, pues ya entraremos de lleno


a React y empezaremos a programar nuestros primeros componentes.

65 | Página
Introducción al desarrollo con
React
Capítulo 3

Ya en este punto, hemos creado nuestra primera aplicación, sin embargo, no


entramos en los detalles como para comprender todavía como programar con
React, es por ello, que en este capítulo buscaremos aprender los conceptos más
básicos para poder programar con React.

Antes de entrar de lleno a la programación, quiero contarte que React soporte


dos formas de trabajar, la primera es crear todos los elementos de una página
con JavaScript puro, y la segunda es mediante una sintaxis propia de React
llamada JSX.

Programación con JavaScript XML (JSX)

Una de las grandes innovaciones que proporciona React, es su propio llamado


JSX (JavaScript XML), el cual es una mescla entre HTML y XML que permite crear
vistas de una forma muy elegante y eficiente.

Si recordamos nuestra clase App.js, teníamos una función llamada render, la cual
tiene como finalidad crear una vista, esta función debe de retornar la página que
finalmente el usuario verá en pantalla. Veamos la función para recordar:

1. render(){
2. return(
3. <h1>Hello World</h1>
4. )
5. }

Observemos que la función regresa una etiqueta <h1>, la cual corresponde con la
etiqueta <h1> que podemos ver en el navegador:

Página | 66
Fig. 34 - Inspector de elementos de Google <h1>

Inspector de elementos
Todos los navegadores modernos nos permiten
inspeccionar una página para ver los elementos que la
componen. En el caso de Chrome, solo requieres dar
click derecho sobre la página y presionar
Inspeccionar.

Aunque la etiqueta <h1> pueda parecer HTML, la realidad es que no lo es, en


realidad ya estamos utilizando JSX. Uno de los éxitos de JSX es que es
extremadamente similar a programar en HTML, pero con las reglas de generación
de un XML.

Diferencia entre JSX, HTML y XML

Dado que JSX y HTML pueden ser muy similares, es muy fácil confundirnos y no
entender cuáles son sus diferencias, provocando errores de compilación o incluso
en tiempo de ejecución. Vamos a analizar las diferencias que existen entre JSX,
HTML y XML

67 | Página
Elementos balanceados

Cuando trabajamos con HTML es común encontrarnos con elementos self-closing


o que no requieres una etiqueta de cierre, como el caso de la etiqueta <img>. En
HTML podríamos declarar una imagen de la siguiente manera:

1. <img src='/images/img1.png' alt='mi imagen'>

Notemos que no tiene una etiqueta de cierre </img> ni termina en />, esto sería
totalmente válido en HTML, sin embargo, en JSX no lo es, ya que JSX utiliza las
reglas de XML, por lo que todos los elementos deben de cerrarse, incluso si en
HTML no es requerido.

Nuevo concepto: Self-closing


Son etiquetas de HTML que no requieren una etiqueta
de cierre o que se cierre en la misma declaración con
/>.

Hagamos una prueba, regresemos al archivo App.js y editemos la función render


para que se vea de la siguiente manera:

1. render(){
2. return(
3. <img src="/images/img1.png" alt="mi imagen">
4. )
5. }

Guardemos los cambios y veremos que Webpack detectará los cambios y tratará
de transpilar los cambios, dando un error en el proceso:

Fig. 35 - Error de elementos no balanceados.

Página | 68
Para corregir este error, es necesario cerrar el elemento, y lo podemos hacer de
dos formas:

Cerrando el elemento inmediatamente después de abrirlo con />.

1. <img src="https://facebook.github.io/react/img/logo.svg" alt="mi imagen"/>

O crear una etiqueta de cierre:

1. <img src="https://facebook.github.io/react/img/logo.svg"></img>

Puedes utilizar el método que más te agrade, al final el resultado será el mismo.

Elemento raíz único

Una de las principales reglas que tiene JSX es que solo podemos regresar un solo
elemento raíz. Esto quiere decir que no podemos retornas dos o más elementos
a nivel de la función return. Por ejemplo, en el componente App.js eliminamos
el <h1> para agregar una etiqueta <img>, pero ¿qué pasaría si quiero retornar las
dos al mismo tiempo?, bueno podría hacer lo siguiente:

1. render(){
2. return(
3. <h1>Hello World</h1>
4. <img src="https://facebook.github.io/react/img/logo.svg"></img>
5. )
6. }

Observemos que tanto <h1> como <img> están declarados al mismo nivel, lo que
quiere decir que tenemos dos elementos raíz, lo que es inválido para JSX.
Guardemos los cambios ver qué sucede:

Fig. 36 - Error de elemento raíz único

69 | Página
Como podemos ver, nuevamente sale un error al transpilar el archivo. ¿Esto
quieres decir que tengo que crear una clase para cada elemento?, la respuesta
es no, tan solo es necesario encapsular los dos elementos dentro de otro, como
podría ser un <div>. veamos cómo quedaría:

1. render(){
2. return(
3. <div>
4. <h1>Hello World</h1>
5. <img src="https://facebook.github.io/react/img/logo.svg"/>
6. </div>
7. )
8. }

Esta nueva estructura ya cumple con la regla de un elemento raíz único, en donde
el <div> sería el elemento raíz. Dentro del <div> ya es posible incluir cualquier
tipo de estructura sin restricciones.

Fig. 37 - Elemento raíz válido

Nuevo concepto: Elemento raíz


Se le conoce como elemento raíz el primero elemento
de un documento XML, el cual deberá contener todos
los demás elementos y ningún otro elemento podrá
estar a la misma altura que él.

Fragments

Página | 70
Como acabamos de ver, mediante JSX solo podemos regresar un elemento raíz,
sin embargo, existe ocasiones en las que es necesario retornar más de un solo
elemento sin tener un elemento padre, para esto React incorpora los Fragments,
los cuales son elementos dummy, lo que quiere decir que nos permite agregarlos
como padre de una serie de elementos, pero al momento de realizar el render
del componente, son ignorados.

Veamos el ejemplo anterior utilizando Fragments:

1. import React from 'react'


2. import { render } from 'react-dom'
3.
4. class App extends React.Component{
5.
6. render(){
7. return(
8. <React.Fragment>
9. <h1>Hello World</h1>
10. <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React
-icon.svg/210px-React-icon.svg.png"></img>
11. </React.Fragment>
12. )
13. }
14. }
15. render(<App/>, document.getElementById('root'));

Para utilizar un Fragment solo es necesario crear un elemento Fragment que se


encuentra dentro de la clase React <React.Fragment>.

El resultado que veremos en el navegador es el siguiente:

1. <html>
2. <head>
3. <title>Mini Twitter</title>
4. <link rel="stylesheet" href="/public/resources/css/styles.css">
5. </head>
6. <body>
7. <div id="root">
8. <h1>Hello World</h1>
9. <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-
icon.svg/210px-React-icon.svg.png">
10. </div>
11. <script type="text/javascript" src="/public/bundle.js"></script>
12. </body>
13. </html>

Puedes observar que los elementos <h1> y <img> quedaron al mismo nivel dentro
del elementos root, pero ya no tiene un div adicional que los encapsule.

Fragments a partir de React 16.2.0


Los Fragments fueron agregados a partir de la versión
16.2.0, por lo que es necesario actualizar las
dependencias de los módulos react y react-dom.
npm install [email protected] [email protected]

71 | Página
Camel Case en atributos

Otra de las características de JSX es que los atributos deben ser escritos en Camel
Case, esto quiere decir que debemos utilizar MAYUSCULAS entre cada palabra.

Nuevo concepto: Camel Case


CamelCase es un estilo de escritura que se aplica a
frases o palabras compuestas. El nombre se debe a
que las mayúsculas a lo largo de una palabra en
CamelCase se asemejan a las jorobas de un camello.

Un ejemplo muy claro de esto, es el evento onclick que tiene todos los elementos
de HTML. En JSX onclick no es correcto, por lo que tendría que escribirse como
onClick, notemos que tenemos la C mayúscula. Veamos qué pasa si poner de
forma incorrecta el atributo:

1. return(
2. <div>
3. <h1>Hello World</h1>
4. <img src="https://facebook.github.io/react/img/logo.svg"/>
5. <br/>
6. <button onclick={()=>alert('Hello World')}>Hello!!</button>
7. </div>
8. )

Si guardamos los cambios, no veremos un error como tal en la consola al


momento de realizar la transpilación, en su lugar obtendremos un error en tiempo
de ejecución, el cual lo podremos ver en el inspector.

Fig. 38 – Error de Camel Case

Página | 72
Adicional a esto, si presionamos el botón, no pasará nada, pues React no sabrá
qué hacer con eso.

Si intentamos nuevamente con el atributo en Camel Case, podremos ver una


gran diferencia, pues ya no nos aparecerá el error y si damos click en el botón,
este nos mandará el mensaje Hello World.

Fig. 39 - Hello World con Camel Case

Contenido dinámico y condicional

En esta sección analizaremos cómo es posible agregar contenido dinámico en


React, así como mostrar valores por medio de una variable o de forma
condicional.

Contenido dinámico

Lo primero que debemos de saber, es que React permite definir valores dinámicos
en prácticamente cualquier lugar. Estos valores pueden ser una variable, el
resultado de un servicio o incluso parámetros enviados a los componentes. Para
ellos es necesario agregar los valores en entre un par de llaves { }, tal como lo
vimos hace un momento con el evento onClick.

1. <button onclick={()=>alert('Hello World')}>Hello!!</button>

Veamos que la alerta está dentro de unas llaves, de lo contrario, React no sabrá
cómo interpretar el valor introducido.

73 | Página
Variables

Otra forma de representar contenido dinámico es por medio de variables. Veamos


el siguiente ejemplo:

1. render(){
2. let variable = {
3. message: "Hello World desde una variable"
4. }
5.
6. return(
7. <div>
8. <h1>{variable.message}</h1>
9. <img src="https://facebook.github.io/react/img/logo.svg"/>
10. <br/>
11. <button onClick={()=>alert('Hello World')}>Hello!!</button>
12. </div>
13. )
14. }

En este ejemplo, hemos definido una variable (línea 2) y luego la hemos utilizado
dentro de un bloque {}. Esto hace que React no interprete como código.

Fig. 40 - Hello World con variables

Valores condicionales

Además de mostrar variables, existen ocasiones donde el valor a mostrar está


condicionado, por tal motivo, debemos hacer una validación y dependiendo el
resultado, será el valor que mostremos en pantalla.

Veamos un ejemplo muy simple, supongamos que debo saludar al usuario según
su sexo. Por lo que antes de mostrar un mensaje, deberá validar el sexo. Para
hacer esta prueba, vamos a retomar el ejemplo pasado, donde saludamos con
una variable, pero agregaremos datos adicionales para saludar con un mensaje
diferente según el sexo.

Página | 74
La primera forma, es mediante expresiones ternarias:

1. render(){
2. let variable = {
3. sexo: "woman",
4. man: "Hola Amigo",
5. woman: "Hola Amiga"
6. }
7. return(
8. <div>
9. <h1>{variable.sexo === 'man' ? variable.man : variable.woman}</h1>
10. <img src="https://facebook.github.io/react/img/logo.svg"/>
11. <br/>
12. <button onClick={()=>alert('Hello World')}>Hello!!</button>
13. </div>
14. )
15. }

Observemos nuevamente la variable, hemos agregado una propiedad sexo, la


cual nos indicará el sexo del usuario, y las propiedades man y woman, que
utilizaremos para saludar según el sexo del usuario.

Fig. 41 - Hello Amiga

Prueba tú mismo a cambar el valor de la propiedad sexo, de woman a man y observa


los resultados.

Este método es bastante efectivo cuando solo tenemos dos posibles valores, pues
las expresiones ternarias nos regresan dos valores posibles. Pero qué pasa
cuando existen más de 2 posibles resultados, una sería anidar expresiones
ternarías, lo cual es posible, pero se crearía un código muy complicado y verboso.
La otra opción es trabajar la condición por fuera, de esta forma, podemos realizar
todas las validaciones que requiramos y al final solo imprimimos el resultado por
medio de una variable.

Retomando el ejemplo anterior, que pasaría si el sexo del usuario es indefinido,


es decir, no lo conocemos. En este caso, tendríamos que lanzar un mensaje más
genérico. Veamos cómo quedaría:

75 | Página
1. render(){
2. let variable = {
3. sexo: "",
4. man: "Hola Amigo",
5. woman: "Hola Amiga",
6. other: "Hola Amig@"
7. }
8. let message = null
9. if(variable.sexo === 'man'){
10. message = variable.man
11. }else if(variable.sexo === 'woman'){
12. message = variable.woman
13. }else{
14. message = variable.other
15. }
16. return(
17. <div>
18. <h1>{message}</h1>
19. <img src="https://facebook.github.io/react/img/logo.svg"/>
20. <br/>
21. <button onClick={()=>alert('Hello World')}>Hello!!</button>
22. </div>
23. )
24. }

Primero que nada, veamos que el sexo está en blanco, también que hemos
agregado una nueva propiedad other, la cual utilizaremos para saludar si no
conocemos el sexo del usuario. Por otra parte, observa la secuencia de
if..elseif..else, en ella, validamos el sexo del empleado, y según el sexo,
escribimos un valor diferente en la variable message. Finalmente, en el <h1> solo
imprimimos el valor de la variable message.

JSX Control Statements

A medida que un proyecto se va complicando, es muy común encontrarnos con


muchos if…elseif..else por todas los componentes, provocando que el proyecto
se vuelva cada vez más difícil de entender y de seguir. Es por ese motivo que
existe un plugin para Babel que permite extender el lenguaje JSX para agregar
condiciones mediante <tags>, lo cual facilita muchísimo el desarrollo, haciendo
que el código que escribamos sea mucho más simple y fácil de entender.

Instalar jsx-control-statements

Primero que nada, será necesario instalar el módulo mediante npm con el
siguiente comando:
npm install -D --save jsx-control-statements

Página | 76
Una vez terminada la instalación, se nos agregará la dependencia en el archivo
package.json en la sección de librerías de desarrollo.

Jsx-control-statement extiende al lenguaje


Jsx-control-statements no es una librería que
debamos importarla para utilizarla, sino más bien,
extiende al lenguaje JSX agregando nueva
funcionalidad.

El siguiente paso será incluir el módulo en el archivo webpack.config.js:

1. module.exports = {
2. entry: [
3. __dirname + "/app/App.js",
4. ],
5. output: {
6. path: __dirname + "/public",
7. filename: "bundle.js",
8. publicPath: "/public"
9. },
10.
11. module: {
12. loaders: [{
13. test: /\.jsx?$/,
14. exclude: /node_modules/,
15. loader: 'babel-loader',
16. query:{
17. presets: ['env','react'],
18. plugins: ["jsx-control-statements"]
19. }
20. }]
21. }
22. };

Solo hemos agregado el plugin en la línea 18, con esto Webpack extenderá el
lenguaje de JSX para soportar un set de estructuras de control, las cuales
analizaremos a continuación.

If

Anteriormente vimos cómo era necesario crear una expresión ternaria o crear
una estructura de if…else para saludar a nuestro usuario según el sexo, pero
ahora con el plugin jsx-control-statements el lenguaje se ha ampliado,
permitiéndonos crear la etiqueta <If>, la cual solo tiene un atributo llamado
condition, que deberá tener una expresión que se resuelva en booleano. Si la
expresión es true, entonces todo lo que este dentro de la etiqueta se mostrará.
Veamos cómo quedaría el ejemplo anterior:

1. render(){
2. let variable = {
3. sexo: "woman",

77 | Página
4. man: "Hola Amigo",
5. woman: "Hola Amiga",
6. other: "Hola Amig@"
7. }
8.
9. return(
10. <div>
11. <img src="https://facebook.github.io/react/img/logo.svg"/>
12. <br/>
13. <button onClick={()=>alert('Hello World')}>Hello!!</button>
14. <If condition={variable.sexo === 'man' }>
15. <h1>{variable.man}</h1>
16. </If>
17. <If condition={variable.sexo === 'woman' }>
18. <h1>{variable.woman}</h1>
19. </If>
20. </div>
21. )
22. }

Veamos que esta vez en lugar de crear una variable y luego asignarle el valor
mediante una serie de if…elseif…else, lo hacemos directamente sobre el JSX.

Un dato importante de <If> es que no nos permite poner <else> o <elseif>, por
lo que solo nos sirve cuando tenemos una expresión a evaluar.

Choose

La estructura de control Choose, nos permite crear una serie de condiciones que
se evalúan una tras otra, permitiendo tener un caso por default, exactamente lo
mismo que hacer un if…elseif…else.

En el ejemplo anterior pusimos un <If> para cada sexo, pero no tuvimos la


oportunidad de definir qué pasaría, si el sexo fuera diferente de man y woman,
y es aquí donde Choose encaja a la perfección. Veamos cómo quedaría este
ejemplo:

1. render(){
2. let variable = {
3. sexo: "",
4. man: "Hola Amigo",
5. woman: "Hola Amiga",
6. other: "Hola Amig@"
7. }
8. return(
9. <div>
10. <img src="https://facebook.github.io/react/img/logo.svg"/>
11. <br/>
12. <button onClick={()=>alert('Hello World')}>Hello!!</button>
13. <Choose>
14. <When condition={variable.sexo === 'man' }>
15. <h1>{variable.man}</h1>
16. </When>
17. <When condition={variable.sexo === 'woman'}>
18. <h1>{variable.woman}</h1>
19. </When>
20. <Otherwise>
21. <h1>{variable.other}</h1>
22. </Otherwise>

Página | 78
23. </Choose>
24. </div>
25. )
26. }

Observemos que Choose, permite definir una serie de <When>, donde cada una
tendrá una condición a evaluarse, si la condición de un <When> se cumple,
entonces su contenido se mostrará. En caso de que ningún <When> se cumpla, se
mostrará el valor que hay en <Otherwise> el cual no requiere de ningún atributo,
pues sería el valor por default.

For

La etiqueta <For> nos permite iterar una array con la finalidad de arrojar un
resultado para cada elemento de la colección. Hasta el momento no hemos visto
como imprimir un arreglo, por lo que iniciaremos con un ejemplo sin utilizar la
etiqueta <For> para poder comparar los resultados.

Lo que haremos será imprimir una lista de usuarios que tenemos en un array sin
utilizar jsx-control-statements:

1. render(){
2. let usuarios = [
3. 'Oscar Blancarte',
4. 'Juan Perez',
5. 'Manuel Juarez',
6. 'Juan Castro'
7. ]
8.
9. let userList = usuarios.map(user => {
10. return (<li>{user}</li>)
11. })
12.
13. return(
14. <div>
15. <ul>
16. {userList}
17. </ul>
18. </div>
19. )
20. }

Primero que nada, veamos la lista de usuarios (línea 2), la cual tiene 4 nombres,
seguido, lo que hacemos es iterar el array mediante el método map, esto nos
permitirá obtener el nombre individual de cada usuario en la variable user,
seguido, regresamos el nombre del usuario dentro de un tag <li> para
mostrarlos en una lista. Finalmente, en la respuesta del método render,
retornamos la lista de usuarios dentro de una lista <lu>.

Ya con este precedente, podemos comprobar cómo quedaría con <For>:

79 | Página
1. render(){
2. let usuarios = [
3. 'Oscar Blancarte',
4. 'Juan Perez',
5. 'Manuel Juarez',
6. 'Juan Castro'
7. ]
8.
9. return(
10. <div>
11. <For each="user" index="index" of={ usuarios }>
12. <li>{user}</li>
13. </For>
14. </div>
15. )
16. }

En este ejemplo se pueden apreciar mucho mejor los beneficios, pues hemos
eliminado la necesidad de una variable secundaria.

Los parámetros que requiere <For> son:

4. of: array al que queremos iterar.


5. each: variable en donde se guardará cada elemento iterado.
6. index: valor numérico referente al index del objeto dentro del array.

Te comparto la liga a la documentación de jsx-control-statements si quieres


adentrarte y conocer más: https://www.npmjs.com/package/jsx-control-
statements.

Transpilación

En el pasado ya hemos hablado un poco acerca del proceso de transpilación, sin


embargo, no hemos dicho quien se encarga de hacerlo y como lo hace. Es por
ello que quise hacer una pequeña sección para explicarlo.

Uno de los problemas cuando utilizamos JSX es que el navegador no es capaz


de interpretarlos, pues este solo sabe de HTML, CSS y JavaScript, por lo que
todo lo que le mandemos que no sea esto, no sabrá cómo interpretarlos, por lo
tanto, nuestra página se mostrará.

Para que el navegador sea capaz de entender nuestro código escrito en JSX es
necesario pasarlo por el proceso de transpilación, este proceso lo hace un
módulo llamado Babel, el cual toma los archivos en JSX y los convierte en
JavaScript nativo, para que, de esta forma, el navegador pueda interpretarlo.

Con Webpack es posible automatizar este proceso mediante un loader especial


para Babel, el cual ya lo hemos estado utilizando, pero no lo habíamos explicado.
A continuación, un pequeño fragmento del archivo webpack.config.js:

Página | 80
1. loaders: [{
2. test: /\.jsx?$/,
3. exclude: /node_modules/,
4. loader: 'babel-loader',
5. query:{
6. presets: ['env','react'],
7. plugins: ["jsx-control-statements"]
8. }
9. }]

Podemos observar que estamos utilizando un loader llamado babel, el cual


procesará todos los archivos con extensión *.js y *.jsx. Adicional, podemos ver
que le estamos agregando el plugin jsx-control-statements para ampliar el
lenguaje.

Esto ha sido una corta explicación acerca de lo que es Babel, pues era importante
entender que, es Babel y no Webpack, el que transpila los archivos. Pero es a
través de los loaders que Webpack que se puede automatizar el proceso de
transpilación por medio de Babel.

Programación con JavaScript puro.

Aunque JSX cubre casi todas las necesidades para crear componentes, existen
ocasiones, en las que es necesario crear elementos mediante JavaScript puro.
No es muy normal ver aplicación que utilicen esta forma de programar, pero
pueden existir ocasiones que lo ameriten.

Mediante JavaScript es posible crear elementos al vuelo mediante la función


React.createElement, la cual recibe 3 parámetros:

1. Type: le indica el tipo de elemento a crear, por ejemplo, input, div,


button, etc.
2. Props: se le envía un arreglo de propiedades. Hasta el momento no
hemos visto propiedades, pero ahora solo imaginemos que son una serie
de parámetros.
3. Childs: se le envía los elementos hijos del elemento, el cual pueden ser
otros elementos o una cadena de texto.

Veamos cómo quedaría la clase App editándola para usar JavaScript Nativo en
lugar de JSX:

1. import React from 'react'


2. import { render } from 'react-dom'
3.
4. class App extends React.Component{
5.
6. render(){

81 | Página
7. let helloWorld = React.createElement('h1',null,'Hello World')
8. let img = React.createElement('img',
9. {src:'https://facebook.github.io/react/img/logo.svg'}, null)
10. let div = React.createElement('div',null,[helloWorld,img])
11. return div
12. }
13. }
14. render(<App/>, document.getElementById('root'));

Primero que nada, veamos que estamos creando un <h1> (línea 7), el primer
parámetro es el tipo de elemento (h1), el segundo parámetro es null, pues no
tiene ninguna propiedad, y como tercer parámetro, le mandamos el texto ‘Hello
World’.

En segundo lugar, creamos un elemento <img>, al cual le mandamos la propiedad


src, que corresponde a la URL de la imagen y null como tercer parámetro, pues
no tiene un contenido.

En tercer lugar, creamos un elemento <div>, el cual contendrá a los dos


anteriores, para esto, le enviamos en un array el <h1> y el <img> como tercer
parámetro.

Finalmente, le retornamos el <div> para ser mostrado en pantalla.

Element Factorys

Como ya vimos, crear elementos con JavaScript es mucho más fácil de lo que
parece, pero por suerte, es posible crear los elementos de una forma mucho más
fácil mediante los Element Factory.

Los Element Factory, son utilidades que ya trae React para facilitarnos la creación
de elementos de HTML, y utilizarlos es tan fácil como hacer lo siguiente:

React.DOM.<element>

Donde <element> es el nombre de una etiqueta HTML válida. Veamos


nuevamente el ejemplo anterior utilizando Element Factory:

1. import React from 'react'


2. import { render } from 'react-dom'
3.
4. class App extends React.Component{
5.
6. render(){
7. return React.DOM.div(null,
8. React.DOM.h1(null,'Hello World'),
9. React.DOM.img(
10. {src: 'https://facebook.github.io/react/img/logo.svg'},null)
11.
12. )
13. }
14. }
15. render(<App/>, document.getElementById('root'));

Página | 82
El ejemplo es bastante parecido al anterior, solo que cambiamos la función
createElement por los Element Factory. Otra diferencia, es que ya no requiere el
tipo de elemento, pues ya viene implícito.

Element Factory Personalizados

Como ya lo platicamos, los Element Factory solo sirve para etiquetas HTML que
existen, por lo que cuando queremos utilizar un Element Factory para un
componente personalizado como sería App, no sería posible, es por este motivo
que existe los Element Factory Personalizados.

Veamos cómo quedaría nuestra aplicación, creando un Element Factory


personalizado para nuestro componente App.

1. import React from 'react'


2. import { render } from 'react-dom'
3.
4. class App extends React.Component{
5.
6. render(){
7. return React.DOM.div(null,
8. React.DOM.h1(null,'Hello World'),
9. React.DOM.img(
10. {src: 'https://facebook.github.io/react/img/logo.svg'},null)
11. )
12. }
13. }
14.
15. let appFactory = React.createFactory(App);
16. render(appFactory(null,null), document.getElementById('root'));

Esta vez el protagonista no es el método render, si no las dos últimas líneas, en


las cuales creo un Factory para el componente App (línea 15) mediante la función
React.createFactory. El Factory se almacena en la variable appFactory, que es
utilizados después (línea 16) para mostrar el elemento en pantalla.

83 | Página
Resumen

En este capítulo hemos aprendido los más esencial e importante del trabajo con
React, pues hemos aprendido a utilizar el lenguaje JSX que nos servirá durante
todo el libro.

También hemos aprendido como introducir contenido dinámico a nuestros


componentes y mostrarlos según una serie de condiciones. Y como dejar pasar
por alto el plugin jsx-control-statements que será indispensable para crear
aplicaciones más limpias y fáciles de mantener.

Hemos abordado el proceso de transpilación, por medio del cual es posible


convertir el lenguaje JSX a JavaScript nativo y que pueda ser interpretado por
cualquier navegador.

Hasta el momento hemos trabajado con el componente App, pero no hemos


entrado en detalles acerca de lo que es un componente, por lo que en el siguiente
capítulo entraremos de lleno al desarrollo de componentes, los cuales son una
parte esencial e innovadoras de React.

Página | 84
Introducción a los Componentes
Capítulo 4

Cuando desarrollamos aplicaciones Web o incluso de escritorio, es normal tratar


de separar nuestra aplicación en pequeños archivos que luego vamos incluyendo
dentro de otros más grande. Todo esto con múltiple finalidad, como separar las
responsabilidades de cada componente, reducir la complexidad y reutilizarlos al
máximo.

En React, es exactamente lo mismo, los componentes nos permiten crear


pequeños fragmentos de interface gráfica, que luego vamos uniendo como si
fuera un rompecabezas.

La relación entre Components y Web


Component

Para comprender que es un Component, es importante entender antes el


concepto de Web Components, para lo cual voy a citar su definición de Mozilla:

“Web Components consiste en distintas tecnologías


independientes. Puedes pensar en Web Components como en
widgets de interfaz de usuario reusables que son creados usando
tecnología Web abierta. Son parte del navegador, y por lo tanto
no necesitan bibliotecas externas como jQuery o Dojo. Un Web
Component puede ser usado sin escribir código, simplemente
añadiendo una sentencia para importarlo en una página HTML.
Web Components usa capacidades estándar, nuevas o aún en
desarrollo, del navegador.”

-- mozilla.org

Web Components es una tecnología


experimental
A pesar que el desarrollo web ya está apuntando al
desarrollo de aplicaciones con Web Components, la

85 | Página
realidad es que todavía está en una fase experimental
o en desarrollo.

Como vimos, los Web Componentes son pequeños widgets que podemos
simplemente ir añadiendo a nuestra página con tan solo importarlos y no requiere
de programación.

Fig. 42 - Desarrollo de aplicaciones con Web Components

En la imagen podemos ver la típica estructura de una página, la cual está


construida mediante una serie de componentes que son importados para crear
una página más compleja, la cual se convierte en un nuevo Componente más
complejo.

Puede que esta imagen no impresione mucho, pues todas las tecnologías Web
nos permiten crear archivos separados y luego simplemente incluirlos o
importarlos en nuestra página, pero existe una diferencia fundamental, los Web
componentes viven del lado del cliente y no del servidor. Además, en las
tecnologías tradicionales, el servidor no envía al navegador un Web Component,
por ejemplo, un tag <App>, si no que más bien hace la traducción del archivo
incluido a HTML, por lo que al final, lo que recibe el navegador es HTML puro.

Con los Web Componentes pasa algo diferente, pues el navegador si conoce de
Web Components y es posible enviarle un tag personalizado como <App>.

En React, si bien los componentes no son como tal Web Components, es posible
simular su comportamiento, ya que es posible crear etiquetas personalizadas que
simplemente utilizamos en conjunto con etiquetas HTML, sin embargo, React no
regresa al navegador las etiquetas custom, si no que las traduce a HTML para
que el navegador las pueda interpretar, con la gran diferencia que esto lo hace
del lado del cliente.

Página | 86
Fig. 43 - React Components Transpilation

Es posible que, en el futuro, cuando los navegadores soporte Web Components


en su totalidad, React pueda evolucionar para hora si, crear Web Components
reales.

Componentes con estado y sin estado

Una característica de los componentes, es su estado, el cual determina tanto la


información que se muestras hasta como se representa la información, de esta
forma, el estado puede cambiar la apariencia gráfica de un componente hasta la
forma en que se muestra la información. Un ejemplo básico de un estado, es,
por ejemplo, un formulario que puede pasar de modo lectura a escritura:

Fig. 44 - Cambio de estado en un componente

Como vemos en la imagen, una componente puede pasar de un formulario que


no permite la edición a otro donde los valores se muestran en <input> para que
el usuario pueda cambiar sus valores. Al guardar los cambios, el componente
puede regresar a su estado principal.

No vamos a entrar en los detalles de lo que es un estado, ni cómo es posible


modificarlo, pues más adelante tenemos una sección especialmente para ello,
por ahora, solo quiero que tengas una idea de lo que es el estado y cómo puede
afectar la forma en que se muestra un componente.

87 | Página
Componentes sin estado

Este tipo de componentes son los más simples, pues solo se utilizan para
representar la información que les llega como parámetros. En algunas ocasiones,
estos componentes pueden transformar la información con el único objetivo de
mostrarla en un formato más amigable, sin embargo, estos compontes no
consumen datos de fuentes externas ni modifican la información que se les envía.

Nuevo concepto: Componentes sin estado


Los componentes sin estado también son llamados
componentes de presentación, pero son mejor
conocidos como Stateless Component por su nombre
en inglés.

Para ejemplificar mejor este tipo de componentes vamos a crear un nuevo


archivo en nuestro proyecto llamado ItemList.js en la carpeta app, el cual
representará un ítem de una lista de productos:

1. import React from 'react'


2.
3. class ItemList extends React.Component{
4.
5. constructor(props){
6. super(props)
7. }
8.
9. render(){
10. return(
11. <li>{this.props.product.name} - {this.props.product.price}</li>
12. )
13. }
14. }
15. export default ItemList

Este componte utiliza algo que hasta ahora no habíamos utilizado, las Props, las
cuales son parámetros que son enviados durante la creación del componente. En
este caso, se le envía una propiedad llamada product, la cual debe de tener un
nombre (name) y un precio (price).

Nuevo concepto: Props


Las propiedades o simplemente props, son parámetros
que se le envían a un componente durante su creación.
Los props pueden ser datos para mostrar o información
para inicializar el estado. Como regla general, las
props son inmutables, lo que quieres decir, que son de
solo lectura.

Obtener las propiedades (props)


Para obtener las propiedades de un componente es
necesario obtenerlas mediante el prefijo this.props,
seguido del nombre de la propiedad.

Página | 88
Observemos que el componente ItemList solo muestra las propiedades que
recibe como parámetro, sin realizar ninguna actualización sobre ella.

Para completar este ejemplo, modificaremos el componente App para que quede
de la siguiente manera:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import ItemList from './ItemList'
4.
5. class App extends React.Component{
6.
7. render(){
8. let items = [{
9. name: 'Item 1',
10. price: 100
11. }, {
12. name: 'Item 2',
13. price: 200
14. }]
15.
16. return (
17. <ul>
18. <For each="item" index='index' of={ items }>
19. <ItemList product={item}/>
20. </For>
21. </ul>
22. )
23. }
24. }
25.
26. render(<App/>, document.getElementById('root'));

Veamos que hemos creado un array de ítems (línea 8), los cuales cuentan con
un nombre y un precio. Seguido, iteramos los ítems (línea 18) para crear un
componente <ItemList> por cada ítem de la lista, también le mandamos los datos
del producto mediante la propiedad product, la cual podrá ser accedida por el
componente <ItemList> utilizando la instrucción this.props.product.

Otra cosa importante a notar, es la línea 3, pues en ella se realiza la importación


del nuevo componente para poder ser utilizando.

89 | Página
Fig. 45 - Componentes sin estado.

Componentes con estado

Los componentes con estado se distinguen de los anteriores, debido a que estos
tienen un estado asociado al componente, el cual manipulan a mediad que el
usuario interactúa con la aplicación. Este tipo de componentes en ocasiones
consumen servicios externos para recuperar o modificar la información.

Un ejemplo típico de componentes con estados, son los formularios, pues es


necesario ligar cada campo a una propiedad del estado, el cual, al modificar los
campos afecta directamente al estado. Veamos cómo quedaría un componente
de este tipo.

Regresaremos al componente App y lo dejaremos de la siguiente manera:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import ItemItem from './ItemList'
4.
5. class App extends React.Component{
6.
7. constructor(){
8. super(...arguments)
9. this.state = {
10. firstName: '',
11. lastName: '',
12. age: ''
13. }
14. }
15.
16. handleChanges(e){
17. let newState = Object.assign(this.state, {[e.target.id]: e.target.value})
18. this.setState(newState)
19. }
20.
21. render(){
22.
23. return (
24. <form>
25. <label htmlFor='firstName'>Nombre</label>
26. <input id='firstName' type='text' value={this.state.firstName}
27. onChange={this.handleChanges.bind(this)}/>

Página | 90
28. <br/>
29. <label htmlFor='lastName'>Apellido</label>
30. <input id='lastName' type='text' value={this.state.lastName}
31. onChange={this.handleChanges.bind(this)}/>
32. <br/>
33. <label htmlFor='age'>Edad</label>
34. <input id='age' type='number' value={this.state.age}
35. onChange={this.handleChanges.bind(this)}/>
36. </form>
37. )
38. }
39. }
40.
41. render(<App/>, document.getElementById('root'));

No vamos a entrar en detalle acerca de los estados, ni cómo se actualizan, más


adelante lo veremos, por lo que solo te cuento rápido que está pasando.

Primero que nada, vemos que en la línea 9 se establece el estado inicial del
componente, el cual tiene un firstName (nombre), lastName (apellido) y ege
(edad). Los cuales están inicialmente en blanco.

Seguido en la línea 16, tenemos la función genérica handleChanges que modifica


el estado a medida que vamos capturando valores en los campos de texto.

Y finalmente en el método render, retornamos 3 campos, correspondientes a las


3 propiedades del estado, adicional, cada campo está ligado a una propiedad del
estado mediante la propiedad value. Cuando el usuario captura valores en los
campos, se dispara la función handleHanges para actualizar el estado.

Fig. 46 - Componente con estado

Seguramente al ver esta imagen, no quede muy claro que está pasando con el
estado, es por eso que utilizaremos el plugin React Developer Tools que
instalamos en el segundo capítulo para analizar mejor como es que el estado se
actualiza. Para esto, nos iremos al inspector, luego seleccionaremos el Tab React:

91 | Página
Fig. 47 - Inspeccionando un componente con estado

Una vez en el tab React, seleccionamos el tag <App> y del lado izquierdo
veremos las propiedades y el estado. Con el inspector abierto, actualiza los
campos de texto y verás cómo el estado también cambia.

Nuevo concepto: Componentes con estado


Se le conoce como componente con estado, a aquellos
componentes que tiene estado y que adicional,
manipulan el estado a medida que el usuario
interactúa con ellos. Estos componentes también son
conocidos como Componentes Contenedores, pero es
más común llamarlos Stateless Components, por su
nombre en inglés.

Jerarquía de componentes

Dada la naturaleza de React, es normal que en una aplicación tengamos muchos


componentes y muchos de estos importarán compontes dentro de ellos, que, a
su vez, contendrá más componentes. Ha esta característica de crear nuevos
componentes utilizando otros, se le conoce como Jerarquía de compones. Esta
jerarquía puede verse como un árbol, donde la raíz sería el componente que
abarque a todos los demás.

Página | 92
Fig. 48 - Jerarquía de componentes

En la imagen anterior que se puede ver claramente como una aplicación completa
es creada a partir de otros compontes.

En React tan solo se requiere importar el componente dentro del componente en


el que se quiere utilizar, una vez importado, el componente se puede utilizar
como un <tag> en JSX o se puede crear mediante los Element Factory
personalizados con JavaScript puro. Para importarlo utilizamos el formato import
<COMPONENT> from <FILE_PATH>, donde:

1. component: es el nombre del componente.


2. File_path: es el path al archivo que contiene el componente.

Para crear los paths, solo debemos seguir las reglas de siempre cuando
trabajamos con JavaScript.

Para poder importar un componente, es necesario exportarlo:

1. import React from 'react'


2.
3. class ItemList extends React.Component{
4.
5. constructor(props){
6. super(props)
7. }
8.
9. render(){
10. return(
11. <li>{this.props.product.name} - {this.props.product.price}</li>
12. )
13. }
14. }
15. export default ItemList

Observemos la última línea, en ella estamos exportando el componte para poder


utilizar desde otros componentes.

93 | Página
Referenciar correctamente el componente
Un error común cuando empezamos a programar en
React, es querer llamar al componente por el nombre
de la clase, sin embargo, el nombre del componente
será el que le pongamos en la instrucción export
default. Por lo que se aconseja que siempre lo
exportemos con el mismo nombre de la clase.

Una vez exportado, lo podemos utilizar en otro componte, como veremos a


continuación:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import ItemList from './ItemList'
4.
5. class App extends React.Component{
6.
7. render(){
8. return (
9. <ItemList product={item}/>
10. )
11. }
12. }
13.
14. render(<App/>, document.getElementById('root'));

El componente anterior, realiza una importación al componente ItemList (línea


3) para después crear una instancia del componente (línea 9).

Propiedades (Props)

Las propiedades son la forma que tiene React para pasar parámetros de un
componente padre a los hijos. Es normal que un componente pase datos a
los componentes hijos, sin embargo, no es lo único que se puede pasar, si no
que existe ocasiones en las que los padres mandar funciones a los hijos, para
que estos ejecuten operaciones de los padres, puede sonar extraño, pero ya
veremos cómo funciona.

Los props se envían a los componentes al momento de crearlos, y es tan fácil


como mandar las propiedades como un atributo del <tag> del componente, por
ejemplo, si retomamos el ejemplo del componente <ItemList>, recordarás que
le mandamos una propiedad product, la cual contenía un nombre y un precio:

1. <ItemList product={item}/>

Mandar objetos es una forma simple y limpia de mandar propiedades, pero


también lo podríamos hacer en varias propiedades, por ejemplo:

1. <ItemList productName={product.name} productPrice={product.price}/>

Página | 94
La única diferencia entre estos dos métodos será la forma de recuperar las
propiedades. Ya habíamos hablado que para recuperar una propiedad es
necesario usar el prefijo, this.props, por lo que en el primer ejemplo, el ítem se
recupera como this.props.product, y en el segundo ejemplo, sería
this.props.productName para el nombre y this.props.productPrice para el
precio.

Una limitación que tenemos para mandar props, es no utilizar un nombre de


propiedad que coincida con atributo de React, por ejemplo: onClick, onChange,
className, etc.

Las propiedades son inmutables


La propiedad tiene la característica de ser inmutables,
es decir, no debemos de modificarlas o actualizarlas,
pues es considerada una mala práctica, solo las
debemos utilizar de modo lectura.

Regresemos al ejemplo del formulario que vimos en la sección de componentes


con estado y sin estado. En este ejemplo, creamos el componente App y sobre
este montamos un formulario de registro de empleados. Ahora bien, ¿qué pasaría
si quisiéramos que la acción de guardar la controlara el componente padre? La
solución es simple, el componente padre tendría que enviar como props la función
de guardar, para que el hijo la ejecute. Veamos cómo quedaría esto:

Lo primero será pasar el formulario a un componente externo, por lo cual,


crearemos un nuevo componente llamado EmployeeForm sobre la carpeta app, el
cual se verá como a continuación:

1. import React from 'react'


2.
3. class EmployeeForm extends React.Component{
4.
5. constructor(){
6. super(...arguments)
7. this.state = {
8. firstName: '',
9. lastName: '',
10. age: ''
11. }
12. }
13.
14. handleChanges(e){
15. let newState = Object.assign(this.state,
16. {[e.target.id]: e.target.value})
17. this.setState(newState)
18. }
19.
20. saveEmployee(e){
21. this.props.save(this.state)
22. }
23.
24. render(){
25. return (
26. <form>
27. <label htmlFor='firstName'>Nombre</label>
28. <input id='firstName' type='text' value={this.state.firstName}

95 | Página
29. onChange={this.handleChanges.bind(this)}/>
30. <br/>
31. <label htmlFor='lastName'>Apellido</label>
32. <input id='lastName' type='text' value={this.state.lastName}
33. onChange={this.handleChanges.bind(this)}/>
34. <br/>
35. <label htmlFor='age'>Edad</label>
36. <input id='age' type='number' value={this.state.age}
37. onChange={this.handleChanges.bind(this)}/>
38. <br/>
39. <button onClick={this.saveEmployee.bind(this)}>Guardar</button>
40. </form>
41. )
42. }
43. }
44. export default EmployeeForm

Este componente es casi idéntico al primer formulario que creamos, pero hemos
agregado dos cosas, lo primero es que en la línea 39 agregamos un botón, el
cual, al ser presionado, ejecutar la función saveEmployee declarada en este mismo
componente, el segundo cambios, la función saveEmployee que declaramos en la
línea 20, el cual lo único que hace es ejecutar la función save enviada por el
parent como prop.

Por otra parte, tenemos al componente App que será el padre del componente
anterior:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import EmployeeForm from './EmployeeForm'
4.
5. class App extends React.Component{
6.
7. save(employee){
8. alert(JSON.stringify(employee))
9. }
10.
11. render(){
12. return (
13. <EmployeeForm save={ this.save.bind(this) }/>
14. )
15. }
16. }
17.
18. render(<App/>, document.getElementById('root'));

Podemos ver que al momento de crear el componente EmployeeForm, este le envía


como una prop la función save. De esta forma, cuando el componente
EmployeeForm, ejecute la propiedad, se lanzará una alerta con los datos
capturados (línea 8).

Página | 96
Fig. 49 - Funciones como props

Binding functions
Cuando es necesario bindear una función con una prop
o queremos utilizar una función en un evento como
onClick, onChange, etc. es necesario siempre empezar
con this, y finalizar con binding(this) como se ve a
continuación this.<function>.bind(this).

PropTypes

Debido a que los componentes no tienen el control sobre las props que se le
envía, y el tipo de datos, React proporciona un mecanismo que nos ayuda a
validar este tipo de aspectos. Mediante PropTypes es posible definir las
propiedades que debe de recibir un componente, el tipo de datos, estructura e
incluso si son requeridas o no. Definir los PropTypes es tan simple cómo:

1. <component>.propTypes = {
2. <propName>: <propType>
3. ...
4. }

Donde:

 Component: nombre de la clase


 <propName>: nombre de la propiedad a validar
 <propType>: regla de validación

Adicional, tenemos que importar la clase PropTypes de la siguiente manera:

import PropTypes from 'prop-types'.

Ahora bien, regresaremos al ejemplo de la lista de productos, para definir el


PropType para validar la estructura del producto:

97 | Página
1. import React from 'react'
2. import PropTypes from 'prop-types'
3.
4. class ItemList extends React.Component{
5.
6. constructor(props){
7. super(props)
8. }
9.
10. render(){
11. return(
12. <li>{this.props.product.name} - {this.props.product.price}</li>
13. )
14. }
15. }
16.
17. ItemList.propTypes = {
18. product: PropTypes.shape({
19. name: PropTypes.string.isRequired,
20. price: PropTypes.number.isRequired
21. }).isRequired
22. }
23.
24. export default ItemList

En este ejemplo, el componte ItemList espera una propiedad llamada product,


la cual está definida con una estructura (shape) que debe de tener name de tipo
String y es obligatoria (isRequired), también debe de tener un price de tipo
numérico (number) y también es obligatoria (isRequired).

isRequired es opcional
Si marcamos una propiedad como isRequired, React
validará que el campo haya sido enviado durante la
creación del componente, sin embargo, si no lo pones,
le indicamos que es un parámetro esperado, pero no
es obligatorio.

Por otra parte, el componente App debe de mandar la propiedad product con la
estructura exacta que se está solicitando, respetando los nombre y los tipos de
datos.

1. import React from 'react'


2. import { render } from 'react-dom'
3. import ItemList from './ItemList'
4.
5. class App extends React.Component{
6.
7. render(){
8. return (
9. <ItemList product={ {name: 100, price: 100}} />
10. )
11. }
12. }
13.
14. render(<App/>, document.getElementById('root'));

Observemos que he enviado un 100 en el campo name, el cual es inválido, ya que


se espera un string. Veamos qué pasa cuando ejecutamos la aplicación.

Página | 98
Fig. 50 - Probando los shape propsTypes

Observemos que si bien, la página se muestra correctamente, se lanza un error


en el log que indica que el campo product.name es inválido.

Validación con propTypes


Un error común es creer que con tener propTypes nos
aseguramos de que el usuario siempre mande la
información correcta, lo cual no es correcto, ya que
los propTypes nos sirve como primera línea de defensa
al indicarnos cuando hemos mandado un valor
incorrecto, sin embargo, no puede impedir que
manden valores en tipo o formato incorrecto.

Validaciones avanzadas

Ya hemos visto que es posible validar las propiedades de un componente, para


ayudar a que las personas que utilicen nuestros componentes, envíen las
propiedades correctas, es por ello que React ofrece un sistema muy completo de
validaciones que nos permitirán definir estructuras robustas que validen a la
perfección las propiedades.

Tipos de datos soportados:

La siguiente tabla muestra todos los tipos de datos que es posible validar con
PropTypes.

Tipo de datos Descripción

99 | Página
PropTypes.string Valida que la propiedad sea tipo String
Eje: {name: PropTypes.string}
PropTypes.number Valida que la propiedad sea numérica
Eje: {price: PropTypes.number}
PropTypes.bool Valida que la propiedad sea booleana
Eje: {checked: PropTypes.bool}
PropTypes.object Valida que la propiedad sea un objeto con cualquier
estructura
Eje: {product: PropTypes.object}
PropTypes.objectOf Valida que la propiedad sea un objeto con propiedades
de un determinado tipo
Eje: {tels: PropTypes.objectOf(PropType.string)}
PropTypes.shape Valida que la propiedad sea un objeto de una
estructura determinada
Eje:
{product: PropTypes.shape({
name: PropTypes.string,
price: PropTypes.number
})}
PropTypes.array Valida que la propiedad sea un arreglo
Eje: {tels: PropTypes.array}
PropTypes.arrayOf Valida que la propiedad sea un arreglo de un
terminado tipo de dato
Eje: {tels: PropTypes.arrayOf(PropType.string)}
PropTypes.oneOfType Valida que la propiedad sea de cualquier de los tipos
de datos especificado (es decir, puede ser de uno o de
otro tipo)
Eje:
{tel: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.arrayOf(PropTypes.string)
])}
PropTypes.func Valida que la propiedad sea una función
Eje: {save: PropTypes.func}
PropTypes.node Valida que la propiedad sea cualquier valor que pueda
ser renderizado en pantalla.
Eje: {node: PropType.node}
PropTypes.element Valida que la propiedad sea cualquier elemento de
React
Eje: {element: PropType.element}
PropTypes.instanceOf Valida que la propiedad sea la instancia de una clase
determinada
Eje: { product: PropType.instanceOf(Product) }
PropTypes.oneOf Valida que el valor de la propiedad este dentro de una
lista de valores permitidos (Igual que una
Enumeración)
Eje:
{status: PropType.oneOf([‘ACTIVO’,’INACTIVO’])}
PropTypes.any Le indica a React que la propiedad puede ser de
cualquier tipo
Eje: {object: PropType.any }

Página | 100
DefaultProps

Los DefaultProps son el mecanismo que tiene React para establecer un valor por
default a las propiedades que no fueron definidas en la creación del componente,
de esta forma, podemos establecer un valor y no dejar la propiedad en null.

Imaginemos que el componente <ItemList> que vimos anteriormente, no


recibiera el precio de un producto, se vería muy mal que al usuario le
mostraremos un null, por lo que entonces podríamos definir un valor de cero por
default. Vamos como quedaría:

1. import React from 'react'


2. import PropTypes from 'prop-types'
3.
4. class ItemList extends React.Component{
5.
6. constructor(props){
7. super(props)
8. }
9.
10. render(){
11. return(
12. <li>{this.props.productName} - {this.props.productPrice}</li>
13. )
14. }
15. }
16.
17. ItemList.propTypes = {
18. productName: PropTypes.string.isRequired,
19. productPrice: PropTypes.number
20. }
21.
22. ItemList.defaultProps = {
23. productPrice: 0
24. }
25.
26. export default ItemList

Definir valores por defecto mediante los defaults props, no tiene gran ciencia,
solo es establecer el nombre de la propiedad con el valor por default, solo
recordemos cumplir con la estructura definida propTypes.

Refs

Los Refs o referencias, son la forma que tiene React para hacer referencia a un
elemento de forma rápida, muy parecido a realizar una búsqueda por medio del
método getElementById de JavaScript. Mediante los Ref, es posible agregarle un
identificador único a los elementos para después accederlos mediante la
instrucción this.refs.<ref>, donde <ref> es el identificador único del elemento.

101 | Página
Regresemos al ejemplo del formulario de registro de empleados. Cuando la
pantalla carga, no hay ningún campo con el foco, por lo que queremos que
cuando inicie la página, el campo del nombre de empleado obtenga el foco.
Veamos cómo quedaría:

1. import React from 'react'


2.
3. class EmployeeForm extends React.Component{
4.
5. constructor(){
6. super(...arguments)
7. this.state = {
8. firstName: '',
9. lastName: '',
10. age: ''
11. }
12. }
13.
14. componentDidMount(){
15. this.refs.firstName.focus()
16. }
17.
18. handleChanges(e){
19. let newState = Object.assign(
20. this.state, {[e.target.id]: e.target.value})
21. this.setState(newState)
22. }
23.
24. saveEmployee(e){
25. this.props.save(this.state)
26. }
27.
28. render(){
29. return (
30. <form>
31. <label htmlFor='firstName'>Nombre</label>
32. <input ref='firstName' id='firstName'
33. type='text' value={this.state.firstName}
34. onChange={this.handleChanges.bind(this)}/>
35. <br/>
36. <label htmlFor='lastName'>Apellido</label>
37. <input id='lastName' type='text' value={this.state.lastName}
38. onChange={this.handleChanges.bind(this)}/>
39. <br/>
40. <label htmlFor='age'>Edad</label>
41. <input id='age' type='number' value={this.state.age}
42. onChange={this.handleChanges.bind(this)}/>
43. <br/>
44. <button onClick={this.saveEmployee.bind(this)}>Guardar</button>
45. </form>
46. )
47. }
48. }
49. export default EmployeeForm

Veamos que al campo firstName le hemos agregado el atributo ref (línea 32), el
cual establece el identificador para Rect, lo segundo importante es la línea 14,
pues declaramos el método componentDidMount, este método no tiene un nombre
al azar, si no que corresponde con uno de los métodos del ciclo de vida de React,
por lo que el método se ejecuta de forma automática cuando el componente es
mostrado en pantalla. Más adelante analizaremos el ciclo de vida de un
componente, pero por ahora, esta breve explicación deberá ser suficiente.

Página | 102
Cuando el método componentDidMount se ejecuta, obtiene la referencia al campo
productName mediante la instrucción this.refs.<ref> y le establece el foco.

Alcance los Refs

Las Refs tiene un alcance global en un proyecto y pueden ser accedidos desde
cualquier componente sin importar su posición en la jerarquía, por lo que es
común utilizarse para hacer referencia a elementos globales de una aplicación u
elementos que pueden ser afectados desde diversos puntos de la aplicación.

Utilizar referencias con prudencia


Las referencias son una excelente herramienta si se
utilizan con delicadeza. Ya que abusar en el uso de
referencias, puede hacer nuestra aplicación muy
complicada, pues desde donde sea podría afectar
nuestros componentes si tener una referencia clara de
quien los está afectando.

Keys

Los Keys pueden ser un tema avanzado para nosotros en este momento, pues
está relacionado con la optimización de React, sin embargo, quisiera explicarlos
en este momento, pues es un concepto muy simple y es fundamental utilizarlos.

Los keys son utilizados por React para identificar de forma más eficiente los
elementos que han cambiado, agregados o eliminados dentro de la aplicación,
los keys son utilizados únicamente cuando queremos mostrar los datos de un
arreglo, como una lista. En el ejemplo de la lista de productos, iterábamos un
array para mostrar todos los productos dentro de una lista <li>.

Cuando un elemento de React, muestra más de un mismo tipo de elemento, debe


poder identificar que elementos es cual, para poder actualizarlo correctamente.
El Key debe de ser un valor único en la colección y se utiliza por lo general el ID
o un campo que los ítems que sea único. Veamos cómo quedaría nuestra lista de
productos con Keys:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import ItemList from './ItemList'
4.
5. class App extends React.Component{
6.
7. render(){
8. let items = [{
9. name: 'Item 1',
10. price: 100
11. }, {
12. name: 'Item 2',

103 | Página
13. price: 200
14. }]
15.
16. return (
17. <ul>
18. <For each="item" index='index' of={ items }>
19. <ItemList product={item} key={ ítem.name } />
20. </For>
21. </ul>
22. )
23. }
24. }
25.
26. render(<App/>, document.getElementById('root'));

Como vez, es tan simple como añadir el atributo key y asignarle un valor único
en la colección. Si nosotros ejecutamos este ejemplo, veremos la lista de
siempre, por lo que no hay nada que mostrar, lo interesante se genera, cuando
quitamos el key:

Fig. 51 - Analizando la importancia de los keys

Como podemos ver en la imagen, nos está solicitando un key para cada elemento
de la lista

Qué hacer si no tenemos un campo único para el


Key
Existe ocasiones donde el array no tiene un campo que
identifique a los elementos, en esas ocasiones,
podemos utilizar el Index del for como Key o podemos
utilizar alguna librería que nos genere un UUID
dinámico como la siguiente:
https://www.npmjs.com/package/uuid.

Las 4 formas de crear un Componente

Página | 104
Debido a que React está creado bajo el lenguaje de programación JavaScript,
este también depende de su sintaxis para construir y declarar sus componentes
y es por ello que existen actualmente 4 formas de crear componentes, las cuales
obedecen a las distintas versiones del Estándar ECMAScript.

ECMAScript 5 – createClass

Este fue el primer método que existió para crear componentes en React y aun
que ya está bastante anticuado, la realidad es que puedes encontrar mucho
material en internet que todavía utiliza este método. Es muy probable que ya no
te toque trabajar con este método, sin embargo, no está de más mencionarlo y
si te lo llegaras a encontrar, sepas de que están hablando.

Para crear componentes en ECMAScript 5 es necesario utilizar el método


createClass de la clase React, Veamos un ejemplo muy rápido:

1. var miComponent = React.createClass({


2. propTypes: {...},
3. getDefaultProps: {...},
4. getInitialState: {...},
5. render: function(){...}
6. })

Lo primero que podemos ver, es que la clase se crea mediante la función


React.createClass, el cual recibe un objeto que debe de tener al menos la función
render, adicional, se puede declarar los propTypes, defaultProps y el estado
inicial del componente.

ECMAScript 6 - React.Component

Este es el método que hemos está utilizando hasta ahora, en la cual los
componentes se cran mediante clases que extienden de React.Component.
vemos un ejemplo rápido:

1. import React from 'react'


2.
3. class ECMAScript6Class extends React.Component{
4.
5. constructor(props){
6. super(props)
7. state = {...}
8. }
9.
10. render(){
11. return (
12. ...
13. )
14. }
15. }
16.
17. ECMAScript6Class.propTypes = {
18. ...
19. }

105 | Página
20.
21. ECMAScript6Class.defaultProps = {
22. ...
23. }

Cuando declaramos una clase, es necesario crear el constructor que reciba los
props, para después enviarlos a la superclase, de esta forma iniciamos
correctamente el componente. Los propsTypes y defaultsProps son declarados
fuera de la clase.

Recomendado para componentes con estado


Este es el componente más recomendable cuando
trabajamos con componentes con estado (Stateful).
Pues permite un mejor control sobre el componente.

ECMAScript 6 - Function Component

La siguiente forma que soporta ECMAScript 6, es la creación de componentes


mediante funciones, y es recomendado únicamente para componentes sin estado
(Stateless). Veamos cómo quedaría:

1. cont MiComponent = ({prop1, prop2, prop2}) => (


2. ...
3. return(
4. ...
5. )
6. )
7. MiComponent.propTypes = {
8. ...
9. }
10. MiComponent.defaultProps = {
11. ...
12. }

Observemos que el componente se reduce a una arrow function, la cual recibe


como parámetros las props. El cuerpo de la función es como el método render,
podremos crear variables, poner un poco de lógica y retornar un JSX.

Adicional, se puede definir los propTypes y defaultProps por fuera de la función.

ECMAScript 7 - React.Component

La última forma disponible, es utilizar las nuevas sintaxis de ECMAScript 7.


Aunque todo apunta a que esta sea la mejor forma de declarar componentes, no
es muy utilizada aun, debido a que ECMAScript 7 todavía se encuentra en
desarrollo. Este método es muy parecido a la creación de componentes mediante
React.Componentes ES6. Veamos cómo quedaría:

1. class MiComponent extends React.Component {

Página | 106
2.
3. static propTypes = {
4. ...
5. }
6.
7. static defaultProps = {
8. ...
9. }
10.
11. state = { ... }
12.
13. constructor(props){
14. super(props)
15. }
16.
17. render(){
18. ...
19. }
20. }

Este método es prácticamente igual que crear una clase en ES6, con la diferencia
que es posible declarar el estado como una propiedad, adicional, podemos
declarar los defaultProps y propTypes dentro de la clase y marcarlos como static
para poder ser accedidos desde fuera sin necesidad de crear una instancia.

Resumen

Este capítulo junto con el anterior, serán claves para el resto de tu aprendizaje
en React, pues hemos visto casi todas las características de React y serán la
base para construir componentes más complejos.

En los siguientes capítulos ya empezaremos de lleno con nuestro proyecto Mini


Twitter, una aplicación muy completa que nos permitirá utilizar gran parte de
todo lo que nos tiene por ofrecer React.

Tener claros todos los conceptos que hemos aprendido hasta ahora, será
claves, por lo que te recomiendo que, si todavía tienes dudas en algunas cosas,
sería buen momento para repasarlos.

107 | Página
Introducción al proyecto Mini
Twitter
Capítulo 5

Hasta este punto, ya hemos visto muchas de las características de React y ya


estamos listo para empezar a desarrollar el proyecto Mini Twitter, el cual es una
réplica de la famosa red social Twitter. Esta app no busca ser una copia de la
aplicación, sino un ejemplo educativo que nos lleve de la mano para construir
una aplicación totalmente funcional utilizando las tecnologías de React, NodeJS
y MongoDB.

Debido a que la aplicación Mini Twitter abarca el desarrollo del FrontEnd y el


BackEnd, organizaremos el libro de tal forma que, primero veremos toda la parte
del FrontEnd, en donde aprenderemos React y utilizaremos Bootstrap como
framework para hacer nuestra aplicación Responsive. Una vez terminada la
aplicación, tendremos un capítulo especial para estudiar Redux y cómo
implementarlo en nuestro proyecto. En segundo lugar, veremos el desarrollo de
un API REST, utilizando NodeJS + Express, el popular framework de NodeJS para
desarrollo web. Adicional, veremos todo lo referente a MongoDB, como crear los
modelos, conectarnos, realizar consultar y actualizar los datos.

Un vistazo rápido al proyecto

Antes de iniciar con el desarrollo del proyecto Mini Twitter, es importante


entender lo que vamos a estar desarrollando, y es por eso que, este capítulo está
especialmente dedicado a ello. Lo primero que haremos será dar un tour por la
aplicación terminada, luego regresaremos para analizar cómo está compuesta, y
finalmente iniciar con el desarrollo.

Página de inicio

Página | 108
La siguiente página corresponde a la página de inicio de Mini Twitter, en la cual
podemos ver el menú bar en la parte superior, los datos del usuario del lado
izquierdo, en el centro tenemos los tweets y del lado derecho, tenemos una lista
de usuarios sugeridos.

Fig. 52 - Página de inicio de Mini Twitter

Perfil de usuario

La siguiente imagen corresponde a la página de perfil de los usuarios, en la cual


puede visualizar un Banner, la foto de perfil, su nombre de usuario y una
descripción. En el centro podemos visualizar los tweets del usuario, seguidores o
los que lo siguen, del lado derecho, tenemos nuevamente usuario sugeridos.

Fig. 53 - Página de perfil de Mini Twitter

Editar perfil de usuario

109 | Página
La página de perfil puede cambiar de estado a editable, la cual permite cambiar
la foto de perfil, banner, nombre y la descripción:

Fig. 54 - Perfil de usuario en modo edición.

Observemos que el banner, cómo la foto, cambian, agregando un ícono de una


cámara, la cual, al poner el mouse enzima se subrayada de color naranja,
habitando cargar una foto nueva con tan solo hacer clic. También, donde aparece
el nombre de usuario y descripción, pasan a ser editables.

Página de seguidores

La siguiente foto corresponde a la sección de seguidores, la cual es igual a la de


las personas que lo siguen:

Fig. 55 - Sección de seguidores de Mini Twitter

Detalle del Tweet

Página | 110
También es posible ver el detalle de cada Tweet, para ver los comentarios que
tiene y agregar nuevos.

Fig. 56 - Detalle de un Tweet en Mini Twitter

Inicio de sesión (Login)

Otra de las páginas que cuenta la aplicación son las clásicas pantallas de iniciar
sección (login), la cual autenticarse ante la aplicación mediante usuario y
password:

Fig. 57 - Inicio de sesión de Mini Twitter

Registro de usuarios (Signup)

111 | Página
Mediante esta página, es posible crear una nueva cuenta para poder acceder a
la aplicación.

Fig. 58 - Página de registro de Mini Twitter

Hemos visto un recorrido rápido a lo que será la aplicación de Mini Twitter, pero
la aplicación es engañosa, porque tiene muchas más cosas de las podemos ver a
simple vista, las cuales tenemos que analizar mucho más a detalle. Es por ese
motivo, que una vez que dimos un tour rápido de la aplicación, es hora de verla
con rayos X y ver cómo es que la aplicación se compone y todos los Components
que vamos a requerir para termina la aplicación.

Análisis al prototipo del proyecto

En esta sección analizaremos con mucho detalle todos los componentes que
conforman la aplicación Mini Twitter.

Componente TwitterDashboard

TwitterDashboard es la página de inicio para un usuario autenticado, en la cual


es posible ver los últimos Tweets de los usuarios, también es posible ver un
pequeño resumen de tu perfil (lazo izquierdo) y una lista de usuario sugeridos
para seguir (lado derecho).

Página | 112
Fig. 59 - Topología de la página de inicio

En la imagen anterior, podemos ver con más detalle cómo está compuesta la
página de inicio. A simple vista, es posible ver 5 componentes, los cuales son:

 TwitterDashboard: Es un componente contenedor, pues alberga al


resto componentes que podemos ver en pantalla.
 Toolbar: Componente que muestra al usuario autenticado.
 Profile: Componente que muestra los datos del usuario autenticado,
como foto, número de Tweets, número de suscriptores y personas que lo
sigue.
 TweetsContainer: Es un componente contendedor, pues en realidad
solo muestra una serie de componentes Tweet, los cuales veremos con
más detalle más adelante.
 SuggestedUser: Muestra una lista de usuario sugeridos para seguir.

Adicional a los componentes que podemos ver en pantalla, existe uno más
llamado TwitterApp, el cual envuelve toda la aplicación e incluyendo las demás
componentes, como las páginas de login, signup, y el perfil del usuario.

Componente TweetsContainer

Hora daremos un zoom al componente TweetsContainer para ver cómo está


compuesto:

113 | Página
Fig. 60 - Topología del componente TweetsContainer

Como podemos apreciar en la imagen, el componente está conformado del


componente Reply, el cual sirve para crear un nuevo Tweet. Adicional, es posible
ver una lista de componentes Tweet, que corresponde a cada tweet de los
usuarios.

Componente UserPage

La página de perfil, permite a los usuarios ver su perfil y ver el perfil de los demás
usuarios. Este componente se muestra de dos formas posibles, ya que si estás
en tu propio los datos siempre y cuando estés en tu propio perfil. Por otra parte,
si estas en el perfil de otro usuario, te dará la opción de seguirlo.

Página | 114
Fig. 61 - Topología de la página UserPage

En esta página es posible ver varios componentes que se reúnen para formar la
página:

 UserPage: Es un componente contenedor, pues alberga al resto de


componentes, como son SuggestedUsers, Followers, Followings,
TweetsContainer.
 TweetsContainer: Este componente ya lo analizamos y aquí solo lo
reutilizamos.
 SuggestedUsers: Nuevamente, este componente ya lo analizamos y
solamente lo reutilizado.
 Followings: Este componente se muestra solo cuando presionamos el
tab “siguiendo”, el cual muestra un listado de todas las personas que
seguimos.
 Followers: Igual que el anterior, solo que esta muestra nuestros
seguidores.

Componente Signup

Este es el formulario para crear un nuevo usuario, el cual solo solicita datos
mínimos para crear un nuevo perfil.

115 | Página
Fig. 62 - Topología de la página Signup

La página de login está compuesta únicamente por el componente Login y


Toolbar.

Componente Login

La página de Login es bastante parecida a la de Signup, es solo un formulario


donde el usuario captura su usuario y password para autenticarse.

Fig. 63 - Topología de la página Login

Página | 116
Hasta este momento, hemos visto los componentes principales de la aplicación,
lo que falta son algunas componentes de popup y compontes secundarios en los
cuales no me gustaría nombrar aquí, pues no quisiera entrar en mucho detalle
para no perdernos. Por ahora, con que tengamos una idea básica de cómo está
formada la aplicación será más que suficiente y a medida que entremos en los
detalles, explicaremos los componentes restantes.

Jerarquía de los componentes del proyecto

Tal vez recuerdes que en el capítulo pasado hablamos acerca de la jerarquía de


componentes, pues en este capítulo mostraremos la jerarquía completa del
proyecto. La idea es que puedas imprimir esta imagen o guardarla en un lugar
accesible, ya que nos servirá muchísimo para entender cómo vamos a ir armando
el proyecto, así de cómo vamos a reutilizar los componentes.

Fig. 64 - Jerarquía de componentes de Mini Twitter

La imagen anterior, nos da una fotografía general de toda la aplicación Mini


Twitter, en la cual podemos ver cómo está compuesto cada componente, así
como también, podemos aprecias donde reutilizamos los componentes.

El enfoque Top-down & Bottom-up

117 | Página
Uno de los aspectos más importantes cuando vamos a desarrollar una nueva
aplicación, es determina el orden en que vamos a construir los componentes,
pues la estrategia que tomemos, repercutirá en la forma que vamos a trabajar.
Es por este motivo que vamos a presentar el en enfoque Top-down y Bottom-up
para analizar sus diferencias y las ventajas que traen cada una.

Top-down

Este enfoque consiste en empezar a construir los componentes de más arriba en


la jerarquía y continuar desarrollando los componentes hacia abajo. Este este es
el enfoque más simple y que es utilizado con más frecuencia por desarrolladores
inexpertos o proyectos donde no hubo una fase de análisis que ayudara a
identificar los componentes necesarios y su jerarquía.

Fig. 65 - El enfoque Top-down

En este enfoque se requiere poco o nula planeación, pues se empieza a construir


de lo menos especifico o lo más específico, de esta manera, vamos construyendo
los componentes a como los vamos requiriendo en el componente padre. El
inconveniente de este enfoque, es que es muy propenso a la refactorización, pues
a medida que vamos descendiendo en la jerarquía, vamos descubriendo datos
que requeríamos arrastrar desde la parte superior, también descubrimos que
algunos componentes que ya desarrollamos pudieron ser reutilizados, por lo que
se requiere refactorizar para reutilizarlos o en el peor de los casos, hacemos un
componente casi idéntico para no modificar el trabajo que ya tenemos echo. Otra
de las desventajas, es que casi cualquier dependencia a otros componentes que
requiera algún componente, no existirá y tendremos que irlos creando al vuelo.

Página | 118
Bottom-up

Este otro enfoque es todo lo contrario que Top-down, pues propone empezar con
los componentes más abajo en la jerarquía, de esta forma, iniciamos con los
componentes que no tienen dependencias y vamos subiendo en la jerarquía hasta
llegar al primer componente en la jerarquía.

Fig. 66 - El enfoque Bottom-up

El enfoque Bottom-up requiere de una planeación mucho mejor, en la cual salgan


a relucir el mayor número de componentes requeridos para el desarrollo, también
se identifica la información que requiere cada componente y se va contemplando
a medida que subimos en la jerarquía.

Este enfoque es utilizado por los desarrolladores más experimentados, que son
capases de analizar con buen detalle la aplicación a desarrollar. Un mal análisis
puede hacer que replanteemos gran parte de la estructura y con ellos, se genera
un gran impacto en el desarrollo.

Bien ejecutado, reduce drásticamente la necesidad de realizar refactor, también


ayuda a identificar todos los datos y servicios que será necesarios para la
aplicación en general.

El enfoque utilizado y porque

Aunque lo mejor sería utilizar el enfoque Botton-up, la realidad es que apenas


estamos aprendiendo a utilizar React, por lo que aventurarnos a utilizar este
enfoque puede ser un poco riesgoso y nos puede complicar más el aprendizaje.
Es por este motivo que en este libro utilizaremos el enfoque Top-down he iremos
construyendo los componentes a como sea requeridos.

119 | Página
Preparando el entorno del proyecto

Dado que la aplicación Mini Twitter cuenta con una serie de servicios para
funcionar, deberemos instalar y ejecutar nuestra API Rest antes de empezar a
desarrollar, ya que toda la información que consultemos o actualicemos, será por
medio del API REST.

Por el momento no entraremos en detalles acerca del API REST, pues más
adelante hablaremos de cómo desarrollar desde cero, todo el API, publicarlos y
prepararlo para producción. Por ahora, solo instalaremos el API y lo utilizaremos.

Instalar API REST

Para instalar el API, deberemos bajar la última versión del repo, es decir el branch
“Capitulo-16-Produccion” directamente desde el repo del libro:

https://github.com/oscarjb1/books-reactiveprogramming.git

El siguiente paso será cambiar la configuración de conexión a la base de datos.


Para esto, será necesario regresar al Mongo Atlas para recuperar el String de
conexión. Una vez que ya estemos allí, deberemos presionar en “CONNECT” y
en la pantalla emergente presionaremos “Connect Your Application” y en la
nueva pantalla presionamos el botón “COPY” como ya lo habíamos visto.

Página | 120
Fig. 67 - Obtener el String de conexión.

Este String de conexión lo deberemos de agregar en las secciones


connectionString del archivo config.js, por ahora dejaremos el mismo el mismo
valor para development y production. No olvides remplazar la sección
<PASSWORD> del String de conexión por el password real.

1. module.exports = {
2. server: {
3. port: 3000
4. },
5. tweets: {
6. maxTweetSize: 140
7. },
8. mongodb: {
9. development: {
10. connectionString: "<Copy connection String from Mongo Atlas>"
11. },
12. production: {
13. connectionString: "<Copy connection String from Mongo Atlas>"
14. }
15. },
16. jwt: {
17. secret: "#$%EGt2eT##$EG%Y$Y&U&/IETRH45W$%whth$Y$%YGRT"
18. }
19. }

Aprovecharemos para cambiar el puerto, de tal forma que cambiaremos del 80


al 3000, como lo podemos ver en la línea 3.

Lo siguiente será configurar el sistema operativo para encontrar nuestra API


como un subdominio del localhost, para ello, realizaremos la siguiente
configuración (siempre como administrador):

121 | Página
En Windows:

No dirigimos a la carpeta C:\Windows\System32\drivers\etc y abrimos el archivo


hosts como administrador, y agregamos la siguiente línea:

1. 127.0.0.1 api.localhost

En Linux

En el caso de Linux, el procedimiento es exactamente el mismo, solamente que


el archivo que tendremos que editar es /etc/hosts/. De tal forma que
agregaremos solamente la siguiente línea:

1. 127.0.0.1 api.localhost

En Mac

En Mac, tendremos que hacer exactamente lo mismo que en los anteriores, sin
embargo, el archivo se encuentra en /private/etc/hosts, allí agregaremos la
línea:

1. 127.0.0.1 api.localhost

NOTA: Esto hará que cualquier llamada al subdominio api.* se redirija al


localhost.

Iniciar el API

Seguido, abrimos la terminal y nos dirigimos a la raíz del API (donde


descargamos el repo), una vez allí ejecutamos el siguiente comando:

npm install
npm start
Con esto, si todo sale bien, habremos iniciado correctamente el API REST.

Página | 122
Probando nuestra API

Una vez iniciado el servidor, entramos al navegador y navegamos a la URL:


http://api.localhost:3000/ la cual nos mostrará la siguiente pantalla:

Fig. 68 - Probando el API REST

Si en el navegador podemos la pantalla anterior, quiere decir que hemos


instalado el API Correctamente. También podrá revisar la documentación de
todos los servicios que utilizaremos al presionar el botón “Ver documentación”.

Fig. 69 - Documentación del API.

En cada servicio podemos ver el nombre, la descripción y el método en


el que acepta peticiones, el path para ejecutarlo y una leyenda que
indica si el servicio tiene seguridad.

123 | Página
Creando un usuario de prueba

Antes de poder empezar a utilizar nuestra API, será necesario crear un usuario,
por lo cual, utilizaremos un archivo de utilidad llamado InitMongoDB.js, que se
encargará de esto. El archivo se ve así:

1. var mongoose = require('mongoose')


2. var configuration = require('./config')
3. var Profile = require('./api/models/Profile')
4. var bcrypt = require('bcrypt')
5.
6. var opts = {
7. keepAlive: 1,
8. useNewUrlParser: true
9. }
10. mongoose.connect(configuration.mongodb.development.connectionString, opts)
11.
12. const newProfile = new Profile({
13. name: "Usuario de prueba",
14. userName: "test",
15. password: bcrypt.hashSync('1234', 10)
16. })
17.
18. Profile.findOne({username: 'test'}, function(err, queryUser){
19. if(queryUser !== null){
20. console.log("====> El usuario de prueba ya ha sido registrado")
21. process.exit()
22. }
23. })
24.
25. newProfile.save(function(err){
26. if(err){
27. console.log("====> Error al crear el usuario de prueba",err)
28. process.exit()
29. return
30. }
31. console.log("====> Usuario de prueba creado correctamente")
32. process.exit()
33. })

No vamos entrar en los detalles, pues este archivo tiene cosas avanzadas que no
hemos visto aún. Sin embargo, lo muestro por si desean analizarlo.

Por ahora, tendremos que dirigirnos a la terminal y dirigirnos a la carpeta del API
(donde descargamos el código) y ejecutar el comando

node InitMongoDB.js

El resultado será el siguiente:

Página | 124
Fig. 70 - Creación del usuario de prueba.

Si sale algún error, deberemos revisar los datos de conexión a la base de datos.

El usuario creado por la ejecución pasada será:

Username: test
Password: 1234

Para comprobar que todo salió bien, podemos regresar a Compass y ver la
colección profiles, en ella deberíamos ver el usuario:

Fig. 71 - Usuario Test creado en MongoDB.

Del lado izquierdo seleccionamos test  profiles y veremos podremos


ver los usuarios. Si no puedes ver la sección “ profiles” tendrás que
actualizar la vista.

125 | Página
Invocando el API REST desde React

En este punto ya tenemos el API funcionando y listo para ser utilizando, pero
debemos aprender cómo es que un servicio REST es consumido desde una
aplicación React.

React proporciona la función fetch la cual se utiliza para consumir cualquier


recurso den la WEB mediante HTTP, por lo que es posible consumir servicios REST
mediante el método POST, GET, PUT, DELETE y PATCH. Esta función recibe dos
parámetros para funcionar, el primero corresponde a la URL en la que se
encuentre el servicio y el segundo parámetro corresponde a los parámetros de
invocación, como el Header y el Body de la petición.

Veamos un ejemplo simple de cómo consumir los Tweets de un usuario. Para


esto deberemos crear una nueva clase en llamada APIInvoker.js en el directorio
/app/utils/. El archivo se ve la siguiente manera:

1. class APIInvoker {
2.
3. invoke(url, okCallback, failCallback, params){
4. fetch(`http://api.localhost:3000${url}`, params)
5. .then((response) => {
6. return response.json()
7. })
8. .then((responseData) => {
9. if(responseData.ok){
10. okCallback(responseData)
11. }else{
12. failCallback(responseData)
13. }
14. })
15. }
16. }
17. export default new APIInvoker();

Lo primero a tomar en cuenta es la función invoke, la cual servirá para consumir


servicios del API de una forma reutilizable. Esta función recibe 4 parámetros
obligatorios:

1. url: String que representa el recurso que se quiere consumir, sin contener el
host y el puerto, ejemplo “/tweets”.
2. okCallback: deberá ser una función, la cual se llamará solo en caso de que
el servicio responda correctamente. La función deberá admitir un parámetro
que representa la respuesta del servicio.
3. failCallback: funciona igual al anterior, solo que este se ejecuta cuando el
servicio responde con algún error.
4. params: Representa los parámetros de invocación HTTP, como son los
header y body.

De estos cuatro parámetros solo dos son requeridos por la función fetch en la
línea 4, la url y params. Cuando el servicio responde, la respuesta es procesada
mediante una promesa (Promise), es decir, una serie de then, los cuales procesan
la respuesta por partes. El primer then (línea 5) convierte la respuesta en un

Página | 126
objeto json (línea 6) y lo retorna para ser procesado por el siguiente then (línea
8), el cual, valida la propiedad ok de la respuesta, si el valor de esta propiedad
es true, indica que el servicio termino correctamente y llamada la función
okCallback, por otra parte, si el valor es false, indica que algo salió mal y se
ejecuta la función failCallback.

Finalmente exportamos una nueva instancia de la clase APIInvoker en la línea


17, con la finalidad de poder utilizar el objeto desde los componentes de React.

El siguiente paso será probar nuestra clase con un pequeño ejemplo que consulte
los Tweets de nuestro usuario de pruebas, para esto modificaremos la clase App
para dejarla de la siguiente manera:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import APIInvoker from "./utils/APIInvoker"
4.
5. class App extends React.Component{
6.
7. constructor(props){
8. super(props)
9. this.state = {
10. tweets: []
11. }
12. }
13.
14. componentWillMount(){
15. let params = {
16. method: 'put',
17. headers: {
18. 'Content-Type': 'application/json',
19. },
20. body: null
21. };
22.
23. APIInvoker.invoke('/tweets/test', response => {
24. this.setState({
25. tweets: response.body
26. })
27. },error => {
28. console.log("Error al cargar los Tweets", error);
29. })
30. }
31.
32. render(){
33. console.log(this.state.tweets);
34. return (
35. <ul>
36. <For each="tweet" of={this.state.tweets}>
37. <li key={tweet._id}>{tweet._creator.name}: {tweet.message}</li>
38. </For>
39. </ul>
40. )
41. }
42. }
43.
44. render(<App/>, document.getElementById('root'));

127 | Página
No entraremos en los detalles del todo el componente, pues nos es el objetivo
de esta sección, pero lo que solo nos centraremos en la función
componentWillMount (línea 14). Esta función no tiene un nombre aleatorio, si no
que corresponde a una de las funciones del ciclo de vida de los componentes de
React, el cual se ejecuta automáticamente justo antes del que el componente
sea montado, es decir, que sea visible en el navegador. Lo primero que hacemos
será preparar los parámetros de la invocación (línea 15). Dentro de la variable
definimos un objeto con las propiedades:

 method: representa el método HTTP que vamos a utilizar para la


llamada al API, recordemos que los métodos utilizados son: GET, POST,
PUT, DELETE, PATCH.
 headers: dentro de header podemos enviar cualquier propiedad de
cabecera que necesitemos, pueden ser propios de HTTP o custom. En
este caso, indicamos el header Content-Type para indicarle que
esperamos un JSON como respuesta
 body: se utiliza para enviar información al servidor, como podría ser un
JSON. En este caso no es necesario, pues el método GET no soporta un
body.

El siguiente paso será invocar el método invoke de la clase APIInvoker que


acabamos de crear, para esto, le estamos enviando la URL del servicio para
consultar los Tweets del usuario test (/tweets/test), el segundo parámetros será
la función que se ejecutará en caso de éxito, la cual solamente actualiza el estado
del componente con la respuesta del servicio, el tercer parámetro es la función
de error, la cual solo imprimirá en la consola los errores retornados.

Finalmente, los Tweets retornados son mostrados en el método render con ayuda
de un <For> para iterar los resultados.

Mejorando la clase APIInvoker

Hasta este punto ya sabemos cómo invocar el API REST desde React, sin
embargo, necesitamos mejorar aún más nuestra clase APIInvoker para
reutilizarla en todo el proyecto.

Lo primero que aremos será quitar todas las secciones Hardcode, como lo son el
host y el puerto y enviarlas a un archivo de configuración externo llamado
config.js, el cual deberemos crear justo en la raíz del proyecto, es decir a la
misma altura que los archivos package.json y webpack.config.js:

Página | 128
Fig. 72 - Archivo config.js

El archivo deberá quedar de la siguiente manera:

1. module.exports = {
2. debugMode: true,
3. server: {
4. port: 3000,
5. host: "http://api.localhost"
6. }
7. }

Una vez que tenemos el archivo de configuración, deberemos de modificar el


archivo APIInvoker para que utilice estas configuraciones:

1. var configuration = require('../../config')


2. const debug = configuration.debugMode
3.
4. class APIInvoker {
5.
6. getAPIHeader(){
7. return {
8. 'Content-Type': 'application/json',
9. authorization: window.localStorage.getItem("token"),
10. }
11. }
12.
13. invokeGET(url, okCallback, failCallback){
14. let params = {
15. method: 'get',
16. headers: this.getAPIHeader()
17. }
18. this.invoke(url, okCallback, failCallback,params);
19. }
20.
21. invokePUT(url, body, okCallback, failCallback){
22. let params = {
23. method: 'put',
24. headers: this.getAPIHeader(),
25. body: JSON.stringify(body)
26. };

129 | Página
27.
28. this.invoke(url, okCallback, failCallback,params);
29. }
30.
31. invokePOST(url, body, okCallback, failCallback){
32. let params = {
33. method: 'post',
34. headers: this.getAPIHeader(),
35. body: JSON.stringify(body)
36. };
37.
38. this.invoke(url, okCallback, failCallback,params);
39. }
40.
41. invoke(url, okCallback, failCallback,params){
42. if(debug){
43. console.log("Invoke => " + params.method + ":" + url );
44. console.log(params.body);
45. }
46.
47. fetch(`${configuration.server.host}:${configuration.server.port}${url}`,
48. params)
49. .then((response) => {
50. if(debug){
51. console.log("Invoke Response => " );
52. console.log(response);
53. }
54. return response.json()
55. })
56. .then((responseData) => {
57. if(responseData.ok){
58. okCallback(responseData)
59. }else{
60. failCallback(responseData)
61. }
62.
63. })
64. }
65. }
66. export default new APIInvoker();

Como vemos, la clase creció bastante a como la teníamos originalmente, lo que


podría resultar intimidador, pero en realidad es más simple de lo que parece.
Analicemos los cambios. Se han agregado una serie de métodos adicionales al
método invoke, pero observemos que todos se llaman invoke + un método de
HTTP, los cuales son:

 invokeGET: Permite invocar un servicio con el método GET


 InvokePUT: Permite invocar un servicio con el método PUT
 InvokePOST: Permite invocar un servicio con el método POST

Si nos vamos al detalle de cada uno de estos métodos, veremos que en realidad
contienen lo mismo, ya que lo único que hacen es crear los parámetros HTTP,
como son los headers y el body, para finalmente llamar al método invoke (sin
postfijo). La única diferencia que tienen estas funciones es que el método
invokeGET no requiere un body.

Página | 130
Otro punto interesante a notar es que cuando definimos los header, lo hacemos
mediante la función getAPIHeader, la cual retorna el Content-Type y una
propiedad llamada authorization. No entraremos en detalle acerca de esta, pues
lo analizaremos más adelante cuando veamos la parte de seguridad.

Finalmente, hemos realizado algunos cambios en la función invoke, lo primero


que podemos apreciar, son dos bloques de debug (líneas 42 y 50) las cuales nos
permite imprimir en el log la petición y la respuesta de cada invocación. Lo
segundo interesante es que en la línea 47 hemos remplazado la URL del API REST
por los valores configurados en el archivo config.js.

En este punto tenemos lista la clase APIInvoker para ser utilizada a lo largo de
toda la implementación del proyecto Mini Twitter, Y ya solo nos restaría ajustar
la clase App.js para reflejar estos últimos cambios, por lo que vamos a modificar
únicamente la función componentWillMount para que se vea de la siguiente
manera:

1. componentWillMount(){
2. APIInvoker.invokeGET('/tweets/test', response => {
3. this.setState({
4. tweets: response.body
5. })
6. },error => {
7. console.log("Error al cargar los Tweets", error);
8. })
9. }

Apreciemos que ya no es necesario definir los parámetros de HTTP, logrando que


sea mucho más simple invocar un servicio.

El componente TweetsContainer

Finalmente ha llegado el momento de iniciar con la construcción del proyecto Mini


Twitter, por lo que iniciaremos con la clase TweetsContainer la cual se encarga
de cargar todos los Tweets. Veamos nuevamente la imagen de la estructura del
proyecto para entender dónde va este componente y saber qué es lo que estamos
programando.

131 | Página
Fig. 73 - Ubicando el componente TweetsContainer

En la siguiente imagen podemos observar una fotografía más detallada del


componente TweetsContainer, en el cual se puedan apreciar los componentes
secundarios que conforman a este.

Fig. 741 - Repaso a la estructura del proyecto.

Este componente tiene dos responsabilidades, la primer, es cargar los Tweets


desde el API y la segunda, es funcionar como un contenedor para ver todos los
Tweets que retorne el API, así como al componente Reply, el cual analizaremos
más adelante.

Página | 132
Lo primero que haremos será crear el archivo TweetsContainer.js en el path
/app, es decir, a la misma altura que el archivo App.js y lo dejaremos de la
siguiente manera:

1. import React from 'react'


2. import APIInvoker from "./utils/APIInvoker"
3. import PropTypes from 'prop-types'
4.
5. class TweetsContainer extends React.Component{
6. constructor(props){
7. super(props)
8. this.state = {
9. tweets: []
10. }
11. }
12.
13. componentWillMount(){
14. let username = this.props.profile.userName
15. let onlyUserTweet = this.props.onlyUserTweet
16. this.loadTweets(username, onlyUserTweet)
17. }
18.
19. loadTweets(username, onlyUserTweet){
20. let url = '/tweets' + (onlyUserTweet ? "/" + username : "")
21. APIInvoker.invokeGET(url , response => {
22. this.setState({
23. tweets: response.body
24. })
25. },error => {
26. console.log("Error al cargar los Tweets", error);
27. })
28. }
29.
30. render(){
31.
32. return (
33. <main className="twitter-panel">
34. <If condition={this.state.tweets != null}>
35. <For each="tweet" of={this.state.tweets}>
36. <p key={tweet._id}>{tweet._creator.userName}
37. : {tweet._id}-{tweet.message}</p>
38. </For>
39. </If>
40. </main>
41. )
42. }
43. }
44.
45. TweetsContainer.propTypes = {
46. onlyUserTweet: PropTypes.bool,
47. profile: PropTypes.object
48. }
49.
50. TweetsContainer.defaultProps = {
51. onlyUserTweet: false,
52. profile: {
53. userName: ""
54. }
55. }
56.
57. export default TweetsContainer;

133 | Página
Lo primero interesante a resaltar es la función componentWillMount, la cual
recupera dos propiedades, username y onlyUserTweet, las cuales necesitará pasar
a la función loadTweets para cargar los Tweet iniciales. Cabe mencionar que este
componente puede mostrar Tweet de dos formas, es decir, puede mostrar los
Tweet de todos los usuarios de forma cronológica o solo los Tweet del usuario
autenticado, y ese es el motivo por el cual son necesarios estas dos propiedades.
Si la propiedad onlyUserTweet es true, le indicamos al componente que muestre
solo los Tweet del usuario autenticado, de lo contrario, mostrara los Tweet de
todos los usuarios. Más adelante veremos la importancia de hacerlo así para
reutilizar el componente.

Antes de ser montado el componente, el método componentWillMount es


ejecutado, lo que dispara la carga de Tweets mediante el método loadTweets,
que a su vez, utilizará la clase APIInvoker para cargar los Tweets desde el API
REST, pero antes, la URL deberá ser generada (línea 20), Si requerimos los
Tweets de todos los usuarios, entonces invocamos el API con la URL “/tweets”,
pero si requerimos los Tweets de un usuario en específico, entonces invocamos
la URL “/tweets/{username}”. Prestemos atención en la parte {username}, pues
esta parte de la URL en realidad es un parámetro, y podría recibir el nombre de
usuario de cualquier usuario registrado en la aplicación.

Usuarios registrados
De momento solo tendremos el usuario test, por lo
que de momento solo podremos consultar los Tweets
de este usuario. Más adelante implementaremos el
registro de usuarios para poder hacer pruebas con más
usuarios.

Documentación del servicio de Tweets


Los servicios de consulta de Tweets se utilizan para
recuperar todos los Tweets de forma cronológica
(/tweets) y todos los de un determinado usuario
(/tweets/:username).

En las líneas 35 a 38 podemos ver como se Iteran todos los Tweets consultados
para mostrar el nombre de usuario del Tweet, el ID y el texto del Tweet.

Al final del archivo también podemos apreciar que hemos definidos los PropTypes
y los DefaultProps, correspondientes a las propiedades onlyUserTweet y profile.
El primero ya lo hemos mencionado, pero el segundo corresponde al usuario
autenticado en la aplicación. De momento no nos preocupemos por esta
propiedad, más adelante regresaremos a analizarla.

Página | 134
Una vez terminado de editar el archivo TweetsContainer, regresaremos al
componente App.js y actualizaremos la función render para que se vea de la
siguiente manera:

1. render(){
2. return (
3. <TweetsContainer />
4. )
5. }

También será necesario agregar el import del componente TweetsContainer al


inicio de la clase.

1. import TweetsContainer from './TweetsContainer'

Ya con estos últimos pasos actualizamos el navegador y veremos los tweets

Fig. 75 - Resultado del componente TweetsContainer.

El componente Tweet

Hasta este momento solo representamos algunos campos del Tweet para poder
comprobar que el componente TweetsContainer está consultando realmente los
datos desde el API REST, por lo que ahora nos concentraremos en el componente
Tweet, el cual utilizaremos pare representar los Tweets en pantalla.

Lo primero que haremos será crear un nuevo archivo llamado Tweet.js en el path
/app y lo dejaremos de la siguiente manera:

1. import React from 'react'


2. import APIInvoker from './utils/APIInvoker'
3. import { render } from 'react-dom';
4. import PropTypes from 'prop-types'
5.
6. class Tweet extends React.Component{
7.
8. constructor(props){
9. super(props)
10. this.state = props.tweet
11. }
12.
13. render(){

135 | Página
14. let tweetClass = null
15. if(this.props.detail){
16. tweetClass = 'tweet detail'
17. }else{
18. tweetClass = this.state.isNew ? 'tweet fadeIn animated' : 'tweet'
19. }
20.
21. return (
22. <article className={tweetClass} id={"tweet-" + this.state._id}>
23. <img src={this.state._creator.avatar} className="tweet-avatar" />
24. <div className="tweet-body">
25. <div className="tweet-user">
26. <a href="#">
27. <span className="tweet-name" data-ignore-onclick>
28. {this.state._creator.name}</span>
29. </a>
30. <span className="tweet-username">
31. @{this.state._creator.userName}</span>
32. </div>
33. <p className="tweet-message">{this.state.message}</p>
34. <If condition={this.state.image != null}>
35. <img className="tweet-img" src={this.state.image}/>
36. </If>
37. <div className="tweet-footer">
38. <a className={this.state.liked ? 'like-icon liked' :
39. 'like-icon'} data-ignore-onclick>
40. <i className="fa fa-heart " aria-hidden="true"
41. data-ignore-onclick></i> {this.state.likeCounter}
42. </a>
43. <If condition={!this.props.detail} >
44. <a className="reply-icon" data-ignore-onclick>
45. <i className="fa fa-reply " aria-hidden="true"
46. data-ignore-onclick></i> {this.state.replys}
47. </a>
48. </If>
49. </div>
50. </div>
51. <div id={"tweet-detail-" + this.state._id}/>
52. </article>
53. )
54. }
55. }
56.
57. Tweet.propTypes = {
58. tweet: PropTypes.object.isRequired,
59. detail: PropTypes.bool
60. }
61.
62. Tweet.defaultProps = {
63. detail: false
64. }
65.
66. export default Tweet;

Este no es un libro HTML ni de CSS, por lo que no nos detendremos en explicar


para que es cada clase de estilo utilizada ni la estructura del HTML generado,
salvo en ocasiones donde tiene una importancia relacionada con el tema en
cuestión.

Lo primero que debemos de resaltar es que este componente recibe dos


propiedades, la primera representa el objeto Tweet como tal y un boolean, que

Página | 136
indica si el Tweet debe mostrar el detalle, es decir, los comentarios relacionados
al Tweet.

En la línea 10 podemos ver como la propiedad this.tweet es establecida como el


Estado del componente (más adelante revisaremos los estados).

En la función render podremos ver que este ya es un componente más complejo,


pues retorna un HTML con varios elementos. Por lo pronto te pido que ignores
las líneas 14 a 19, pues las explicaremos más adelante y nos centremos en la
parte del return. Quiero que veas que cada Tweet es englobado como un
<article> para darle más semántica al HTML generado. Cada article tendrá un
ID generado a partir del ID del Tweet (línea 22), de esta forma podremos
identificar el Tweet más adelante.

En la línea 23 definimos una imagen, la cual corresponde al Avatar del usuario


que publico el Tweet. Fijémonos como tomamos la imagen del estado, mediante
this.state._creator.avatar. De esta misma forma definimos nombre (línea 28),
nombre de usuario (línea 31), el mensaje de tweet (línea 33) y la imagen
asociada al Tweet, siempre y cuando tenga imagen (línea 35).

Las líneas 38 a 48 son las que muestran los iconos de like y compartir, por el
momento no tendrá funcionalidad, pero más adelante regresaremos para
implementarla.

En la línea 51 tenemos un div con solo un ID, este lo utilizaremos más adelante
para mostrar el detalle del Tweet, por lo pronto no lo prestemos atención.

En este punto el componente Tweet ya debería estar funcionando correctamente,


sin embargo, hace falta mandarlo llamar desde el componente TweetsContainer,
para esto regresaremos al archivo TweetsContainer.js y editaremos solamente
el método render para dejarlo de la siguiente manera:

1. render(){
2. return (
3. <main className="twitter-panel">
4. <If condition={this.state.tweets != null}>
5. <For each="tweet" of={this.state.tweets}>
6. <Tweet key={tweet._id} tweet={tweet}/>
7. </For>
8. </If>
9. </main>
10. )
11. }

Adicional tendremos que agregar el import al componente al inicio del archivo:

1. import Tweet from './Tweet'

Como último paso tendremos que agregar las clases de estilo CSS al archivo
styles.css que se encuentra en el path /public/resources/css/styles.css. Solo
agreguemos lo siguiente al final del archivo:

137 | Página
1. /** TWEET COMPONENT **/
2.
3. .tweet{
4. padding: 10px;
5. border-top: 1px solid #e6ecf0;
6. }
7.
8.
9. .tweet .tweet-link{
10. position: absolute;
11. display: block;
12. left: 0px;
13. right: 0px;
14. top: 0px;
15. bottom: 0px;
16. }
17.
18. .tweet:hover{
19. background-color: #F5F8FA;
20. cursor: pointer;
21. }
22.
23. .tweet.detail{
24. border-top: none;
25. }
26.
27. .tweet.detail:hover{
28. background-color: #FFF;
29. cursor: default;
30. }
31.
32.
33. .tweet .tweet-img{
34. max-width: 100%;
35. border-radius: 5px;
36. }
37.
38. .tweet .tweet-avatar{
39. border: 1px solid #333;
40. display: inline-block;
41. width: 45px;
42. height: 45px;
43. position: absolute;
44. border-radius: 5px;
45. text-align: center;
46. }
47.
48. .tweet .tweet-body{
49. margin-left: 55px;
50. }
51.
52. .tweet .tweet-body .tweet-name{
53. color: #333;
54. }
55.
56. .tweet .tweet-body .tweet-name{
57. font-weight: bold;
58. text-transform: capitalize;
59. margin-right: 10px;
60. z-index: 10000;
61. }
62.
63. .tweet .tweet-body .tweet-name:hover{
64. text-decoration: underline;
65. }
66.

Página | 138
67. .tweet .tweet-body .tweet-username{
68. text-transform: lowercase;
69. color: #999;
70. }
71.
72. .tweet.detail .tweet-body .tweet-user{
73. margin-left: 70px;
74. }
75.
76. .tweet.detail .tweet-body{
77. margin-left: 0px;
78. }
79.
80. .tweet.detail .tweet-body .tweet-name{
81. font-size: 18px;
82. }
83.
84. .tweet-detail-responses .tweet.detail .tweet-body .tweet-message{
85. font-size: 16px;
86. }
87.
88. .tweet.detail .tweet-body .tweet-username{
89. display: block;
90. font-size: 16px;
91. }
92.
93. .tweet.detail .tweet-message{
94. position: relative;
95. display: block;
96. margin-top: 25px;
97. font-size: 26px;
98. left: 0px;
99. }
100.
101. .reply-icon,
102. .like-icon{
103. color: #999;
104. transition: 0.5s;
105. padding-right: 40px;
106. font-weight: bold;
107. font-size: 18px;
108. z-index: 99999;
109. }
110.
111. .like-icon:hover{
112. color: #E2264D;
113. }
114.
115. .like-icon.liked{
116. color: #E2264D;
117. }
118.
119. .reply-icon:hover{
120. color: #1DA1F2;
121. }
122.
123. .like-icon i{
124. color: inherit;
125. }
126.
127. .reply-icon i{
128. color: inherit;
129. }

139 | Página
Guardamos todos los cambios y refrescar el navegador para apreciar cómo va
tomando forma el proyecto.

Fig. 76 - Componente Tweet

Finalmente, nuestro proyecto se debe ver de la siguiente manera:

Fig. 77 - Estructura actual del proyecto

Una cosa más antes de concluir este capítulo. Hasta el momento hemos trabajado
con la estructura de los Tweets retornados por el API REST, pero no los hemos
analizado, es por ello que dejo a continuación un ejemplo del JSON.

1. {

Página | 140
2. "ok":true,
3. "body":[
4. {
5. "_id":"598f8f4cd7a3b239e4e57f3b",
6. "_creator":{
7. "_id":"598f8c4ad7a3b239e4e57f38",
8. "name":"Usuario de prueba",
9. "userName":"test",
10. "avatar":""
11. },
12. "date":"2017-08-12T23:29:16.078Z",
13. "message":"Hola mundo desde mi tercer Tweet",
14. "liked":false,
15. "likeCounter":0,
16. "replys":0,
17. "image":null
18. }
19. ]
20. }

Lo primero que vamos a observar de aquí en adelante es que todos los servicios
retornados por el API tienen la misma estructura base, es decir, regresan un
campo llamado ok y un body, el ok nos indica de forma booleana si el resultado
es correcto y el body encapsula todo el mensaje que nos retorna el API. Si
regresamos al componente TweetContainer en la línea 22 veremos lo siguiente:

1. this.setState({
2. tweets: response.body
3. })

Observemos que el estado lo crea a partir del body y no de todo el retorno del
API.

Ya que conocemos la estructura general del mensaje, podemos analizar los


campos del Tweet, los cuales son:

 Id: Identificador único en la base de datos.


 _creator: Objeto que representa el usuario que creo el Tweet
o _id: Identificador único del usuario en la base de datos.
o Name: Nombre del usuario
o userName: Nombre de usuario
o avatar: Representación en Base 64 de la imagen del
Avatar.
 Date: fecha de creación del Tweet.
 Message: El mensaje que escribió el usuario en el Tweet.
 Liked: Indica si el usuario autenticado le dio like al Tweet.
 likeCounter: número de likes totales recibidos en este Tweet.
 Reply: número de respuestas o comentarios recibidos en el
Tweet.
 Image: Imagen asociada al Tweet (opcional)

Resumen

141 | Página
Este capítulo ha sido bastante emocionante pues hemos iniciado con el proyecto
Mini Twitter y hemos aplicados varios de los conceptos que hemos venido
aprendiendo a lo largo del libro. Si bien, solo hemos empezado, ya pudimos
apreciar un pequeño avance en el proyecto.

Por otra parte, hemos visto como instalar el API REST y hemos aprendido como
consumirlo desde React, también hemos analizado la estructura de un Tweet
retornado por el API.

Página | 142
Introducción al Shadow DOM y los
Estados
Capítulo 6

Una de las características más importantes de React, es que permite que los
componentes tengan estado, el cual es un objeto JavaScript de contiene la
información asociada al componente y que por lo general, representa la
información que ve el usuario en pantalla, pero también el estado puede
determinar la forma en que una aplicación se muestra al usuario. Podemos ver
el estado como el Modelo en la arquitectura MVC.

Introducción a los Estados

Para ilustrar mejor que es el estado, podemos imaginar un formulario de registro


o actualización de empleados. El cual estaría en modo edición cuando se trate de
un nuevo empleado o pongamos al empleado existente en modo edición, pero si
solo queremos consultar su información, entonces estaría en modo solo lectura:

Fig. 78 - Cambio de estado en un Componente.

En este ejemplo podemos apreciar la importancia del estado, pues el estado


contendrá los datos del empleado y una bandera booleana que indique si el
formulario está en modo edición. Si el componente está en modo lectura,

143 | Página
entonces solo mostrara los datos del empleado en etiquetas <label>, pero si
pasamos a edición, entonces los <label> se remplazan por etiquetas <input>, y
una vez que guardamos los datos, entonces podemos pasar el formulario
nuevamente a modo solo lectura.

Como podemos apreciar, los estados permiten representar la información en


pantalla, pero al mismo tiempo, puede repercutir en la forma en que el
componente muestra la información y se comporta. En este ejemplo, utilizamos
el estado para guardar los datos del empleado, como son nombre, edad, teléfono
y fecha de nacimiento, pero al mismo tiempo, utilizamos el estado para
determina como es que el componente de se debe de mostrar al usuario.

1. {
2. editMode: false,
3. employee: {
4. name: "Juan Pérez",
5. age: 22,
6. tel: "12334567890",
7. birthdate: "10/10/1900"
8. }
9. }

Este fragmento de código, representa el estado de la pantalla anterior, y como


podemos observar, es un objeto JavaScript común y corriente, el cual tiene la
información del empleado (líneas 3 a 8), pero por otro lado, tiene una propiedad
que sirven exclusivamente para determinar la forma de mostrar la información,
como es el caso de la propiedad editMode (línea 2).

Los estados pueden tener la estructura que sea y también pueden ser tan grandes
y complejos como nuestra interface lo requieres, de tal forma que podemos tener
muchos más datos y muchos más campos de control de interface gráfica.

Establecer el estado de Componente

Existe dos formas de establecer el Estado de un componente; definiéndolo en el


constructor del componente mediante la asignación directa a la propiedad
this.state o establecerlo dinámicamente en cualquier función o evento,
mediante la función setState().

Si lo que buscamos es inicializar el estado antes de realizar cualquier otra acción,


podemos hacerlos directamente sobre el constructor, por ejemplo:

1. constructor(props){
2. super(props)
3. this.state = {
4. tweets: []
5. }
6. }

Página | 144
En este ejemplo, definimos el estado del componente TweetsContainer mediante
una lista de tweets vacío, esto con la finalidad de que cuando el componente se
muestre por primera vez, no marce un error debido a que el estado es Null.

this.state solo funciona en el constructor


Es muy importante resaltar que establecer el Estado
directamente mediante la instrucción this.state solo
se puede hacer en el constructor, ya que de lo
contrario, provocaría que React no sea capaz de
detectar los cambios y es considerada una mala
práctica.

El segundó método, es establecer el estado mediante la función setState(), la


cual se utiliza para establecer un nuevo estado al componente. Al hacer esto,
estamos sobrescribiendo el estado anterior, lo que hará que nuestro componente
se actualice con los valores del nuevo estado.

Es normal que durante la vida de un componente establezcamos el estado varias


veces a medida que interactuamos con la interface, o simplemente cuando
cargamos datos del backend.

Si recordamos, en el componente TweetsContainer definimos la función


componentWillMount, la cual cargaba los Tweets del API y luego actualizaba el
Estado del componente mediante this.setState(). Recordemos como quedo
esta parte:

1. loadTweets(username, onlyUserTweet){
2. let url = '/tweets' + (onlyUserTweet ? "/" + username : "")
3. APIInvoker.invokeGET(url, response => {
4. this.setState({
5. tweets: response.body
6. })
7. },error => {
8. console.log("Error al cargar los Tweets", error);
9. })
10. }

El estado es actualizado cuando obtenemos la respuesta del API (línea 4). Cuando
actualizamos el estado mediante la función this.setState(), React
automáticamente actualiza el componente, para que de esta forma, la vista sea
actualizada para reflejando el nuevo Estado.

Actualizando el estado de un Componente

Una de las principales diferencias que existe entre las propiedades (Props) y el
Estado, es que el estado está diseñado para ser mutable, es decir, podemos

145 | Página
realizar cambios en el, de tal forma que los componentes puedan ser interactivos
y responder a las acciones del usuario.

Cuando React detecta la actualización del estado en uno de sus componentes,


este inicia una actualización en cascada de todos los componentes hijos, para
asegurarse de que todos los componentes sean actualizados con el nuevo estado.

Fig. 79 - Waterfall update

Para actualizar el Estado de un componente, se utiliza la función setState(), por


lo que en realidad no existe una diferencia entre actualizar el Estado y establecer
su valor inicial, sin embargo, la diferencia radica en la forma en la forma en que
modificamos el Objeto asociado al estado.

React está pensado para que el objeto que representa el estado sea inmutable,
por lo que cuando creamos un nuevo estado, tendremos que hacer una copia del
estado actual y sobre esa copia agregar o actualizar los nuevos valores. Esto es
así para garantiza que React pueda detectar los nuevos cambios y actualizar la
vista.

Una de las formas que tenemos para actualizar el estado, es mediante el método
Object.assign, el cual se utiliza para copiar los valores de todas las propiedades
enumerables de uno o más objetos fuente a un objeto destino. Retorna un nuevo
objeto con los cambios. Veamos un ejemplo:

1. let newState = {
2. edit: true
3. }
4. let stateUpdate = Object.assign({},this.state,newState)
5. console.log(stateUpdate)
6. this.setState(stateUpdate)

En este ejemplo, vamos a asumir que el estado actual del componente es un


objeto que solo tiene la propiedad edit = false y queremos cambiar el valor edit
= true, para hacer esto, nos apoyamos de Object.assign (línea 4) que en este
caso recibe 3 parámetros, el primero será el objeto al cual deberá aplicar los
cambios, por lo que el valor {} indica que es un objeto nuevo. El segundo
parámetro es el estado actual del componente (this.state). Como tercer
parámetro enviamos el objeto newState, el cual contiene los nuevos valores para
el estado. Como resultado de la asignación se realizará una “merge” (mescla)

Página | 146
entre el estado actual y el nuevo estado, por lo que los valores que ya están en
el estado actual solo se actualizarán y los que no estén, se agregarán. Como
resultado de “merge”, Finalmente, actualizamos el estado mediante
this.setState().

Tras ejecutar este código podremos ver en el log del navegador como quedaría
la variable stateUpdate:

Fig. 80 - Object.assign result

Actualizar el estado con Object.assign puede resultar bastante simple, y lo es,


pero tiene un problema fundamental, y es que no es capaz de realizar copias a
profundidad, esto quiere decir que, si nuestro estado tiene otros objetos dentro,
este no realizará una clonación de estos objetos, en su lugar, los pasará como
referencia.

Un ejemplo claro de este problema es cuando nuestro objeto tiene una array,
veamos otro ejemplo:

1. let newState = {
2. edit: true,
3. tweet: [
4. "tweet 1",
5. "tweet 2"
6. ]
7. }
8. let stateUpdate = Object.assign({},this.state,newState)
9. newState.tweet.push("tweet 3")
10. console.log(stateUpdate, newState);

Veamos que hemos agregar al estado una lista de tweets (línea 1 a 7), luego
aplicada la asignación (línea 8). Finalmente agregamos un nuevo Tweet al objeto
newState. (línea 9). Uno esperaría que el objeto newState tenga el tweet 3,
mientras que el stateUpdate se quedaría con los dos primero. Sin embargo, la
realidad es otra; veamos el resultado de log (línea 10) tras ejecutar este código:

Fig. 81 - Object.assign con objetos anidados

Si esto lo hiciéramos de esta forma, tuviéramos un problema con React, pues


podríamos modificar el Array desde el otro objeto y React no podría detectar los

147 | Página
cambios. Para solucionar este problema, existe una librería más sofisticada para
actualizar los estados de una forma más segura, la cual veremos a continuación.

La librería react-addons-update

La librería react-addons-update es una librería de ayuda para actualizar los


objetos de una forma segura, pues esta se encarga de realizar la clonación de
los objetos a profundidad. De esta forma, si nuestro objeto contuviera otro
objetos o arreglos anidados, este también los clonará.

Antes de empezar a trabajar con la librería, debemos instalarla con npm, para
ello, ejecutamos la siguiente instrucción:
npm install --save react-addons-update

por otra parte, será necesario realizar el import correspondiente en cada


componente donde requerimos utilizarlo.

1. import update from 'react-addons-update'

Esta librería proporciona solamente el método update, al cual solo requiere de


dos parámetros, el estado actual y los cambios que vamos a realizar sobre el
estado, la función update nos va a regresar un objeto totalmente nuevo, por lo
que deberemos tener cuidad de asignar el resultado en una nueva variable.

1. let stateUpdate = update(this.state,{


2. edit: {$set: true}
3. })
4. this.setState(stateUpdate)

Este ejemplo es parecido a los anteriores, pues modificamos el valor del atributo
edit a true, sin embargo, notemos una pequeña diferencia en la sintaxis, pues
en lugar de poner el valore del atributo directamente, tenemos que hacerlo
mediante un par de llaves, el cual tiene dos partes, la operación y el valor. La
operación le indica que hacer con el valor, en este caso, $set indica que se
establezca el valor true al atributo edit, en caso de que el valor exista lo
actualiza, y si no existe, lo agregará.

La librería react-addons-update ofrece varias operaciones que podremos utilizar


según el objetivo que buscamos. Las operaciones disponibles son:

Operación Descripción
$push Agrega todos los elementos de una lista al final de la lista objetivo.
Funciona igual que la operación push() de ES
Formato: {$push: array}
Ejemplo:

1. update( [1, 2] ,{$push: [3]}) // => 1, 2, 3

Página | 148
$unshift Agrega todos los elementos de una lista al principio de la lista
objetivo. Funciona igual que la operación unshift de ES
Formato: {$unshift: array}
Ejemplo:

1. update( [1, 2] ,{$unshift: [3]}) // => 3, 1, 2

$splice Agrega o elimina elementos de una lista.


Formato: {$splice: array of arrays}
Por cada ítem del arrays llama la función splice() en objeto destino
Ejemplo:

1. const collection = [1, 2, {a: [12, 17, 15]}];


2. update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}});
3. // => [1, 2, {a: [12, 13, 14, 15]}]

$set Remplaza por completo el valor del objeto destino


Formato: {$set: any}
Ejemplo:

1. const state = {edit: false}


2. update( state, {edit: {$set: true}} // => {edit: true}

$marge Realiza una combinación de dos objetos, remplaza llaves


existentes y agrega las que no están en el objeto destino.
Formato: {$merge: object}
Ejemplo:

1. const obj = {a: 5, b: 3};


2. update(obj, {$merge: {b: 6, c: 7}}); // => {a: 5, b: 6, c: 7}

$apply Permite actualiza el valor actual por medio de una función, esta
función permite realizar cálculos más complejos.
Formato: {$apply: function}
Ejemplo:

1. const state = {a: 10};


2. update(obj, {a: {$apply: (value) => value*2 }});
3. // => {a: 20}

Cabe mencionar que las secciones que no son manipuladas por alguna operación,
pasan intacta al objeto resultante. Por lo que solo es necesario realizar
operaciones sobre los campos que queremos modificar.

De momento no te preocupes si no comprender del todo como se utilizan algunas


de estas operaciones, pues a lo largo del libro utilizaremos algunas y las
explicaremos con más detalle. Por otra parte, puedes revisar la documentación
oficial de la librería https://www.npmjs.com/package/react-addons-update.

149 | Página
El Shadow DOM de React

Dado que React se ejecuta del lado del navegador, este está obligado a
arreglárseles solo para la actualización de las vistas. De esta forma, React es el
que tiene que decirle al navegador que elementos del DOM deberán ser
actualizados para reflejar los cambios.

La forma que tiene React para identificar si un componente ha sido actualizado,


es muy compleja, aun que podemos resumirla en que React detecta los cambios
en los estados en los componentes, y de allí determina que compontes deben de
ser actualizado. Sin embargo, actualizar el DOM directamente cada vez que hay
un cambio en la vista, es un proceso pesado, por lo que React utiliza algo llamado
Shadow DOM, el cual es una representación del DOM real, pero administrado
por React.

Fig. 82 - DOM y Shadow DOM

El Shadow DOM es utilizado por React pare evaluar las actualizaciones que deberá
aplicar al DOM real, y una vez evaluados los cambios, se ejecuta un proceso
llamado reconciliación, encargado de sincronizar los cambios del Shadow DOM
hacia el DOM real. Este proceso hace que React mejore su rendimiento, al no
tener que actualizar la vista ante cada cambio, en su lugar, calcula todos los
cambios y los aplica en Batch al DOM real.

Nuevo concepto: Reconciliación (Reconciliación)


Reconciliación es el proceso que ejecuta React para
sincronizar los cambios que existen en el Shadow DOM
contra el DOM real del navegador.

Atributos de los elementos:

Como ya mencionamos en la sección de JSX, React requiere que todos los


atributos de los elementos estén en formato Camel Case, por lo que para React
no será lo mismo decir maxlength que maxLength. En caso de no poner los

Página | 150
atributos en Camel Case, es posible que React no reconozca el atributo y/o que
nos lance un error en la consola.

React tiene una lista de todos los atributos que soporte, la cual nos puede servir
de guía:
accept, acceptCharset, accessKey, action, allowFullScreen, allowTransparency, alt,
async, autoComplete, autofocus, autoPlay, capture, cellPadding, cellSpacing, challenge,
charSet, checked classID, className, colSpan, cols, content, contentEditable,
contextMenu, controls, coords, crossOrigin, data, dateTime, default, defer, dir,
disabled, download, draggable, encType, form, formAction, formEncType, formMethod,
formNoValidate, formTarget, frameBorder, headers, height, hidden, high, href, hrefLang,
htmlFor, httpEquiv, icon, id, inputMode, integrity, is, keyParams, keyType, kind, label,
lang, list, loop, low, manifest, marginHeight, marginWidth, max, maxLength, media,
mediaGroup, method, min, minLength, multiple, muted, name, noValidate, nonce, open,
optimum, pattern, placeholder, poster, preload, radioGroup, readOnly, rel, required,
reversed, role, rowSpan, rows, sandbox, scope, scoped, scrolling, seamless, selected,
shape, size, sizes, span, spellCheck, src, srcDoc, srcLang, srcSet, start, step, style,
summary, tabIndex, target, title, type, useMap, value, width, wmode, wrap.

Más detalles en la documentación oficial:


https://www.reactenlightenment.com/react-jsx/5.7.html

Muchos de los atributos que ves en esta lista se ven exactamente igual al HTML
tradicional, y esto se debe a que son atributos formados por una palabra, por lo
que no hay necesidad de utilizar Camel Case. Ahora bien, existe ciertos atributos
con los que debemos de tener cuidado, pues son difieren del nombre original de
HTML, como es el caso de className para class, defaultChecked para checked,
htmlFor para for, etc.

Puedes ver más información al respecto en la documentación oficial:


https://facebook.github.io/react/docs/dom-elements.html

Eventos

Los eventos en React tiene el mismo tratamiento que los atributos, pues deben
de definirse en Camel Case, de lo contrario no serán tomados en cuenta por
React, los principales eventos son:

Keyboard Events
onKeyDown onKeyPress onKeyUp

Focus Events
DOMEventTarget relatedTarget

Mouse Events

151 | Página
onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit onDragLeave
onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave onMouseMove
onMouseOut onMouseOver onMouseUp

Selection Events
onSelect

Puedes ver la lista completa de eventos en la documentación oficial de React:


https://facebook.github.io/react/docs/events.html

Página | 152
Resumen

A lo largo de este capítulo hemos analizado los Estados, y como es que estos
afectan la forma que se ven y actualizan los componentes. Por otra parte, hemos
analizado las dos formas de establecer el estado, ya sea en el constructor o
mediante la función setState(). También analizamos la librería react-addons-
update para actualizar de forma correcta el estado.

Hemos analizado la forma en que React actualiza los componentes mediante el


Shadow DOM y como es que este se utiliza para optimizar la actualización del
DOM del navegador.

Finalmente hemos visto la forma correcta de declarar los atributos y eventos


mediante Camel Case.

153 | Página
Trabajando con Formularios
Capítulo 7

Los formularios son una parte fundamental de cualquier aplicación, pues son la
puerta de entrada para que los usuarios puedan interactuar con la aplicación, y
en React no es la excepción. Sin embargo, existe una diferencia sustancia al
trabajar con formularios en React y en una tecnología web convencional, ya que
React almacena la información en sus Estados, y no en una sesión del lado del
servidor, como sería el caso de una aplicación web tradicional.

En React existe dos formas de administrar los controles de un formulario,


controlados y no controlados, el primero permite ligar el valor de un campo al
estado, mientras que el segundo permite tener un campo de libre captura, el cual
no está ligado al estado o una propiedad.

Nuevo concepto: Controles


El termino Controles se utiliza para llamar de forma
genérica a los elementos que conforman un formulario
y que el usuario puede interactuar con ellos, por
ejemplo: input, radio button, checkbox, select,
textarea, etc.

Controlled Components

Los componentes controlados son todos aquellos que su valor está ligado
directamente al estado del componente o una propiedad (prop), lo que
significa que el control siempre mostrará el valor del objeto asociado.

Para que un control sea considerado controlador (Controlled), es necesario


vincular su valor con el estado o una propiedad mediante el atributo value, como
podemos ver en el siguiente ejemplo.

1. <input type="text" value={this.state.field} />

Debido a que el valor mostrado es una representación del estado o propiedad, el


usuario no podrá editar el valor del control directamente. Veamos un ejemplo

Página | 154
para comprender esto. Crearemos un nuevo archivo llamado FormTest.js en el
path /app y lo dejaremos de la siguiente forma:

1. import React from 'react'


2.
3. class FormTest extends React.Component{
4.
5. constructor(props){
6. super(props)
7. this.state = {
8. field: "Init values"
9. }
10. }
11.
12. render(){
13.
14. return (
15. <input type="text" value={this.state.field} />
16. )
17. }
18. }
19. export default FormTest

A continuación, modificaremos el archivo App.js para mostrar el componente


FormTest en el método render. No olvidemos importar el componente:

1. render(){
2. return (
3. <FormTest/>
4. )
5. }

Finalmente guardamos los cambios y actualizamos el navegador para reflejar los


cambios:

Fig. 83 - Controlled Componente

Una vez actualizado el navegador, podremos ver el campo de texto con el valor
que pusimos en el estado del componente, y si intentamos actualizar el valor,
veremos que este simplemente no cambiará. Esto es debido a que React muestra
el valor como inmutable.

155 | Página
Observemos en la imagen que React nos ha arrojado un Warning, esta
advertencia se debe a que un campo controlado, deberá definir el atributo
onChange para controlar los cambios en el control, de lo contrario, este campo
será de solo lectura.

Para solucionar este problema, deberemos de crear una función que tome los
cambios en el control y posteriormente actualice el estado con el nuevo valor.
Cuando React detecte el cambio en el estado, iniciará la actualización de la vista
y los cambios será reflejados. Veamos cómo implementar esta función:

1. import React from 'react'


2. import update from 'react-addons-update'
3.
4. class FormTest extends React.Component{
5.
6. constructor(props){
7. super(props)
8. this.state = {
9. field: "Init values"
10. }
11. }
12.
13. updateField(e){
14. this.setState(update(this.state, {
15. field: {$set: e.target.value}
16. }))
17. }
18.
19. render(){
20. return (
21. <input type="text" value={this.state.field}
22. onChange={this.updateField.bind(this)}/>
23. )
24. }
25. }
26. export default FormTest

Lo primero será definir el atributo onChange y asignarle la función updateField.


Con esto, cuando se presione una tecla sobre el control, este lanzará un evento
que será tratado por esta función. Cuando la función reciba el evento de cambio,
actualizará el estado (línea 14) y React automáticamente actualizará la vista,
cuando esto pase, el campo tomará el nuevo valor del estado.

Todos los controles funcionan exactamente igual que en HTML tradicional, sin
embargo, existe dos controles que se utilizan de forma diferente, los cuales son
los TextArea y Select. Analicémoslo por separado.

TextArea

La forma tradicional de utilizar un TextArea es poner el valor entre la etiqueta de


apertura y cierre, como se ve en el siguiente ejemplo:

1. <textarea>{this.state.field}</textarea>

Página | 156
En React, para definir el valor de un TextArea se deberá utilizar el atributo value,
como podemos ver en el siguiente ejemplo:

1. <textarea value={this.state.field}/>

Select

El control Select cambia solo la forma en que seleccionamos el valor por default,
pues en HTML tradicional solo deberemos utilizar el atributo selected sobre el
valor por default, veamos un ejemplo:

1. <select>
2. <option value="volvo">Volvo</option>
3. <option value="saab">Saab</option>
4. <option value="vw">VW</option>
5. <option value="audi" selected>Audi</option>
6. </select>

En React, cambiamos el atributo selected sobre la etiqueta option, por el atributo


value sobre la etiqueta select, el cual deberá coincidir con el value del option,
veamos un ejemplo:

1. <select value="audi">
2. <option value="volvo">Volvo</option>
3. <option value="saab">Saab</option>
4. <option value="vw">VW</option>
5. <option value="audi">Audi</option>
6. </select>

Uncontrolled Components

Los componentes no controlados son aquellos que no están ligados al estado


o propiedades. Por lo general deberemos evitar utilizar controles no
controlados, pues se sale del principio de React, aunque esto no significa que no
debamos utilizarlos nunca, pues existen escenarios concretos en donde pueden
resultar útiles, como lo son formularios grandes, donde no requerimos realizar
acción alguna hasta que se manda el formulario.

Para crear un componente no controlado, están simple como definir el control sin
el atributo value. Al no definir este atributo, React sabrá que el valor de este
control es libre y permitirá su edición sin necesidad de crear una función que
controle los cambios en el control. Para analizar esto, modifiquemos el método
render del componente FormTest para que quede de la siguiente manera.

1. render(){
2. return (
3. <div>
4. <input type="text" value={this.state.field}
5. onChange={this.updateField.bind(this)}/>

157 | Página
6. <br/>
7. <input type="text" name="field2" defaultValue="Init Value 2" />
8. </div>
9. )
10. }

Otro de las peculiaridades de React es que ofrece el atributo defaultValue para


establecer un valor por default, y así poder inicializar el control con un texto
desde el comienzo. También será necesario utilizar el atributo name, pues será la
forma en que podremos recuperar el valor del campo al momento de procesar el
envío.

Una vez aplicados los cambios, actualizamos el valor del control y presionamos
el botón submit para que nos arroje en pantalla el valor capturado

Fig. 84 - Probando los controles no controlados.

Enviar el formulario
Ahora bien, si lo que queremos hacer es recuperar los valores del control al
momento de mandar el formulario, deberemos encapsular los controles dentro
de un form. Regresemos al archivo FormTest y actualicemos la función render
para que sea de la siguiente manera:

1. render(){
2. return (
3. <div>
4. <input type="text" value={this.state.field}
5. onChange={this.updateField.bind(this)}/>
6. <br/>
7. <form onSubmit={this.submitForm.bind(this)} >
8. <input type="text" name="field2" />
9. <br/>
10. <button type="submit">Submit</button>
11. </form>

Página | 158
12. </div>
13. )
14. }

Adicional, agregaremos la función submitForm:

1. submitForm(e){
2. alert(this.state.field)
3. alert(e.target.field2.value)
4. e.preventDefault();
5. }

Existen dos variantes para recuperar los valores de un control. Si está controlado,
solo tendremos que hacer referencia a la propiedad del estado al que está ligado
el campo (línea 2), por otra parte, si el campo no es controlado, entonces
deberemos recuperar el valor del campo mediante su tributo name, como lo vemos
en la línea 3.

preventDefault function
La función e.preventDefault previene que el
navegador mande el formulario al servidor, lo que
provocaría que el navegador se actualice y nos haga
un reset del estado del componente.

Debido a que React trabajo por lo general con AJAX, es importante impedir que
el navegador actualice la página, pues esto provocaría que los compontes sean
cargados de nuevo y nos borre los estados. Es por eso la importancia de utilizar
la función preventDefault.

Mini Twitter (Continuación 1)

Una vez que hemos explicado la forma de trabajar con los controles,
continuaremos desarrollando nuestro proyecto de Mini Twitter, pero en este
capítulo nos centraremos en los componentes que utilizan formularios.

El componente Signup

El primer componente que trabajaremos será Signup, el cual es un formulario de


registro, en donde los nuevos usuarios se registran para crear una cuenta en la
aplicación y poder autenticarse. La siguiente imagen muestra como se ve el
componente terminado.

159 | Página
Fig. 85 - Signup Component

Antes de irnos al código, entendamos como funciona. Para dar de alta a un nuevo
usuario, este deberá de capturar un nombre de usuario, su nombre y una
contraseña. El usuario deberá ser único, por lo que, si selecciona una ya
existente, la aplicación le mostrará una leyenda advirtiendo el error y no lo
deberá dejar continuar. El usuario también deberá confirmar que acepta los
términos de licencia, de lo contrario, tampoco podrá continuar.

Para confirmar el envío del formulario crearemos un botón, el cual tome los datos
capturados y cree el nuevo usuario mediante el API REST. Si todo sale bien, el
usuario será redirigido al componente de login, el cual crearemos en la siguiente
sección.

Finalmente, al final de la vista crearemos un link que lleve al usuario a la pantalla


de iniciar sesión. Esto con la intención de que los usuarios que ya tiene una
cuenta puedan autenticarse.

En esta pantalla utilizaremos dos servicios del API REST, el primero nos ayudará
a validar que el nombre de usuario no este repetido, y este se ejecutará cuando
el campo del nombre de usuario pierda el foco. El segundo servicio es el de
creación del usuario, el cual se ejecutará cuando el usuario presione el botón de
registro.

Página | 160
Nuevo concepto: Foco
Se dice que un elemento tiene el foco cuando este está
recibiendo los eventos de entrada como el teclado. Por
lo que cuando seleccionamos un campo de texto para
escribir sobre él, se dice que tiene el foco. Solo puede
existir un elemento con el foco en un tiempo
determinado.

Documentación: “Alta de usuarios” (Signup)


El servicio de alta de usuarios es el utilizado para crear
una nueva cuenta de usuario (/signup).

Documentación: “Validación de nombre de


usuario”
Este servicio es el encargado de validar que un nombre
de usuario no esté siendo utilizado ya por otro usuario
durante la creación de la cuenta
(/usernamevalidate/:username).

Iniciemos creando el formulario sin ninguna acción, y posteriormente iremos


agregando la integración con los servicios del API REST. Para esto, deberemos
crear un nuevo archivo en llamado Signup.js en el path /app y lo dejaremos
como se ve a continuación:

1. import React from 'react'


2. import update from 'react-addons-update'
3. import APIInvoker from './utils/APIInvoker'
4.
5. class Signup extends React.Component{
6.
7. constructor(){
8. super(...arguments)
9. this.state = {
10. username: "",
11. name:"",
12. password: "",
13. userOk: false,
14. license: false
15. }
16. }
17.
18. handleInput(e){
19. let field = e.target.name
20. let value = e.target.value
21. let type = e.target.type
22.
23. if(field === 'username'){
24. value = value.replace(' ','').replace('@','').substring(0, 15)
25. this.setState(update(this.state,{
26. [field] : {$set: value}
27. }))
28. }else if(type === 'checkbox'){
29. this.setState(update(this.state,{
30. [field] : {$set: e.target.checked}
31. }))
32.
33. }else{

161 | Página
34. this.setState(update(this.state,{
35. [field] : {$set: value}
36. }))
37. }
38. }
39.
40. render(){
41.
42. return (
43. <div id="signup">
44. <div className="container" >
45. <div className="row">
46. <div className="col-xs-12">
47.
48. </div>
49. </div>
50. </div>
51. <div className="signup-form">
52. <form>
53. <h1>Únite hoy a Twitter</h1>
54. <input type="text" value={this.state.username}
55. placeholder="@usuario" name="username" id="username"
56. onChange={this.handleInput.bind(this)}/>
57. <label ref="usernameLabel" id="usernameLabel"
58. htmlFor="username"></label>
59.
60. <input type="text" value={this.state.name} placeholder="Nombre"
61. name="name" id="name" onChange={this.handleInput.bind(this)}/>
62. <label ref="nameLabel" id="nameLabel" htmlFor="name"></label>
63.
64. <input type="password" id="passwordLabel"
65. value={this.state.password} placeholder="Contraseña"
66. name="password" onChange={this.handleInput.bind(this)}/>
67. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
68.
69. <input id="license" type="checkbox" ref="license"
70. value={this.state.license} name="license"
71. onChange={this.handleInput.bind(this)} />
72. <label htmlFor="license" >Acepto los terminos de licencia</label>
73.
74. <button className="btn btn-primary btn-lg"
75. id="submitBtn">Regístrate</button>
76. <label ref="submitBtnLabel" id="submitBtnLabel" htmlFor="submitBtn"
77. className="shake animated hidden "></label>
78. <p className="bg-danger user-test">
Crea un usuario o usa el usuario <strong>test/test</strong></p>
79. <p>¿Ya tienes cuenta? Iniciar sesión</p>
80. </form>
81. </div>
82. </div>
83. )
84. }
85. }
86. export default Signup

Para comprender mejor este componente dividiremos la explicación en 3 partes,


el constructor, la función handleInput y render

En el constructor no hay mucho que ver, salvo que iniciamos el estado (línea 9)
con los valores en blanco para username, name, password, los cuales son
precisamente los mismos campos que hay en la pantalla. Adicional, definimos
dos variables booleanas de control, userOk y license, la primera nos indica si el
nombre de usuario es válido (validado por el servicio de validación de nombre de

Página | 162
usuario) y license corresponde al checkbox para aceptar los términos de licencia.
Ambos deberán ser true para permitir la creación del usuario.

Como ya sabemos, la función render es la encargada de generar la vista, por lo


que no entraremos mucho en los detalles, pero si hablaremos de los controles
utilizados.

Podemos observar que creamos un input de texto para nombre de usuario (línea
54) y para el nombre (línea 60), los cuales están ligados a su campo
correspondiente del estado this.state.username y this.state.name
respectivamente, para el password (línea 64) creamos también un input de tipo
password ligado a this.state.password, finalmente, creamos otro input de tipo
checkbox para los términos de licencia (línea 69) ligado a this.state.license.

Cabe mencionar que todos estos controles tienen definido el atributo onChange
ligado a la función handleInput, el cual analizamos a continuación.

La función handleInput tiene como responsabilidad tomar los cambios realizados


a los controles y actualizar el estado, de tal forma que la vista pueda reflejar los
cambios. Recordemos que ya habíamos hablado de esto. Solo cabe resaltar que
limpiamos los espacios en blanco y la @ del nombre de usuario, también le
hacemos un substring a 15 caracteres (línea 24). Con esto impedimos que el
usuario utiliza una @ y espacios en blanco en su nombre de usuario, del mismo
modo, restringimos los nombres de usuario a 15 caracteres.

El siguiente paso es agregar los estilos (CSS) correspondientes, por lo que


regresamos al archivo estyles.css y agregamos las siguientes líneas al final del
archivo:

1. /** SIGNUP COMPONENT **/


2. #signup{
3. position: relative;
4. padding-top: 150px;
5. height: auto;
6. }
7.
8. #signup h1{
9. font-size: 27px;
10. margin-bottom: 30px;
11. }
12.
13. #signup .signup-form{
14. position: relative;
15. display: block;
16. width: 400px;
17. margin: 0 auto;
18. }
19.
20. #signup input{
21. margin-bottom: 30px;
22. }
23.
24. #signup .signup-form button{

163 | Página
25. background-color: #1DA1F2;
26. padding: 10px;
27. outline: none;
28. border: none;
29. }
30.
31. #signup input[type="text"],
32. #signup input[type="password"]{
33. outline: none;
34. border: 1px solid #999;
35. font-size: 18px;
36. width: 100%;
37. padding: 8px;
38.
39. border-radius: 5px;
40. }
41.
42. #signup button{
43. display: block;
44. padding: 5px;
45. width: 100%;
46.
47. }
48.
49. #signup .user-test{
50. margin-top: 30px;
51. padding: 10px;
52. border-radius: 5px;
53. }
54.
55.
56. #signup #usernameLabel{
57. display: inline-block;
58. position: absolute;
59. width: 100%;
60. padding: 10px;
61. }
62.
63. #signup #usernameLabel.ok{
64. color: #1DA1F2;
65. }
66.
67. #signup #usernameLabel.fail{
68. color: tomato;
69. }
70.
71. #signup #submitBtnLabel{
72. display: block;
73. text-align: center;
74. color: red;
75. margin-top: 20px;
76. }

Ya con los estilos agregados, deberemos actualizar el archivo App.js para mostrar
el componente Signup al iniciar la aplicación:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import APIInvoker from "./utils/APIInvoker"
4. import TweetsContainer from './TweetsContainer'
5. import FormTest from './FormTest'
6. import Signup from './Signup'
7. import Login from './Login'
8.
9. class App extends React.Component{

Página | 164
10.
11. constructor(props){
12. super(props)
13. }
14.
15. render(){
16. return (
17. <Signup/>
18. )
19. }
20. }
21.
22. render(<App/>, document.getElementById('root'));

Finalmente actualizamos el navegador para comprobar que el formulario de


registro ya se ve correctamente:

Fig. 86 - Vista previa del Componente Signup.

En este punto, el formulario puede capturar los datos del usuario y actualizando
el estado al mismo tiempo que esto pasa.

Validando usuario único

Para asegurarnos de que el nombre de usuario capturado sea único antes de


enviar el formulario, deberemos ejecutar un servicio que nos proporciona el API
REST cada vez que el campo del usuario pierda el foco. Para esto, vamos a
regresar al componente Signup y vamos a crear la función validateUser.

165 | Página
1. validateUser(e){
2. let username = e.target.value
3. APIInvoker.invokeGET('/usernameValidate/' + username, response => {
4. this.setState(update(this.state, {
5. userOk: {$set: true}
6. }))
7. this.refs.usernameLabel.innerHTML = response.message
8. this.refs.usernameLabel.className = 'fadeIn animated ok'
9. },error => {
10. console.log("Error al cargar los Tweets");
11. this.setState(update(this.state,{
12. userOk: {$set: false}
13. }))
14. this.refs.usernameLabel.innerHTML = error.message
15. this.refs.usernameLabel.className = 'fadeIn animated fail'
16. })
17. }

Y agregaremos el evento onBlur al campo username:

1. <input type="text" value={this.state.username}


2. placeholder="@usuario" name="username" id="username"
3. onBlur={this.validateUser.bind(this)}
4. onChange={this.handleInput.bind(this)}/>

Cuando el campo username pierda el foco, llamará la función validateUser, la cual


funciona de la siguiente manera: en la línea 2 de la función recuperamos el valor
del campo, luego consumimos el servicio /usernameValidate/{username} (línea
3), donde {username} corresponde al usuario capturado. Si el servicio responde
correctamente, entonces actualizamos el estado con el campo userOk = true
(línea 5), de lo contrario, establecemos el campo userOk = false y mandamos
una leyenda a al usuario (línea 14 y 15).

Fig. 87 - Validación del nombre de usuario único.

Para probar la validación, solo es necesario introducir como nombre de usuario


test y presionar tabulador. Como el usuario test ya existe, React nos arrojará el
error.

Crear el usuario

Para finalizar el componente, solo nos queda habilitar la función para que se cree
el usuario, para lo cual deberemos hacer 3 cambios a nuestro componente. El
primer cambio será agregar el evento onClick al botón para que llame la función
signup justo después de presionar el botón.

1. <button className="btn btn-primary btn-lg " id="submitBtn"


2. onClick={this.signup.bind(this)}>Regístrate</button>

Página | 166
El segundo cambio es agregar el evento onSubmit a la etiqueta form para llamar
la función signup.

1. <form onSubmit={this.signup.bind(this)}>

Notemos que estamos mandando llamar la función signup desde el botón y desde
la etiqueta form, lo que puede resultar redundante o confuso, pero existe una
razón por la cual hacerlo así. Cuando el usuario presione el botón directamente,
entonces se mandará llamar la función por medio del evento onClick, sin
embargo, si el usuario decide presionar enter sobre algún campo en lugar del
botón, entonces el evento onSubmit procesará la solicitud.

Finalmente, solo nos falta agregar el método singup al componte:

1. signup(e){
2. e.preventDefault()
3.
4. if(!this.state.license){
5. this.refs.submitBtnLabel.innerHTML =
6. 'Acepte los términos de licencia'
7. this.refs.submitBtnLabel.className = 'shake animated'
8. return
9. }else if(!this.state.userOk){
10. this.refs.submitBtnLabel.innerHTML =
11. 'Favor de revisar su nombre de usuario'
12. this.refs.submitBtnLabel.className = ''
13. return
14. }
15.
16. this.refs.submitBtnLabel.innerHTML = ''
17. this.refs.submitBtnLabel.className = ''
18.
19. let request = {
20. "name": this.state.name,
21. "username": this.state.username,
22. "password": this.state.password
23. }
24.
25. APIInvoker.invokePOST('/signup',request, response => {
26. //browserHistory.push('/login');
27. alert('Usuario registrado correctamente')
28. },error => {
29. this.refs.submitBtnLabel.innerHTML = response.error
30. this.refs.submitBtnLabel.className = 'shake animated'
31. })
32. }

Recordemos que debemos mandar llamar la función preventDefault (línea 2)


para evitar que el navegador manda una solicitud al servidor y actualice la
pantalla.

Veamos que en las líneas 4 y 9 validamos que los campos license y userOk del
estado sean true, de lo contrario mandamos el error correspondiente al usuario.
Si las validaciones son correctas, entonces limpiamos cualquier error sobre la
vista (líneas 16 y 17).

167 | Página
En la línea 19 creamos el request para crear el usuario por medio del API REST,
el cual contiene el nombre (name), el nombre de usuario (username) y el password.

Finalmente, en la línea 25 se ejecuta la operación signup del API REST utilizando


el método POST. Si el servicio responde correctamente, entonces
redireccionamos al usuario a la vista de login, sin embargo, como todavía no
desarrollamos este componente, hemos comentado esta línea y en su lugar
hemos puesto un alert para darnos cuenta de que todo funcionó bien. Por otro
lado, si el servicio retorna con algún error (línea 28) entonces actualizamos la
vista con dicho error.

Librería de animación
En la línea 7 y 30 estamos haciendo uso de la clase de
estilo animated, la cual es proporcionada por librería
de animación Animate, la cual sirve para dar efectos
de entrada y salida a los elementos, puede revisar su
documentación en
https://daneden.github.io/animate.css/
Esta librería la importamos en el archivo index.html
(línea 5)

Una vez terminados estos cambios, actualizamos el navegador e intentamos


crear un nuevo usuario, si todo funciona bien, la pantalla nos arrojara un mensaje
de éxito.

Fig. 88 - Registro existo se un nuevo usuario.

Página | 168
El componente login

En esta sección desarrollaremos el componente Login, el cual es un formulario


de autenticación, en donde el usuario deberá capturar su nombre de usuario y
contraseña para tener acceso.

Este componente es muy parecido al anterior, pero mucho más simples, pues
este solo tiene dos campos de texto, tiene el botón para iniciar sesión y un link
que lleva al usuario a la pantalla de registro en caso de que no tenga una cuenta
registrada.

Fig. 89 - Vista previa del componente Login.

Iniciaremos creando un archivo llamado Login.js en el path /app con el siguiente


contenido:

1. import React from 'react'


2. import update from 'react-addons-update'
3. import APIInvoker from './utils/APIInvoker'
4.
5. class Login extends React.Component{
6.
7. constructor(){
8. super(...arguments)
9. this.state = {
10. username: "",
11. password: ""
12. }
13. }
14.
15. handleInput(e){
16. let field = e.target.name
17. let value = e.target.value
18.
19.

169 | Página
20. if(field === 'username'){
21. value = value.replace(' ','').replace('@','').substring(0, 15)
22. this.setState(update(this.state,{
23. [field] : {$set: value}
24. }))
25. }
26.
27. this.setState(update(this.state,{
28. [field] : {$set: value}
29. }))
30. }
31.
32. login(e){
33. e.preventDefault()
34.
35. let request = {
36. "username": this.state.username,
37. "password": this.state.password
38. }
39.
40. APIInvoker.invokePOST('/login',request, response => {
41. window.localStorage.setItem("token", response.token)
42. window.localStorage.setItem("username", response.profile.userName)
43. window.location = '/'
44. },error => {
45. this.refs.submitBtnLabel.innerHTML = error.message
46. this.refs.submitBtnLabel.className = 'shake animated'
47. console.log("Error en la autenticación")
48. })
49. }
50.
51. render(){
52.
53. return(
54. <div id="signup">
55. <div className="container" >
56. <div className="row">
57. <div className="col-xs-12">
58. </div>
59. </div>
60. </div>
61. <div className="signup-form">
62. <form onSubmit={this.login.bind(this)}>
63. <h1>Iniciar sesión en Twitter</h1>
64.
65. <input type="text" value={this.state.username}
66. placeholder="usuario" name="username" id="username"
67. onChange={this.handleInput.bind(this)}/>
68. <label ref="usernameLabel" id="usernameLabel"
69. htmlFor="username"></label>
70.
71. <input type="password" id="passwordLabel"
72. value={this.state.password} placeholder="Contraseña"
73. name="password" onChange={this.handleInput.bind(this)}/>
74. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
75.
76. <button className="btn btn-primary btn-lg " id="submitBtn"
77. onClick={this.login.bind(this)}>Regístrate</button>
78. <label ref="submitBtnLabel" id="submitBtnLabel" htmlFor="submitBtn"
79. className="shake animated hidden "></label>
80. <p className="bg-danger user-test">
81. Crea un usuario o usa el usuario
82. <strong>test/test</strong></p>
83. <p>¿No tienes una cuenta? Registrate</p>
84. </form>
85. </div>

Página | 170
86. </div>
87. )
88. }
89. }
90. export default Login

Nuevamente vamos a dividir la explicación en 3 secciones, el constructor, la


función render y login.

En el constructor no hay mucho que ver, salvo que iniciamos el estado (línea 9)
con el nombre de usuario y password con un valor en blanco.

En la función render creamos el campo username (línea 65) de tipo text el cual
está ligado al estado mediante this.state.username, creamos otro campo de
texto de tipo password también ligado al estado, mediante this.state.password.
Creamos el botón (línea 76) que procesará la autenticación mediante la función
login. Como podemos ver, estamos haciendo exactamente lo mismo que en el
componente Signup.

Cuando el usuario presione el botón para autenticarse, este mandara llamar la


función login, la cual recupera los datos del formulario para crear el request
(línea 35) para el API REST. Seguido se hace la llamada al servicio /login
mediante el método POST. Si el login se llega a cabo correctamente, entonces
guardamos en el LocalStorage un token (línea 41) que genera el API y el nombre
del usuario registrado (línea 42). Adicional, la aplicación redirección al usuario al
usuario a la raíz de la aplicación (línea 43). De momento la raíz es la misma
página de login, pero más adelante cambiaremos esto para que la raíz sea donde
se muestran los Tweets.

Nuevo concepto: Token


Un token es una cadena alfanumérica encriptada la
cual sirve para que el usuario pueda autenticarse en el
API. El token no tiene un significado claro para el
usuario, pero para el API si tiene un significado.

Nuevo concepto: LocalStorage


LocalStorage es una de las nuevas características de
HTML5, la cual permite almacenar información
persistente del lado del navegador, de tal forma que
esta permanece aun cuando cerramos el navegador.

Por el momento solo guardaremos el token, pero más adelante lo retomaremos


para utilizarlo al consumir servicios de API REST. En el capítulo 15 nos
introduciremos de lleno al desarrollo del API REST y hablaremos de cómo es que
el token se genera y como lo utilizamos para autenticar al usuario, pero por
ahora, solo es importante saber que el token lo genera el API y nosotros lo
guardamos en el LocalStorage.

171 | Página
Por último, regresaremos al archivo App.js para retornar el componente Login,
para esto, agregamos el import al componente Login y modificaremos la función
render para que se vea de la siguiente manera:

1. render(){
2. return (
3. <Login/>
4. )
5. }

Si hemos hecho todo bien hasta ahora, ya deberíamos de poder ver la página de
login al actualizar el navegador, y cuando esto pase, podremos intentar
autenticarnos con el usuario test/1234.

Si el usuario es incorrecto entonces la aplicación nos advertirá, pero si todo está


bien, entonces aparentemente no pasará nada, pues recordemos que la idea es
que nos redirecciones a la raíz, la cual es la mima página en este momento. Sin
embargo, el LocalStorage debería haberse afectado. Para validar esto,
tendremos que entrar al inspector de Chrome y dirigirnos a la pestaña
Application, luego del lado izquierdo, buscamos la sección que dice Local
Storage y seleccionamos la página localhost, en el centro de la pantalla nos
arrojará el token y el username registrado, tal como se ve en la siguiente imagen.

Fig. 90 - Analizando el LocalStorae.

El token tiene una vigencia de 24 hrs, por lo que, al pasar este tiempo, el token
ya no servirá y será necesario autenticarnos de nuevo para renovarlo.

Página | 172
173 | Página
Resumen

En esta unidad hemos analizado la forma de trabajar con Formularios, y como es


que React administra los Controles. También hemos analizado la diferencia que
existe entre controles controlados y no controlados.

Hemos hablado de que los controles no controlados son por lo general una mala
práctica, pues difieren de la propuesta de trabajo de React, aunque también
hemos dicho que existe situaciones en las que podría ser una buena idea, lo
importante es no abusar del uso de controles no controlados.

También hemos creado el componente de registro y de autenticación,


apoyándonos del API REST para recuperar el token que más adelante
utilizaremos para consumir servicios restringidos del API.

Página | 174
Ciclo de vida de los componentes
Capítulo 8

El ciclo de vida (life cycle) de un componente, representa las etapas por las que
un componente pasa durante toda su vida, desde la creación hasta que es
destruido. Conocer el ciclo de vida ayuda de un componente es muy importante
debido a que nos permite saber cómo es que un componente se comporta
durante todo su tiempo de vida y nos permite prevenir la gran mayoría de los
errores que se provocan en tiempo de ejecución.

Hasta el momento hemos hablado de muchas características de React, como lo


son las props, los estados, el shadow dom y controles, pero poco hemos hablado
acerca de cómo React administra los componentes. Seguramente recordarás que
hablamos muy superficialmente del método componentWillMount que
implementamos en el componente TweetsContainer, en cual dijimos que React lo
ejecutaba justo antes de montar el componente. Así como existe este método,
existe varios más que tenemos que analizar para entender como React monta,
actualiza y desmonta un componente.

Fig. 91 - Ciclo de vida de los componentes en React.

175 | Página
En la imagen podemos apreciar los métodos que conforman el ciclo de vida de
un componente en React, los métodos están listados en el orden en que React
los ejecuta, aunque hay métodos que no siempre se ejecutan.

Function componentWillMount

La función componentWillMount() se ejecuta antes de que el componente sea


montado y antes de la función render(). Se utiliza por lo general para realizar la
carga de datos desde el servidor o para realizar una inicialización síncrona del
estado.

Esta función no recibe ningún parámetro y se ve de la siguiente manera:

1. componentWillMount(){
2. //Any action
3. }

En este punto, los elementos del componente no podrán ser accedidos por medio
del DOM, pues aún no han sido creados.

Function render

La función render() se encarga de generar los elementos que conformará el


componente, en ella de utiliza JSX para retornar la vista. La función no recibe
parámetros y se ejecuta inicialmente cuando el componente es montado por
primera vez o cuando React requiere actualizar el componente. La función tiene
el siguiente formato:

1. render(){
2. // Vars and logic sección
3. return (
4. //JSX section
5. )
6. }

Function componentDidMount

La función componentDidMount() se ejecuta después del método render(), y es


utilizado para cargar datos del servidor o para realizar operaciones que requieren

Página | 176
elementos del DOM. En este punto todos los elementos ya existen en el DOM y
pueden ser accedidos.

1. componentDidMount(){
2. //Any action
3. }

Function componentWillReceiveProps

La función componentWillReceiveProps(nextProps) se invoca cuando el


componente recibe nuevas propiedades. Este método no se ejecuta durante
montaje inicial.

Recibe la variable nextProps que contiene los valores de las nuevas propiedades,
por lo que es posible comparar los nuevos valores (nextProps) contra los props
actuales (this.props) para determinar si tenemos que realizar alguna acción para
actualizar el componente.

1. componentWillReceiveProps(nextProps){
2. // Any action
3. }

Hay que tener en cuenta que esta función se puede llamar incluso si no ha habido
ningún cambio en las props, por ejemplo, cuando el componente padre es
actualizado.

Function shouldComponentUpdate

La función shouldComponentUpdate(nextProps, nextState) se ejecuta justo antes


de la función render() y se utiliza para determinar si un componente requiere ser
actualizado o se puede saltar la actualización. Por default, React determina si un
componente debe de ser actualizado por lo que no es necesario declarar este
método, sin embargo, existe escenarios donde por cuestiones de performance
requeridos determinar nosotros mismo si el componte requiere o no ser
actualizado.

Esta función recibe las nuevas propiedades (nextProps) y el nuevo estado


(nextState) y deberá de retornar forzosamente un valor booleano, donde un true
le indica a React que el componente debe de ser actualizado y false en caso
contrario.

1. shouldComponentUpdate(nextProps, nextState) {
2. // Any action
3. return boolean
4. }

177 | Página
Function componentWillUpdate

La función componentWillUpdate(nextProps, nextState) se ejecuta antes de la


función render() cuando se reciben nuevas propiedades o estado. Se utiliza para
preparar el componente antes de la actualización. Este método no se llama
durante el montado inicial del componente.

Tenga en cuenta que no se puede llamar a this.setState() desde aquí. En caso


de requerir actualizar el estado en respuesta a un cambio en las propiedades,
entonces deberá utilizar el método componentWillReceiveProps().

1. componentWillUpdate(nextProps, nextState) {
2. // Any action
3. }

La función componentWillUpdate() no será invocado si la función


shouldComponentUpdate() retorna false.

Function componentDidUpdate

La función componentDidUpdate(prevProps, prevState) se ejecuta justo después


del método render(), es decir, una vez que el componente se ha actualizado en
la vista. Esta función recibe como parámetro el estado y las propiedades
anteriores.

Esta función se utiliza para realizar operaciones sobre los elementos del DOM o
para consumir recursos de red, imágenes o servicios del API. Sin embargo, es
necesario validar el nuevo estado y props contra los anteriores, para determinar
si es necesario realizar alguna acción.

1. componentDidUpdate(prevProps, prevState){
2. // Any action
3. }

Function componentWillUnmount

La función componentWillUnmount() se invoca inmediatamente antes de que un


componente sea desmontado y destruido. Se utiliza para realizar tareas de
limpieza, como invalidar temporizadores, cancelar solicitudes de red o limpiar
cualquier elemento del DOM. Esta función no recibe ningún parámetro.

1. componentWillUnmount() {
2. // Any action

Página | 178
3. }

Flujos de montado de un componente

En esta sección se describe el ciclo de vida por el cual pasa un componente


cuando es montado por primera vez. Se dice que un componente es montado
cuando es creado inicialmente y mostrado por primera vez en pantalla.

Fig. 92 - Ciclo de vida del montaje de un componente.

Cuando un componente es montado inicialmente, siempre se ejecutará el


constructor de la clase, pues el componente no existe en ese momento. El
constructor lo podemos utilizar para inicializar el estado y no se recomienda
realizar operaciones de carga de servicios. Cabe mencionar que el constructor no
es precisamente parte del ciclo de vida del componente, sino más bien es una
característica de los lenguajes orientados a objetos.

Una vez que el objeto es creado mediante su constructor, el método


componentWillMount es ejecutado. En este momento el componente no se ha
mostrado en pantalla, por lo tanto, los elementos no existen en el DOM y no será
posible realizar ninguna operación que requiera actualizar los elementos del
componente en el DOM. Se suele utilizar para realizar carga de datos por medio
de los servicios o para inicializar el estado mediante this.setState().

El siguiente paso es la renderización del componente mediante el método


render(). En este momento son creados los elementos en el DOM y el
componente es mostrado en pantalla. En este punto no debemos actualizar el
estado mediante setState(), pues podemos ciclar en una actualización sin fin.

179 | Página
Una vez que React ha terminado de renderizar el componente mediante
render(), ejecuta la función componentDidMount para darle la oportunidad a la
aplicación de realizar operaciones sobre los elementos del DOM. En este punto
es recomendable realizar carga de datos o simplemente actualizar elementos
directamente sobre el DOM.

Flujos de actualización del estado

Un componente inicia el proceso de actualización del estado cuando el estado es


establecido mediante this.setState(), pero también las propiedades entrantes
pueden detonar en la actualización del estado, por lo que en ocasiones la
actualización del componente padre, puede detonar en la actualización del estado
en los elementos hijos.

Fig. 93 – Ciclo de vida de la actualización del estado.

Cuando React detecta cambios en el estado, lo primero que hará será validar si
el componente debe ser o no actualizado, para esto, existe la función
shouldComponentUpdate. Esta función deberá retornar true en caso de que el
componente requiera una actualización y false en caso contrario. Si esta función
retorna false, el ciclo de vida se detiene y no se ejecuta el resto de los métodos.

La función componenteWillUpdate es ejecutada siempre y cuando el método


shouldComponentUpdate retorna true. Desde aquí es posible realizar carga de
datos del servidor o actualizar el estado.

El siguiente paso es la actualización del componente en pantalla, mediante la


ejecución de la función render(), en este punto los elementos son creados en el
DOM.

Página | 180
Cuando el componente es actualizado en pantalla, se ejecuta la función
componentDidUpdate, el cual permite realizar carga de datos o realizar operaciones
que requiera de la existencia de los elementos en el DOM.

Flujos de actualización de las


propiedades
Este flujo se activa cuando el componente padre manda nuevas propiedades al
componente hijo. Si recordamos, las propiedades son inmutable, por lo que un
componente no debe de actualizar las propiedades, un que si puede recibir
nuevas propiedades por parte de los componentes padre.

Fig. 94 - Ciclo de vida en la actualización de las propiedades.

Primero que nada, hay que resaltar que el ciclo de vida de la actualización de las
propiedades, es el mismo que el ciclo de vida del cambio de estado, con una
única diferencia, en este ciclo se ejecuta primero que nada la función
componentWillReceiveProps, el cual recibe como parámetro las nuevas
propiedades enviadas, incluso antes de actualizar el componente con estas
propiedades.

Validar si el componente debe ser o no actualizado es el segundo paso, para esto,


existe la función shouldComponentUpdate. Esta función deberá retornar true en
caso de que el componente requiera una actualización y false en caso contrario.
Si esta función retorna false, el ciclo de vida se detiene y no se ejecuta el resto
de los métodos.

181 | Página
La función componenteWillUpdate es ejecutada siempre y cuando el método
shouldComponentUpdate retorna true y permite realizar carga de datos del servidor
o actualizar el estado. En este punto los elementos no existen en el DOM.

El siguiente paso es la actualización del componente en pantalla, mediante la


ejecución de la función render(), en este punto los elementos son creados en el
DOM.

Cuando el componente es actualizado en pantalla, se ejecuta la función


componentDidUpdate, el cual permite realizar carga de datos o realizar operaciones
que requiera de la existencia de los elementos en el DOM.

Flujos de desmontaje de un componente

El ciclo de vida de desmontaje se activa cuando un componente ya no es


requerido más y requiere ser eliminado. El desmontaje se pueda dar por ejemplo
cuando un componente es eliminado de una lista o un componente es remplazado
por otro.

Fig. 95 - Ciclo de vida del desmontaje de un componente.

El ciclo de vida de desmontaje es el más simple, pues solo se ejecuta la función


componenteWillUnmount, la cual es utilizada para realizar alguna limpieza o
eliminar algún elemento del DOM antes de que el componente sea destruido.

Mini Twitter (Continuación 2)

En esta sección continuaremos desarrollando nuestro proyecto Mini Twitter. Con


los nuevos conocimientos podremos continuar desarrollando componentes más
complejos y al terminar esta sección podremos ir viendo como el proyecto va
tomando forma.

Página | 182
El componente TwitterApp

En el capítulo 4 hablamos de la jerarquía en los componentes y como los


componentes se van construyendo utilizando otros componentes, creado una
jerarquía en la cual, un componente complejo se crea mediante una serie de
componentes más simples. ¿Pero por qué menciono esto ahora? La razón es que
el componente TwitterApp se convertirá en el componente de más alto nivel en
la jerarquía de nuestra aplicación y a partir de este momento, todos los
componentes que creemos estarán contenidos dentro de este componente.

El componente TwitterApp es solo un contenedor, lo que quiere decir que solo se


limita a mostrar otros componentes dentro de él, por ejemplo, puede mostrar el
perfil de un usuario, la página de login o registro, etc. Pero adicionalmente tiene
una responsabilidad clave, este componente se encarga de determinar si un
usuario esta autenticado o no, por lo que, dependiendo de esto, se puede mostrar
un componente diferente. Si el usuario esta autenticado, se debería de mostrar
el muro del usuario, pero si no está autenticado, deberá de mandarlo a la página
de Login.

Para iniciar, creemos el archivo TwitterApp.js en el path /app, el cual deberá


tener el siguiente contenido:

1. import React from 'react'


2. import APIInvoker from "./utils/APIInvoker"
3. import { browserHistory } from 'react-router'
4. import TweetsContainer from './TweetsContainer'
5. import Login from './Login'
6.
7. class TwitterApp extends React.Component{
8.
9. constructor(props){
10. super(props)
11. this.state = {
12. load: true,
13. profile: null
14. }
15. }
16.
17. componentWillMount(){
18. let token = window.localStorage.getItem("token")
19. if(token == null){
20. browserHistory.push('/login')
21. this.setState({
22. load: true,
23. profile: null
24. })
25. }else{
26. APIInvoker.invokeGET('/secure/relogin', response => {
27. this.setState({
28. load: true,
29. profile: response.profile

183 | Página
30. });
31. window.localStorage.setItem("token", response.token)
32. window.localStorage.setItem("username", response.profile.userName)
33. },error => {
34. console.log("Error al autenticar al autenticar al usuario " );
35. window.localStorage.removeItem("token")
36. window.localStorage.removeItem("username")
37. browserHistory.push('/login');
38. })
39. }
40. }
41.
42. render(){
43.
44. return (
45. <div id="mainApp">
46. <Choose>
47. <When condition={!this.state.load}>
48. <div className="tweet-detail">
49. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
50. </div>
51. </When>
52. <When condition={this.props.children == null
53. && this.state.profile != null}>
54. <TweetsContainer/>
55. </When>
56. <Otherwise>
57. {this.props.children}
58. </Otherwise>
59. </Choose>
60. <div id="dialog"/>
61. </div>
62. )
63. }
64. }
65. export default TwitterApp;

Primero que nada, analizaremos la función render, desde la cual construiremos


la vista. La vista esta generada a partir de una serie de condiciones, las cuales,
determinan lo que se ve en pantalla. En este caso, utilizamos un <Choose> para
crear una serie de condiciones.

La primera condición (línea 47) valida si se ha terminado de validar si el usuario


esta autenticado, si la condición se resuelve como verdadera, indica que se sigue
validando y se muestra una animación de cargando.

La segunda condición (línea 52) valida si el perfil del usuario ya fue cargado, lo
cual solo ocurrirá cuando el usuario se encuentra autenticado. En la condición
vemos la instrucción this.props.children el cual analizaremos a continuación.

Finalmente tenemos la sección <Otherwise> la cual es especial, pues utiliza una


nueva característica de React, que son los componentes hijos (children). Por lo
pronto no entraremos en los detalles, pues analizaremos mejor este concepto en
la siguiente sección. Por lo pronto, basta saber, que this.props.children
contiene todos los componentes hijos, los cuales son incluidos dinámicamente
dentro de la vista. En este caso, es utilizado para incluir la pantalla de Login,
cuando el usuario no está autenticado.

Página | 184
Ya explicada la vista, entraremos en los detalles acerca de la forma en que un
usuario es autenticado una vez que inicia la aplicación, por tal motivo
analizaremos la función componentWillMount de la cual ya hemos hablado en el
pasado. Esta función tiene dos posibles escenarios, los cuales describiremos a
continuación:

 El usuario no está autenticado: Lo primero que realizará la función


será determinar si un usuario ya está autenticado en la aplicación, para
esto, se apoyará del LocalStorage (línea 18) para obtener el token de
autenticación. Recordemos que el token lo obtenemos en la pantalla de
Login. Si al consultar el token, este es nulo (línea 19) quiere decir que el
usuario no se ha autenticado, por lo que será redirigido a la pantalla de
login (línea 20) y actualizamos la propiedad load a true, para indicar que
hemos terminado de validar la autenticación del usuario.
 El usuario si está autenticado: En caso de que el token sea diferente
de nulo, nos indica que el usuario ya se ha autenticado en el pasado, sin
embargo, no hay garantía de que el token esté vigente. Para validar su
vigencia, nos apoyamos de un nuevo servicio llamado relogin (línea 26),
al cual solo le enviaremos nuestro token actual. Si el token es válido, el
API nos creará un nuevo token, con una nueva vigencia. Si la validación
se lleva a cabo correctamente, actualizamos el estado (línea 27) con la
propiedad load a verdadero y actualizamos la propiedad profile con el
perfil que nos ha retornado el servicio. Finalmente, actualizamos el
LocalStorage (líneas 31 y 32) para almacenar los nuevos valores del
token y username devueltos por el servicio relogin. Por otra parte, si la
autenticación con el token no se ha llevado correctamente, entonces
procedemos a remover las propiedades token y username del
LocalStorage (líneas 35 y 36) para evitar nuevos intentos de
autenticación con un token caduco y redirigimos al usuario a la pantalla
de Login (línea 37).

Documentación: actualización de credenciales


(relogin)
Este servicio tiene como finalidad validar si un token
es vigente y darnos un nuevo token con una vigencia
actualizada (/secure/relogin)

Como te habrás dado cuenta, estamos haciendo uso del objeto browserHistory,
el cual es una utilidad del módulo react-router que no hemos utilizado hasta el
momento, por lo que será necesario instalarlo mediante npm, para lo cual
utilizaremos la instrucción
 npm install --save [email protected]
 npm install --save [email protected]

Por otra parte, tendremos que actualiza drásticamente el archivo App.js para que
quede de la siguiente manera:

185 | Página
1. import React from 'react'
2. import { render } from 'react-dom'
3. import APIInvoker from "./utils/APIInvoker"
4. import TwitterApp from './TwitterApp'
5. import Signup from './Signup'
6. import Login from './Login'
7. import { Router, Route, browserHistory, IndexRoute } from "react-router";
8.
9. var createBrowserHistory = require('history/createBrowserHistory')
10.
11. render((
12. <Router history={ browserHistory }>
13. <Router component={TwitterApp} path="/">
14. <Route path="/signup" component={Signup}/>
15. <Route path="/login" component={Login}/>
16. </Router>
17. </Router>
18. ), document.getElementById('root'));

Nuevamente no entraremos en muchos detalles, pues todo lo que vemos aquí es


nuevo y lo abordaremos en la siguiente sección de este libro, cuando abordemos
de lleno el tema de React Router. Por ahora solo limitémonos a dejar el archivo
tal cual se muestra.

Solo me adelantaré un poco, si ves la línea 13, podrás ver que el componente
Raíz es TwitterApp y dentro de él, están los componentes Signup (línea 14) en el
path /signup y Login (línea 14) en el path /login. Ahora bien, ¿recuerdas la
propiedad this.props.children? pues lo que estamos indicando es que tanto el
componente Login como Signup son hijos de TwitterApp pero solo se activan bajo
ciertos Paths, ¿recuerdas la instrucción browserHistory.push(‘/login’) y
browserHistory.push(‘/signup)? Pues lo que hacemos es justamente activar los
componentes hijos, redireccionando al usuario.

En este punto, nuestra aplicación debería funcionar perfectamente, de tal forma


que, si no estamos autenticados, nos deberá mandar a la pantalla de Login, y en
casi de si estar autenticado nos deberá mostrar los Tweets.

Fig. 96 - Usuario no autenticado.

Página | 186
Fig. 97 - Usuario autenticado.

El componente TwitterDashboard

TwitterDashboard es el componente central de la aplicación, pues en él se


muestran nuestro Perfil, lo Tweets y los usuarios sugeridos y es la pantalla a la
cual el usuario es dirigido una vez que se autentifica. Para comprender mejor
como está conformado este componente, regresará a la siguiente imagen:

Fig. 98 – Componente TwitterDashboard.

Lo único que no pertenece a este componente es el Toolbar, el cual veremos más


adelante. Todo lo demás forma parte del componente.

Antes de comenzar, me gustaría aclarar que este componente es solo un


contendedor, y no tiene ninguna lógica, y su único propósito es distribuir los
componentes Profile, TweetsContainer y SuggestedUser. Dicho esto, iniciemos
con su construcción, por lo que iniciaremos creando el archivo

187 | Página
TwitterDashboard.js en el path /app, el cual se deberá ver de la siguiente
manera:

1. import React from 'react'


2. import Profile from './profile'
3. import TweetsContainer from './TweetsContainer'
4. import SuggestedUser from './SuggestedUser'
5. import PropTypes from 'prop-types'
6.
7. class TwitterDashboard extends React.Component{
8.
9. render(){
10. return(
11. <div id="dashboard">
12. <div className="container-fluid">
13. <div className="row">
14. <div className="hidden-xs col-sm-4 col-md-push-1
15. col-md-3 col-lg-push-1 col-lg-3" >
16. <Profile profile={this.props.profile}/>
17. </div>
18. <div className="col-xs-12 col-sm-8 col-md-push-1
19. col-md-7 col-lg-push-1 col-lg-4">
20. <TweetsContainer profile={this.props.profile} />
21. </div>
22. <div className="hidden-xs hidden-sm hidden-md
23. col-lg-push-1 col-lg-3">
24. <SuggestedUser/>
25. </div>
26. </div>
27. </div>
28. </div>
29. )
30. }
31. }
32.
33. TwitterDashboard.propTypes = {
34. profile: PropTypes.object.isRequired
35. }
36.
37. export default TwitterDashboard;

Como podemos observar, este componente no tiene nada nuevo en lo cual


debamos detenernos a explicar. Sin embargo, cabe mencionar que el método
render muestra columnas, en las cuales se muestra cada uno de los tres
compontes (Profile, TweetsContainer y SuggestedUser). Con ayuda de Bootstrap
hacemos que sean responsivas, de tal forma que, en pantallas grandes, se
muestran las 3 columnas, en pantallas medianas como tablets, solo se verán dos
y en dispositivos pequeños como Smarphones solo se verá una.

Otra cosa a tomar en cuenta es que los componentes Profile y SuggestedUser


todavía no los desarrollamos, por lo que no podremos ejecutar este cambio hasta
tenerlos. Por lo que por ahora continuaremos con estos dos componentes.

También podemos observar que la propiedad profile es requerida y esta deberá


de contener los datos del usuario. Pues será utiliza para ser enviada en la creación
de los componentes Profile y TweetsContainer como propiedad.

Página | 188
El componente Profile

El componente Profile es un Widget que muy simple, que muestra nuestro


status en la red social, en el podemos ver nuestra foto, nombre de usuario,
número de seguidores, número de personas a las que seguimos y un contador
de Tweets.

Iniciaremos creado un archivo llamado Profile.js en el path /app, el cual deberá


tener la siguiente estructura:

1. import React from 'react'


2. import { Link } from 'react-router'
3. import PropTypes from 'prop-types'
4.
5. class Profile extends React.Component{
6.
7. constructor(props){
8. super(props)
9. this.state = {}
10. }
11.
12.
13. render(){
14. let bannerStyle = {
15. backgroundImage: (this.props.profile.banner!=null ?
16. 'url('+this.props.profile.banner +')' : 'none')
17. }
18. return(
19. <aside id="profile" className="twitter-panel">
20. <div className="profile-banner">
21. <Link to={"/" + this.props.profile.userName}
22. className="profile-name" style={bannerStyle}/>
23. </div>
24. <div className="profile-body">
25. <img className="profile-avatar" src={this.props.profile.avatar}/>
26. <Link to={"/" + this.props.profile.userName}
27. className="profile-name">
28. {this.props.profile.name}
29. </Link>
30. <Link to={"/" + this.props.profile.userName}
31. className="profile-username">
32. @{this.props.profile.userName}
33. </Link>
34. </div>
35. <div className="profile-resumen">
36. <div className="container-fluid">
37. <div className="row">
38. <div className="col-xs-3">
39. <Link to={"/"+this.props.profile.userName}>
40. <p className="profile-resumen-title">TWEETS</p>
41. <p className="profile-resumen-value">
42. {this.props.profile.tweetCount}</p>
43. </Link>
44. </div>
45. <div className="col-xs-4">
46. <Link to={"/"+this.props.profile.userName + "/following"}>
47. <p className="profile-resumen-title">SIGUIENDO</p>
48. <p className="profile-resumen-value">
49. {this.props.profile.following}</p>
50. </Link>
51. </div>
52. <div className="col-xs-5">

189 | Página
53. <Link to={"/"+this.props.profile.userName + "/followers"}>
54. <p className="profile-resumen-title">SEGUIDORES</p>
55. <p className="profile-resumen-value">
56. {this.props.profile.followers}</p>
57. </Link>
58. </div>
59. </div>
60. </div>
61. </div>
62. </aside>
63. )
64. }
65. }
66.
67. Profile.propTypes = {
68. profile: PropTypes.object.isRequired
69. }
70.
71. export default Profile;

Este componente podría resultar imponente por su extensión, sin embargo, lo


único que hace es mostrar los datos del usuario como se ve en la siguiente
imagen:

Fig. 99 - Campos del componente Profile.

NOTA: Es esta clase veremos repetidas veces un componente llamado Link, el


cual analizaremos en la siguiente sección, por lo pronto te adelanto que este
componente se convierte en una etiqueta <a> y el atributo to equivaldría al
atributo href.

En la línea 15 se prepara el background del banner, el cual es la imagen de fondo


que vemos detrás de la foto. Una vez preparada la variable bannerStyles, es
establecida en la línea 22 mediante la propiedad style.

De esta misma forma tenemos los campos:

Página | 190
 Avatar: línea 25
 Nombre: líneas 26 a 29,
 Nombre de usuario: líneas 30 a 32
 Contador de Tweets: Líneas 39 a 42
 Siguiendo: Líneas 46 a 49
 Seguidores: Líneas 53 a 56

Al dar click en name, userName, tweets o en banner, el usuario será dirigido al


perfil del usuario, y la url generada será /<username>, por ejemplo, para el usuario
test, la url generada será /test.

En el caso del contador de “Siguiendo” el usuario será dirigido a


/<username>/following y en el caso del contador de “Seguidores” el usuario será
dirigido a /<username>/followers. En la siguiente sección analizaremos como es
que las URL son resueltas para mostrar el componente adecuado según la URL.

Por ahora las url /<username>, /<username>/following y /<username>/followers


no tiene un componente que las respalde, por lo que, si intentamos dar click en
ellas, no funcionará. Más adelante iremos implementando estos componentes.

Finalmente, tendremos que agregar las clases de estilo para que el componente
se vea correctamente, para esto, regresaremos al archivo styles.css y
agregaremos las siguientes clases de estilo al final del archivo.

1. /** PROFILE COMPONENT **/


2.
3. .twitter-panel{
4. background-color: #fff;
5. background-clip: inherit;
6. border: 1px solid #e6ecf0;
7. border-radius: 5px;
8. line-height: inherit;
9. }
10.
11. #suggestedUsers,
12. #profile{
13. max-width: 350px;
14. }
15.
16. #profile{
17. float: right;
18. width: 100%;
19. max-width: 350px;
20. }
21.
22. #profile{
23. overflow: hidden;
24. }
25.
26. #profile .profile-banner{
27. }
28.

191 | Página
29. #profile .profile-banner a{
30. min-height: 100px;
31. background-size: cover;
32. display: block;
33.
34. }
35.
36. #profile .profile-body{
37. position: relative;
38. padding-top: 5px;
39. padding-bottom: 10px;
40. }
41.
42. #profile .profile-body img{
43. position: absolute;
44. display: inline-block;
45. border: 3px solid #fafafa;
46. border-radius: 8px;
47. height: 75px;
48. width: 75px;
49. left: 10px;
50. top: -30px;
51. }
52.
53. #profile .profile-body > a{
54. margin-left: 90px;
55. color: inherit;
56. }
57.
58. .profile-body > a:hover{
59. text-decoration: underline;
60. }
61.
62. #profile .profile-resumen a {
63. color:#657786;
64. }
65.
66. #profile .profile-resumen a:hover{
67. color: #1B95E0;
68. }
69.
70. #profile .profile-resumen a .profile-resumen-title{
71. font-size: 10px;
72. margin: 0px;
73. color:inherit;
74. transition: 0.5s;
75. }
76.
77. #profile .profile-resumen a .profile-resumen-value{
78. color: #1B95E0;
79. font-size: 18px;
80. }
81.
82. #profile .profile-name,
83. #profile .profile-username{
84. display: block;
85. margin: 0px;
86. }
87.
88. #profile .profile-username{
89. color: #66757f;
90. }
91.
92. #profile .profile-name{
93. font-weight: bold;
94. font-size: 18px;

Página | 192
95. }

En este punto no es posible probar los cambios, sino hasta terminar el siguiente
componente.

El componente SuggestedUsers

El componente SuggestedUsers es un Widget que se muestra del lado derecho del


componente TwitterDashboard, el cual muestra tres usuarios recomendados para
seguir. Los usuarios a mostrar son retornados por el API, por lo que el
componente solo se limita a mostrarlos.

Lo primero que deberemos de hacer, será crear un archivo llamado


SuggestedUsers.js en el path /app, el cual se deberá ver de la siguiente manera:

1. import React from 'react'


2. import APIInvoker from './utils/APIInvoker'
3. import { Link } from 'react-router';
4.
5. class SuggestedUser extends React.Component{
6.
7. constructor(){
8. super(...arguments)
9. this.state = {
10. load: false
11. }
12. }
13.
14. componentWillMount(){
15. APIInvoker.invokeGET('/secure/suggestedUsers', response => {
16. this.setState({
17. load: true,
18. users: response.body
19. })
20. },error => {
21. console.log("Error al actualizar el perfil", error);
22. })
23. }
24.
25. render(){
26. return(
27. <aside id="suggestedUsers" className="twitter-panel">
28. <span className="su-title">A quién seguir</span>
29. <If condition={this.state.load} >
30. <For each="user" of={this.state.users}>
31. <div className="sg-item" key={user._id}>
32. <div className="su-avatar">
33. <img src={user.avatar} alt="Juan manuel"/>
34. </div>
35. <div className="sg-body">
36. <div>
37. <a href={"/" + user.userName}>
38. <span className="sg-name">{user.name}</span>
39. <span className="sg-username">@{user.userName}</span>
40. </a>

193 | Página
41. </div>
42. <a href={"/" + user.userName}
43. className="btn btn-primary btn-sm">
44. <i className="fa fa-user-plus" aria-hidden="true"></i>
45. Ver perfil</a>
46. </div>
47. </div>
48. </For>
49. </If>
50. </aside>
51. )
52. }
53. }
54. export default SuggestedUser;

En este componente haremos uso de un nuevo servicio del API el cual se


encuentra en la URL /secure/suggestedUsers. Este servicio nos retornará los
usuarios sugeridos para nuestro usuario, por lo cual, requiere de estar
autenticado para poderlo consumir.

Documentación: Consulta de usuario sugeridos


Este servicio tiene como finalidad retornar 3 usuario
recomendados para seguir (/secure/suggestedUsers)

Este componente implementa la función componentWillMount (línea 14) en el cual


realizará una invocación al servicio /secure/suggestedUsers (línea 15). Si todo
sale bien, el estado será actualizado con los usuarios recomendados en la
propiedad users y la propiedad load en true, indicando que la carga ha
terminado.

Fig. 100 - Campos del componente SuggestedUsers.

Por otra parte, la función render, validará la propiedad load (línea 29) para
determinar si es posible mostrar los usuarios recomendados. Entonces, si la carga
ya termino, se realizará un <For> por cada usuario de la lista (línea 30). En cada
iteración se imprimiera en pantalla, la imagen del avatar (línea 33), nombre y
nombre de usuario (líneas 37 a 40) y un botón nos dirija al perfil del usuario
recomendado (línea 42 a 45).

Página | 194
Para finalizar el componente, tendremos que agregar las clases de estilo.
Regresaremos al archivo styles.css y agregaremos las siguientes clases de estilo
al final del archivo:

1. /** SuggestedUsers Component **/


2. #suggestedUsers{
3. padding: 10px;
4. }
5.
6.
7. #suggestedUsers .su-title{
8. font-size: 18px;
9. color: #66757f;
10. margin: 0px;
11. }
12.
13. #suggestedUsers .sg-item{
14. padding: 5px 0px;
15. }
16.
17. #suggestedUsers .sg-item .su-avatar{
18. position: relative;
19. }
20.
21. #suggestedUsers .sg-item .su-avatar img{
22. display: inline-block;
23. position: absolute;
24. width: 48px;
25. height: 48px;
26. border-radius: 10px;
27. top: 3px;
28. }
29.
30. #suggestedUsers .sg-item .sg-body{
31. position: relative;
32. display: block;
33. margin-left: 55px;
34.
35. }
36.
37. #suggestedUsers .sg-item .sg-body .sg-name{
38. color: #333;
39. font-weight: bold;
40. padding-right: 5px;
41. }
42.
43. #suggestedUsers .sg-item .sg-body .sg-username{
44.
45. }
46.
47. #suggestedUsers .sg-item .sg-body i{
48. color: #fafafa;
49. }

En este punto, ya hemos terminado de desarrollar el componente


TwitterDashboard con todas sus dependencias, por lo que ya será posible
probarlos, pero antes, requerimos hacer un paso adicional. Regresemos al
componente TwitterApp.js y remplazaremos el componente <TwitterContainer>
por <TwitterDashboard> (recordemos importarlo). De tal forma que la función
render debería de quedar de la siguiente manera:

1. render(){

195 | Página
2.
3. return (
4. <div id="mainApp">
5. {/* <Toolbar profile={this.state.profile} selected="home"/> */}
6. <Choose>
7. <When condition={!this.state.load}>
8. <div className="tweet-detail">
9. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
10. </div>
11. </When>
12. <When condition={this.props.children == null
13. && this.state.profile != null}>
14. <TwitterDashboard profile={this.state.profile}/>
15. </When>
16. <Otherwise>
17. {this.props.children}
18. </Otherwise>
19. </Choose>
20. <div id="dialog"/>
21. </div>
22. )
23. }

Observa que propagamos el Perfil del usuario al componente TwitterDashboard


mediante el atributo profile.

Guardamos los cambios, actualizamos y ya deberíamos de ver la aplicación de la


siguiente forma:

Fig. 101 - Componente TwitterDashboard terminado.

El componente Reply

Reply es el componente que dejamos pendiente de la sección de formularios,


pues requería tener la aplicación un poco más desarrollara para que tuviera
sentido. Este componente nos permitirá crear nuevos Tweets, los cuales podrán
tener texto y una imagen. Una de las características de este componente, es que
tiene dos estados posibles, el primero es el inicial o cuando no tiene el foco:

Página | 196
Fig. 102 - Estado inicial del componente Reply.

Cuando el componente no tiene el foco, se ve de una forma compacta, en la cual


solamente muestra una leyenda invitando al usuario a que escriba lo que está
pensando.

Cuando el usuario selecciona el área de texto, este pasa a su segundo estado,


desplegando el botón para cargar una foto y habilita el botón “Twittear” para
enviar el mensaje.

Fig. 103 – Estado con el foco del componente Reply.

Debido a que este componente tiene cierto nivel de complejidad, lo iremos


desarrollando paso a paso, con la finalidad de no poner todo el código de una y
no comprender lo que está pasando. Iniciaremos creando el archivo Reply.js en
el path /app, el cual tendrá en un inicio el siguiente contenido:

1. import React from 'react'


2. import update from 'react-addons-update'
3. import config from '../config.js'
4. import PropTypes from 'prop-types'
5.
6. const uuidV4 = require('uuid/v4');
7.
8. class Reply extends React.Component{
9.
10. constructor(props){
11. super(props)
12. this.state={
13. focus: false,
14. message: '',
15. image: null
16. }
17. }
18.
19. render(){
20. let randomID = uuidV4();
21.
22. return (
23. <section className="reply">
24. <If condition={this.props.profile!=null} >
25. <img src={this.props.profile.avatar} className="reply-avatar" />
26. </If>
27. <div className="reply-body">
28. <textarea
29. ref="reply"

197 | Página
30. name="message"
31. type="text"
32. maxLength = {config.tweets.maxTweetSize}
33. placeholder="¿Qué está pensando?"
34. className={this.state.focus ? 'reply-selected' : ''}
35. value={this.state.message}
36. />
37. <If condition={this.state.image != null} >
38. <div className="image-box"><img src={this.state.image}/></div>
39. </If>
40.
41. </div>
42. <div className={this.state.focus ? 'reply-controls' : 'hidden'}>
43. <label htmlFor={"reply-camara-" + randomID}
44. className={this.state.message.length===0 ?
45. 'btn pull-left disabled' : 'btn pull-left'}>
46. <i className="fa fa-camera fa-2x" aria-hidden="true"></i>
47. </label>
48.
49. <input href="#" className={this.state.message.length===0 ?
50. 'btn pull-left disabled' : 'btn pull-left'}
51. accept=".gif,.jpg,.jpeg,.png"
52. type="file"
53. id={"reply-camara-" + randomID}>
54. </input>
55.
56. <span ref="charCounter" className="char-counter">
57. {config.tweets.maxTweetSize - this.state.message.length }</span>
58.
59. <button className={this.state.message.length===0 ?
60. 'btn btn-primary disabled' : 'btn btn-primary '}
61. >
62. <i className="fa fa-twitch" aria-hidden="true"></i> Twittear
63. </button>
64. </div>
65. </section>
66. )
67. }
68. }
69.
70. Reply.propTypes = {
71. profile: PropTypes.object,
72. operations: PropTypes.object.isRequired
73. }
74.
75. export default Reply;

Antes de continuar, será necesario instalar el módulo UUID que nos ayudará para
la generación de ID dinámicos mediante el comando:
npm install --save uuid

Primero que nada, nos centraremos en el constructor del componente, pues en


él se establece el estado inicial. El estado está compuesto por tres propiedades:

Página | 198
 focus: Indica si el área de texto tiene el foco, con la finalidad de mostrar
el botón para la carga de la imagen y el botón para enviar el Tweet.
 message: esta propiedad está ligada al área de texto, por lo que todo lo
que escribimos en él, actualiza esta propiedad.
 Image: En esta propiedad se guarda la imagen carga.

Fig. 104 - Campos del componente Reply.

Dentro del método render, podríamos dividir la vista en dos partes, lo que se ve
cuándo el componente no está activo (focus =false) y cuando está activo.
Cuando no está activo y solo se requiere mostrar un área de texto, solo se verá
lo que está entre las líneas 24 a 41 y lo que está entre las líneas 42 a 64 solo se
mostrará cuando el área de texto obtenga el foco.

Empecemos analizando la primera sección (no activo), específicamente el


<textarea> de la línea 28. Debido a que todo el componente gira alrededor de
este control, le asignaremos un nombre de referencia (línea 29) en con el cual
podremos referenciarlo de una forma más rápida. Recordemos que podemos
obtener las referencias mediante this.refs.<name>, en este caso sería
this.refs.reply.

También podemos ver el control esta mapeado contra la propiedad message del
estado (línea 35), por lo que todos lo que escribamos actualizará esta propiedad.
También tenemos un placeholder para mostrar un texto por default (línea 32).

El atributo maxLength nos ayuda a controlar el número máximo de caracteres


permitidos, el valor lo obtenemos del archivo config.js que más adelante
analizaremos.

Finalmente, asignamos una clase de estilo dependiendo si el control tiene o no el


foco (línea 34). Solo agregamos el estilo reply-selected cuando el control tiene
el foco, con la finalidad de aumentar su tamaño.

En segundo lugar, tenemos parte del componente que se activa cuando el


textarea tiene el foco, esta sección corresponde a la línea 42 en adelante, como
podrá ver en esta misma línea, ocultamos o mostramos toda la sección mediante
una clase de estilo condicional (className={this.state.focus ? 'reply-
controls' : 'hidden'}).

199 | Página
Una vez esta sección es mostrada, tenemos 3 componentes que van a mostrarse,
el botón para cargar la foto, el contador de caracteres y el botón de Twittear.
Veamos la siguiente imagen:

Fig. 105 - Controles que se activan con el foco.

El contador de caracteres es lo más simple, pues solo es un span (línea 56) el


cual realiza el cálculo
(config.tweets.maxTweetSize - this.state.message.length ), es decir, el
número máximo de caracteres configurado, menos el número de caracteres
capturados.

Por otra parte, el botón es muy simple, pues solo mandará llamar una función en
el evento onClick. En este momento no hemos implementado la función, por lo
que más adelante lo retomaremos.

Finalmente, el botón de la foto es el componente más complejo, pues para darle


una apariencia más estética, hemos tenido que jugar con dos controles, un
<input> y un <label> (líneas 43 a 54), esto debido a que los inputs de tipo file
se ven diferente en cada navegador:

Fig. 106 - Input file en diferentes navegadores.

Para asegurarnos de que el botón siempre se va bien en todos los navegadores,


hemos decido ocular el input mediante CSS y el label funcionara como botón,
para esto, hemos ligado el label y el input mediante el atributo id y htmlFor
respectivamente. Con ayuda del atributo htmlForm estamos asegurando que
cuando el usuario presione el label, este realizara una acción contra el input.

Algo que puede llamar mucho la atención es el ID puesto al input ("reply-camara-


" + randomID). Primero que nada, la variable randomID la creamos con ayuda del

Página | 200
módulo UUID, en segundo lugar y debido a que Reply es un componente pensado
para ser reutilizado, es posible que exista más de una instancia del componente
en una misma vista, es por ello, que requerimos de un identificador que nos
ayude a distinguir a cada uno, y es allí donde entra el módulo UUID, pues nos
permite crear un ID que no se repita entre cada una de las instancias de este
componente.

Configuración

Como pudimos percatarnos, el número de caracteres permitidos en el Tweet y el


contador de caracteres están ligados a la propiedad config.tweets.maxTweetSize,
el cual es un valor configurable que hemos definido en el archivo config.js que
se encuentra en la raíz del proyecto, este archivo ya lo habíamos creado en el
pasado, por lo que solo lo actualizaremos para dejarlo como se ve a continuación:

1. module.exports = {
2. debugMode: true,
3. server: {
4. port: 3000,
5. host: "http://api.localhost"
6. },
7. tweets: {
8. maxTweetSize: 140
9. }
10. }

Esta configuración bien se puede guardada en la base de datos y proporcionarla


por API para hacerla más configurable, pero sería aumentar aún más la
complejidad de este componente. Así que, si te sientes confiado, puedes mejorar
esta característica más adelante.

Registrando los eventos

En este punto, nuestro componente se podrá ver correctamente en pantalla, pero


no tendrá ninguna acción, ya que no hemos registrado ningún evento, por lo que
en esta sección empezaremos a implementarlos.

Los 3 componentes que tendrá eventos son:

201 | Página
 Textarea:
o onFocus: Cuando el control gane el foco, deberá actualizar la
propiedad focus del estado, disparando la actualización de todo el
componente para mostrar el resto de controles.
o onKeyDown: Cuando el usuario presione la tecla escape, el
componente se deberá limpiar y pasar a su estado inicial.
o onBlur: Cuando el control pierda el foco deberá limpiar el componente
dejándolo en su estado inicial, siempre y cuando no allá texto en el
textarea.
o onChange: actualizará la propiedad message del estado, sincronizado el
textarea con el estado.
 Input file:
o onChange: cuando el usuario seleccione una foto el evento onChange
se disparará para cargar la foto y ponerla en la propiedad image del
estado.
 Botón Twittear:
o onClick: cuando el usuario presione el botón twittear, se invocará una
funcione para guardar el Tweet y regresar el componente en su estado
inicial.

Control TextArea

Una vez mencionados los eventos esperados, iniciaremos con los eventos del
textarea, para lo deberemos actualizar para agregar los siguientes eventos:

1. <textarea
2. ref="reply"
3. name="message"
4. type="text"
5. maxLength = {config.tweets.maxTweetSize}
6. placeholder="¿Qué está pensando?"
7. className={this.state.focus ? 'reply-selected' : ''}
8. value={this.state.message}
9. onKeyDown={this.handleKeyDown.bind(this)}
10. onBlur={this.handleMessageFocusLost.bind(this)}
11. onFocus={this.handleMessageFocus.bind(this)}
12. onChange={this.handleChangeMessage.bind(this)}
13. />

El primer evento a analizar será cuando toma el foco, pues es lo que sucede
primero, para ello agregaremos la siguiente función a nuestro componente:

1. handleMessageFocus(e){
2. let newState = update(this.state,{
3. focus: {$set: true}
4. })
5. this.setState(newState)
6. }

Como podemos ver, esta función únicamente cambia la propiedad focus del
estado a true. Este pequeño cambio hace que el componente se actualice y

Página | 202
muestre el botón para cargar una imagen, el contador de caracteres y el botón
para envía el Tweet.

El siguiente evento que se produce es onChange cuando el usuario empieza a


capturar información:

1. handleChangeMessage(e){
2. this.setState(update(this.state,{
3. message: {$set: e.target.value}
4. }))
5. }

Esta función es tan simple como actualizar la propiedad message del estado a
medida que el usuario capturar el mensaje del Tweet.

Lo siguiente que puede pasar es que el usuario decida siempre no mandar el


tweet, por lo que puede cancelar lo capturado y reiniciar el componte a su estado
inicial, para ello utilizamos el evento onKeyDown:

1. handleKeyDown(e){
2. //Scape key
3. if(e.keyCode === 27){
4. this.reset();
5. }
6. }
7.
8. reset(){
9. let newState = update(this.state,{
10. focus: {$set: false},
11. message: {$set: ''},
12. image: {$set:null}
13. })
14. this.setState(newState)
15.
16. this.refs.reply.blur();
17. }

Cuando una tecla es presionada, el evento onKeyDown es disparado, con ello,


validamos que la tecla presionada sea “escape”, es decir el KeyCode 27 (línea 3).
Si la tecla es escape, entonces se manda llamar la función reset, la cual se
encargará de limpiar el estado y volver a la normalidad el componente.

Por otra parte, la función reset además de limpiar el estado, invoca la función
blur del textarea, el cual se accede por medio de la referencia (refs), línea 16.
Con esto, el componente pierde el foco.

Finalmente, el usuario puede optar por seleccionar otra cosa en la pantalla y el


componente perderá el foco, es en ese momento cuando deberá pasar a su
estado inicial (siempre y cuando no tenga texto capturado). El evento onBlur es
ejecutado en este caso:

1. handleMessageFocusLost(e){
2. if(this.state.message.length=== 0){

203 | Página
3. this.reset();
4. }
5. }

Nuevamente, la ecuación se repite, la función reset es invocada cuando se pierde


el foco si el número de caracteres es 0.

Control Input file (Carga de imagen)

Recordemos que el botón realmente es el label asociado al input, sin embargo,


el que realiza la acción es el input. Por lo tanto, deberemos agregar el evento
onChange al input para que se vea de la siguiente manera:

1. <input href="#" className={this.state.message.length===0 ?


2. 'btn pull-left disabled' : 'btn pull-left'}
3. accept=".gif,.jpg,.jpeg,.png"
4. type="file"
5. onChange={this.imageSelect.bind(this)}
6. id={"reply-camara-" + randomID}>
7. </input>

La función imageSelect es la encargada de procesar la carga de la imagen, para


esto, se obtiene la imagen seleccionada (línea 4), luego se valida que la imagen
no sea superior a un Megabyte (línea 5). Para cargar la imagen se implementa
la función onloadend (línea 10) que actualizará el estado una vez se termine la
carga de la imagen, la imagen se guarda en la propiedad image.

1. imageSelect(e){
2. e.preventDefault();
3. let reader = new FileReader();
4. let file = e.target.files[0];
5. if(file.size > 1240000){
6. alert('La imagen supera el máximo de 1MB')
7. return
8. }
9.
10. reader.onloadend = () => {
11. let newState = update(this.state,{
12. image: {$set: reader.result}
13. })
14. this.setState(newState)
15. }
16. reader.readAsDataURL(file)
17. }

Con ayuda de la función readAsDataURL (línea 16) del objeto FileReader iniciamos
la carga del archivo.

Control Button

Página | 204
Finalmente tenemos el botón para enviar el Tweet, para lo cual,
implementaremos el evento onClick:

1. <button className={this.state.message.length===0 ?
2. 'btn btn-primary disabled' : 'btn btn-primary '}
3. onClick={this.newTweet.bind(this)}>
4. <i className="fa fa-twitch" aria-hidden="true"></i> Twittear
5. </button>

Cuando el usuario presione el botón se ejecutará la función newTweet la cual


podemos ver a continuación:

1. newTweet(e){
2. e.preventDefault();
3.
4. let tweet = {
5. _id: uuidV4(),
6. _creator: {
7. _id: this.props.profile._id,
8. name: this.props.profile.name,
9. userName: this.props.profile.userName,
10. avatar: this.props.profile.avatar
11. },
12. date: Date.now,
13. message: this.state.message,
14. image: this.state.image,
15. liked: false,
16. likeCounter: 0
17. }
18.
19. this.props.operations.addNewTweet(tweet)
20. this.reset();
21. }

En esta función realizamos tres acciones, la primera es crear el request para el


servicio de creación del Tweet, para lo cual le mandamos los datos:

 _id: asignamos un nuevo ID con ayuda del módulo UUID.


 _creator: esta sección no es necesario para crear el Tweet en el servicio
REST, si no para que se vea correctamente en pantalla.
 Date: Corresponde a la fecha de creación, es decir, en ese mismo
momento.
 message: Mensaje del Tweet, es decir, lo que el usuario escribió.
 Image: Imagen asociada a Tweet (opcional)
 like: En la creación siempre es false, pues indica si le hemos dado like al
Tweet.
 likeCounter: contador de likes que tiene el tweet, en la creación
siempre es 0.

Una vez que hemos creado el request mandamos llamar a la función addNewTweet
la cual es recibida como un prop dentro del objeto this.props.operations. Esta
función será que se encargue realmente de crear el Tweet, por lo este
componente no se deberá preocupar más por la creación. Vamos a analizar la
función addNewTweet más adelante.

205 | Página
Finalmente, se manda llamar la función reset, la cual limpiará el componente y
lo dejará en su estado inicial.

Funciones como props


Recuerda que mediante las props es posible enviar
cualquier tipo de objetos, incluyendo funciones que
son heredadas de los padres, como es el caso de la
función addNewTweet.

En este punto deberíamos estar muy felices, pues hemos terminado de


implementar el componente Reply, sin embargo, todavía nos falta un paso más,
y es utilizar el componente Reply dentro de la aplicación. Para esto, regresaremos
al componente TweetsContainer y modificaremos la función render para dejarla
como se ve a continuación:

1. render(){
2.
3. let operations = {
4. addNewTweet: this.addNewTweet.bind(this)
5. }
6.
7. return (
8. <main className="twitter-panel">
9. <Choose>
10. <When condition={this.props.onlyUserTweet} >
11. <div className="tweet-container-header">
12. TweetsDD
13. </div>
14. </When>
15. <Otherwise>
16. <Reply profile={this.props.profile} operations={operations}/>
17. </Otherwise>
18. </Choose>
19. <If condition={this.state.tweets != null}>
20. <For each="tweet" of={this.state.tweets}>
21. <Tweet key={tweet._id} tweet={tweet}/>
22. </For>
23. </If>
24. </main>
25. )
26. }

Podemos apreciar dos cambios, por una parte, hemos creado una variable
llamada operations (línea 3) que contiene la referencia a la función addNewTweet,
observemos que hemos referenciado la función con bind(this), ya que, de lo
contrario, no funcionará.

El segundo cambio es la sección del <Choose> el cual analizaremos más adelante,


pero lo que nos incumbe ahora es que en el caso del <Otherwise> se creará un
componente Reply (línea 16). El componente recibe dos props, el perfil del
usuario autenticado y la variable operations, el cual tiene la función addNewTweet.

Finalmente deberemos agregar la famosa función addNewTweet, la cual realiza la


creación del Tweet en el API REST.

Página | 206
1. addNewTweet(newTweet){
2. let oldState = this.state;
3. let newState = update(this.state, {
4. tweets: {$splice: [[0, 0, newTweet]]}
5. })
6.
7. this.setState(newState)
8.
9. //Optimistic Update
10. APIInvoker.invokePOST('/secure/tweet',newTweet, response => {
11. this.setState(update(this.state,{
12. tweets:{
13. 0 : {
14. _id: {$set: response.tweet._id}
15. }
16. }
17. }))
18. },error => {
19. console.log("Error al cargar los Tweets");
20. this.setState(oldState)
21. })
22. }

Analicemos que hace la función; primero que nada, recibe como parámetro el
Tweet a crear en la variable newTweet, lo segundo en hacer es respaldar el estado
actual en la variable oldState (línea 2), luego agregamos el nuevo Tweet a un
nuevo estado que hemos llamado newState, seguido actualizamos el estado del
componente con la variable newState, es decir con el nuevo Tweet que vamos a
crear. Para terminar, llamamos al servicio /secure/tweet del API REST para crear
el Tweet.

Documentación: Creación de un nuevo Tweet


Este servicio nos permite crear un nuevo Tweet
asociado al usuario autenticado (/secure/tweets).

Si el servicio logra guardar correctamente el Tweet, entonces solo actualiza el ID


con el ID real que nos asignó el API REST (línea 14) y no el UUID que habíamos
generado. En caso de error, actualizo el estado del componte con el oldState
(estado viejo).

Te voy a pedir que te tomes un tiempo y veas detenidamente esta función y


pienses si no vez algo extraño. Analiza el orden de los pasos y pregúntate si algo
que no encaje. Si te diste cuenta, Felicidades, pero si no, no te preocupes aquí
lo explicamos.

Seguramente te estarás preguntando, porque actualizo el estado con el nuevo


Tweet (línea 7) antes de haberlo creado con el API (línea 10). Y Luego tengo que
regresar el estado del componente al anterior en caso de falla. Entonces, ¿no
sería más simple, crear el Tweet y luego actualizar el estado? De esta forma de
ahorro tener que respaldar el estado actual y luego restaurarlo en caso de error.

207 | Página
La verdad es que se podría haber hecho así, sin embargo, esta era una excelente
oportunidad para explicar una de las características más potentes React, que es
el Optimistic update.

Nuevo concepto: Optimistic update


Es una característica de React para actualizar la vista
antes del BackEnd, dando al usuario una experiencia
de usuario superior, pues permite darle una respuesta
inmediata.

Como acabamos de ver, el Optimistic Update nos permite actualizar la vista con
el nuevo Tweet sin tener la confirmación del servidor, lo que dará al usuario una
sensación de velocidad extraordinaria. Aunque, por desgracia, sabemos que
cualquier cosa puede fallar en cualquier momento, por lo que puede pasar que el
API nos regrese error o sea inaccesible en ese momento, es entonces cuando es
necesario realizar un Rollback.

En nuestro caso, solo hemos revertido el estado del componente, pero en la


práctica lo mejor sería que adicional al rollback del componente, lancemos alguna
notificación visual al usuario para que esté al tanto de la situación. Esto te lo
puedes llevar de tarea, para seguir mejorando tus habilidades en React.

Para concluir, no olvidemos realizar el import de la función update:

1. import update from 'react-addons-update'

En este punto ya está todo funcionando, pero falta agregar los estilos para que
todo se vea estéticamente bien, por lo que agregamos las siguientes clases de
estilo en el archivo styles.css:

1. /** Reply component **/


2.
3. .reply{
4. padding: 10px;
5. border-top: 1px solid #e6ecf0;
6. background-color: #E8F4FB;
7. }
8.
9. .reply .reply-avatar{
10. display: inline-block;
11. position: absolute;
12. border: 1px solid #333;
13. border-radius: 5px;
14. height: 35px;
15. width: 35px;
16. left: 40px;
17. text-align: center;
18. }
19.
20. .reply .reply-body{
21. margin-left: 80px;
22.
23. }
24.

Página | 208
25. .reply .reply-body textarea{
26. height: 35px;
27. display: block;
28. width: 100%;
29. border: 1px solid #DDE2E6;
30. outline: none;
31. padding-left: 10px;
32. padding-right: 10px;
33. resize: none;
34. border-radius: 5px;
35.
36. }
37.
38. .reply .reply-body .reply-selected{
39. height: 70px;
40. }
41.
42. .reply .reply-body .image-box{
43. display: block;
44. position: relative;
45. color: #F1F1F1;
46. padding: 10px;
47. border-left: 1px solid #DDE2E6;
48. border-right: 1px solid #DDE2E6;
49. border-bottom: 1px solid #DDE2E6;
50. border-radius: 0 0 5px 5px;
51. max-height: 250px;
52. width: 100%;
53. }
54.
55. .tweet-event{
56. position: absolute;
57. display: block;
58. left: 0;
59. right: 0;
60. top: 0;
61. bottom: 0;
62. z-index: 0;
63. }
64.
65. .reply .reply-body .image-box img{
66. display: inline-block;
67. position: relative;
68. max-height: 230px;
69. border-radius: 5px;
70. max-width: 100%;
71. }
72.
73.
74. .reply .reply-controls{
75. padding: 10px 0px 0px 0px;
76. text-align: right;
77. margin-left: 55px;
78. }
79.
80. .reply .reply-controls button{
81. font-size: 18px;
82. font-weight: bold;
83. }
84.
85. .reply .reply-controls button i{
86. color: inherit;
87. font-size: inherit;
88. }
89.
90. .reply .reply-controls .char-counter{

209 | Página
91. margin-right: 10px;
92. color: #333;
93. }
94.
95. input[type="file"]{
96. display: none;
97. }

No suelo detenerme en explicar las clases de estilo, pues no es el foco de este


libro, pero si quisiera que vieras la línea 95 a 97, en donde hacemos que el input
de tipo file no se vea nunca. Es por eso que solo alcanzamos a ver el label
asociado al input.

Si hemos seguido todos los pasos hasta ahora, podrás guardar los cambios y
actualizar el navegador para ver como se ve nuestra aplicación hasta el
momento:

Fig. 107 - Reply componente terminado.

En la imagen ya podemos apreciar cómo hemos podidos capturar un nuevo Tweet


con todo y su imagen, y luego de presionar el botón “Tweet” podemos ver como
este ya se ve en nuestro feed y el componte Reply ha regresado a su estado
inicial.

Página | 210
Fig. 108 - Nuevo Tweet crado.

Este componente ha sido por mucho el más complejo y largo de explicar, pues
tiene varios comportamientos que teníamos que explicar, de lo contrario, podrían
quedar dudas acerca de su funcionamiento.

Solo para recapitular lo que llevamos hasta el momento, te dejo esta imagen,
donde se ve la estructura actual de nuestro proyecto, para que la compares y
veas si todo está bien.

Fig. 109 - Estructura actual del proyecto.

Recuerda que, si has tenido algún problema en hacer funcionar algún


componente hasta este momento, puedes descargar el código fuente del
repositorio y colocarte en el branch de este capítulo, de esta forma tendrá el
código hasta este momento totalmente funcional.

211 | Página
Página | 212
Resumen

Sin duda alguna, este ha sido unos de los capítulos más interesantes hasta el
momento, pues hemos aprendido el ciclo de vida de los componentes, lo cual es
clave para poder desarrollar aplicaciones correctamente.

Por otra parte, hemos hablado del concepto de Optimistic Update, una de las
ventajas que ofrece React para crear aplicaciones con una experiencia de
usuarios sin precedentes, pues crea una sensación de respuesta inmediata por
parte del servidor, incluso si este no responde a la misma velocidad.

También hemos visto con el Componente Reply, que las propiedades (props)
también pueden contener funciones, las cuales son transmitidas por los padres
hacia los hijos, con la intención de delegar la responsabilidad a otro componte.
Sin olvidar que hemos reforzado nuestros conocimientos acerca de la forma de
utilizar los eventos, como lo son el onClick, onBlur, onKeyDown, onChange, onFocus,
etc.

213 | Página
React Routing
Capítulo 9

Hasta el momento hemos aprendido a desarrollar componentes y como estos se


comportan a medida que interactuamos con ellos, pero poco o nada hemos
hablado acerca de cómo un usuario navega por la aplicación, y como es que a
medida que navegamos son otros los componentes que se muestran en pantalla.

Cuando la WEB inicio, las URL no representaban nada más que una simple
dirección a un documento, por lo que la estructura de la misma era casi
irrelevante. Sin embargo, con todas las mejoras que ha tenido la WEB, las URL
ha evolucionado a tal punto que hoy en día, son capases de darnos mucha
información acerca de donde estamos parados dentro de la aplicación. Veamos
el siguiente ejemplo:

http://twitter.com/juan/followers

Solo con ver esta URL puedo determinar que estoy en los seguidores (followers)
del usuario juan.

Podemos ver la ventaja evidente de crear URL amigables, no solo por estética y
que el usuario puede recordar mejor la URL, sino que también los buscadores
como Google toman en cuenta la URL para posicionar mejor nuestras páginas.

Veamos otro ejemplo rápido, imagina que tengo una página que vende cursos
online, por lo que cada curso debe de tener su URL, yo podría tener las siguientes
dos URL:

 http://mysite.com/react
 http://mysite.com/cursos/react

Cuál de las dos siguientes URL crees que es más descriptiva para vender un
curso, la primera URL me deja claro que se trata de React, pero no sé si sea un
artículo, un video, o cualquier otro caso, en cambio, la segunda URL me deja muy
en claro que se trata del curso de React.

Página | 214
Pues bien, para no entrar mucho en detalles, React Router es el módulo de React
que nos permite crear la navegación del usuario mediante las URL, utilizando un
concepto llamado Single Page App, el cual analizaremos a continuación.

Single page App

Para entender cómo funciona la navegación y el direccionamiento de usuario por


medio de las URL, es sumamente importante entender el concepto Single Page
App, ya que de lo contario se nos dificultará mucho entender cómo se comporta
la aplicación y la experiencia que recibirá el usuario durante la navegación.

El concepto single page app, consiste en aplicaciones que se ejecutan sobre una
única página, esto quiere decir que el navegador no requiere hacer una petición
GET al servidor para recuperar la siguiente página y mostrarla, en su lugar, React
crea la vista desde el navegador de forma dinámica, por lo que no es necesario
actualizar el navegador.

Ya habíamos hablado de este concepto en la sección Server Side Apps vs Single


Page Apps. Por si gustas regresar a leerlo, ya que este concepto es fundamental
para entender lo vamos a explicar.

Router & Route

Debido a que React no requiere ir al servidor para obtener una página según la
URL ejecutada, debemos definir las reglas de navegación que tendrá nuestra
página mediante una serie de reglas.

Las reglas de navegación se definen básicamente con los componentes Router y


Route, donde Router es utilizado únicamente para iniciar las reglas y definir el
tipo de historial que utilizaremos para navegar (lo analizaremos más adelante) y
Route nos sirve para definir que componente se tendrá que ver en cada path.
Veamos un ejemplo:

1. <Router history={ browserHistory }>


2. <Route path="/" component={TwitterApp}>
3. <Route path="signup" component={Signup}/>
4. <Route path="login" component={Login}/>
5. <Route path=":user" component={UserPage} >
6. <IndexRoute component={MyTweets} tab="tweets" />
7. <Route path="followers" component={Followers} tab="followers"/>
8. <Route path="following" component={Followings} tab="followings"/>
9. <Route path=":tweet" component={TweetDetail}/>
10. </Route>
11. </Route>
12. </Router>

215 | Página
Quiero que te tomes un momento y analices detenidamente el código anterior,
quisiera que seas capaz de descubrir cómo funciona las reglas con solo ver el
código. Desde luego que no espero que entiendas todo, pero sí que te des una
idea.

Bien, si lo analizaste lo suficiente te darás cuenta que solo tenemos un Router al


inicio de toda la estructura, pues este define el principio de las reglas de ruteo,
también apreciarás que cuenta con un atributo llamado history, el cual lo
pasaremos por alto en este momento y lo retomaremos más adelante.

Lo siguiente que te puedes dar cuenta es que tenemos una serie de Route, los
cuales están anidados de forma jerárquica, es decir, unos dentro de otros. Cada
Route tiene un atributo path, el cual indica ante que URL se activarán, tiene,
además, un atributo llamado componente, el cual indica el componente asociado
a la URL.

La jerarquía en los Route no es solamente estética, si no que el atributo path es


acumulativo con los Route padres. Por ejemplo, veamos el path followers (línea
7), este Route esta entro de Route con path (:user) y este a la vez está dentro
del Route con path /, por lo tanto, esto me indica que para que el Route followers
se active, debería estar en el Path /:user/followers ( :user indica que esta
sección es variable, por ejemplo /oscar/followers o /maria/followers). Entonces,
si toda la cadena del path no coincide, el Route no se active.

Un error muy común cuando trabajamos con React Router es pensar que cuando
un path se activa, el único componente que se debería de ver es el componente
asociado al Route en cuestión, sin embargo, no es así. Cuando un Route se activa
por medio de la URL, lo que pasa es que en realidad el componente activado pasa
como un hijo (this.props.children) al componente padre. Si seguimos con el
ejemplo del path followers, si este se activará se crearía una estructura de
componentes parecida a la siguiente:

1. <TwitterApp>
2. <UserPage>
3. <Followers/>
4. <UserPage>
5. <TwitterApp>

IndexRoute
Existe ocasiones en donde un Router puede tener más de un Router hijo, lo que
indica que el Router padre puede mostrar dentro de sí, más de un componente,

Página | 216
sin embargo, solo un componente puede estar activo a la vez, veamos el
siguiente ejemplo:

1. <Route path=":user" component={UserPage} >


2. <IndexRoute component={MyTweets} tab="tweets" />
3. <Route path="followers" component={Followers} tab="followers"/>
4. <Route path="following" component={Followings} tab="followings"/>
5. <Route path=":tweet" component={TweetDetail}/>
6. </Route>

Claramente, el componente padre es UserPage, pues está en la parte superior de


la jerarquía, y debajo de él, tenemos cuatro posibles componentes que se pueden
visualizar, MyTweets, Followers, Followings y TweetDetail, sin embargo, solo uno
se podrá ver a vez, y todo dependerá del path en el que estén:

Fig. 110 - Elección de componentes según la URL.

Para que cualquiera de estos componentes se active, deberemos estar al menos


en el path /:user para esto, queremos que cuando estemos en /:user se active
un componente por default, para esto utilizamos <IndexRoute> para que cuando
se active el componente UserPage el primero componente que muestre sea
MyTweets, pero al memento de navegar a /:user/followers el componente
MyTweet es eliminado y remplazado por Followers, y lo mismo para Followings y
TweetDetails. Por otra parte, cuando regresemos a la URL /:user el componente
<IndexRoute> se activa de nuevo. En pocas palabras, es como tener un
componente hijo por default.

History
El History es la forma en que React-router interpreta la barra de la navegación
del navegador, con la finalidad de saber que reglas de Route se han activado y

217 | Página
mostrar los componentes adecuados según la URL actual del navegador. Existen
básicamente tres formas distintas de crear un History, los cuales podríamos
llamar “in of the box” (o de caja), sin embargo, es posible crear tu propia
implementación de un objeto History y utilizarlo, aunque en la práctica es muy
raro ver una implementación custom. Los tres tipos más frecuentes son:

browserHistory

Mediante el BrowserHistory es posible crear URL mucho más limpias y claras, con
un formato parecido al siguiente: http://example.com/some/path. para esto, se
apoya en el objeto History del navegador para generar las URL.

Este tipo Historial es el más recomendado para aplicaciones web, pues permite
la creación de URL muy limpias y amigables, sin embargo, tiene un pequeño
inconveniente, y es que es necesaria una pequeña configuración del lado del
servidor. Te explico por qué:

Existen dos formas en las que un usuario puede navegar por una página, la
primera es navegar mediante los enlaces que hay en la misma página, y la
segunda es que el usuario coloque la URL completa en la barra de navegación y
presione “enter”. Pues bien, aunque aparentemente sea lo mismo, la realidad es
que no lo es, y esto se debe a que cuando nosotros presionamos un enlace desde
la página, React podrá capturar ese evento y mapearlo contra un <Route>, sin
embargo, cuando navegamos por medio de la barra de navegación, estamos
forzando al navegador a que busque una ruta en internet y refresque la pantalla.

En este punto te seguirás preguntando, ¿y cuál es el problema? Si pongo la nueva


URL en la barra de navegación, React-router podrá descomponer la URL y
determinar los Route que se deben de activar. Pues la verdad es que sí, pero el
problema no radica en React, si no en el servidor, pues por default hemos
expuesto nuestra aplicación para responder únicamente en la raíz, es decir
http://<host>:<port> y no en las subcarpetas, como podría ser
http://<host>:<port>/user/followings.

Entendamos mejor el problema, te voy a pedir que abras nuevamente la


aplicación tal y como la tenemos hasta este momento y veremos algo parecido a
esto:

Página | 218
Fig. 111 - Estado actual de la aplicación.

Quiero que veas la barra de navegación, estamos en http://localhost:8080, lo


cual es correcto porque estamos en la raíz de la aplicación, ahora bien, te voy a
pedir que presiones el nombre de usuario, el cual está marcado con rojo en la
imagen:

Fig. 112 - URL actualizada.

Quiero que observes nuevamente la barra de navegación, este cambio a


http://localhost:8080/oscar, sin embargo, no hay ningún cambio en pantalla,
lo cual es correcto, porque no hemos definido ningún Route para este path. Ahora
bien, sin hacer nada más, actualiza el navegador, puede ser con el ícono de
actualizar, con F5 o Ctrl +R, da lo mismo:

Fig. 113 - Error de Ruteo.

Como verás, no ha lanzado error. Es decir, la misma URL que pudimos acceder
por un enlacen, no la podemos acceder directamente por la barra de navegación.

219 | Página
Esto se debe a que, por default, nuestro servidor, en este caso la configuración de Webpack,
solo está respondiendo a las peticiones a la raíz del servidor y todos los archivos que estén
en la carpeta public de nuestro proyecto. es decir, hay una regla parecida a la siguiente:

1. app.get('/', function (req, res) {


2. res.sendFile(path.join(__dirname, 'index.html'));
3. });

Este código es solo un ejemplo, y no es parte de nuestro proyecto, solo quiero


que entiendas que pasa en el servidor cuando llega una petición por medio de la
barra del navegador.

Como puedes ver en el código, existe un oyente que escucha las peticiones en el
path /, y ante esa solicitud, regresa el archivo index.html, sin embargo, si llegara
una petición como la anterior /oscar, no sabría cómo resolverla y es allí donde
sale el error. Para solucionar este problema, requerimos crear nuestra propia
implementación de un Servidor y habilitar una función como la siguiente:

1. app.get('/*', function (req, res) {


2. res.sendFile(path.join(__dirname, 'index.html'));
3. });

Como puedes ver, esta nueva función cambia ligeramente, pues escucha en /*,
donde el * indica cualquier cosa después de /. Lo que habilitaría URL’s como
/oscar, /oscar/followers, etc.

Vamos a implementar nuestro propio servidor para corregir este problema


cuando retomemos el proyecto Mini Twiter al final de este capítulo, para no
adelantarnos. Por ahora, solo quería que quedará muy claro lo que estaba
pasando.

hashHistory

El HashHistory permite crear URL muy parecidas a las anteriores, sin embargo,
este antepone el signo de numeral (#) antes cada URL, por ejemplo
http://example.com/#/some/path. Este tipo de URL tiene la ventaja que no requiere
ninguna configuración por parte del servidor. Pues todas las rutas generadas son un ancla al
mismo documento raíz (/).

Este tipo de historiales no debe de utilizarse en ambientes productivos, pues generan URL
nada amigables y que pueden repercutir en el SEO. Además, este tipo de historial solo se
utiliza para realizar cosas muy simples o pruebas que no requieran de un servidor. Debido a

Página | 220
esto, no profundizaré en este tema. En su lugar, solo quería que supieras de su existencia
por si alguna vez te presentarás con ellas.

Te dejo el enlace a la documentación de este tipo de History si quieres profundizar en el


tema.

MemoryHistory

MemoryHistory es un caso especial pues no requiere de la URL del navegador


para gestionar las URL, en su lugar, se apoya de objetos en memoria para
administrar la URL e la aplicación. Este tipo de History’s no son recomendados
para aplicaciones, en su lugar, su uso se centra más en aplicaciones nativas con
React Native. Por esto mismo, tampoco vamos a entrar en detalles acerca de su
implementación. Si guastas profundizar más en el tema, te dejo el enlace a la
documentación oficial.

Nuevo concepto: React Native


React Native es la versión de React diseñada para el
desarrollo de aplicaciones móviles, las cuales son
programadas con React y compiladas a aplicaciones
nativas del sistema operativo, como lo es Android y
IOS

Link

Cuando trabajamos con react-router, hay que tener especial cuidado en la forma
en que creamos los enlaces con la etiqueta <a>, pues esta puede tener un
resultado adverso al esperado. Las etiquetas <a> fuerzan al navegador a realizar
una consulta al servidor y actualizar toda la página, provocando que todos
nuestros componentes se creen de nuevo, pasando por el ciclo de vida de
creación, como lo es el constructor, componentDidMount, render,
componentWillMount, etc. y puede provocar un degrado en el performance y la
experiencia de uso del usuario.

Para evitar esto, react-router ofrece un componente llamado <Link> el cual actual
exactamente como la etiqueta <a>, pero esta tiene la ventaja que no lanza un
request al servidor, si no que ejecuta las reglas de navegación de React-router.
Link da como resultado una etiqueta <a> al memento de renderizar en el
navegador.

Veamos un ejemplo de cómo se utiliza este componente:

1. <Link to={"/followers"}>
2. //Any element

221 | Página
3. </Link>

Link define únicamente el atributo to, el cual deberá definir el path al cual
redireccionaremos al usuario.

NOTA: Cabe mencionar que, no es obligatorio utilizar siempre Link, pues puede
haber ocasiones en las que utilizar un <a> puede ser una buena estrategia, sobre
todo cuando navegamos a una página en la cual es necesario actualizar toda la
información, en estos casos <a> puede ayudarnos, pues creará nuevamente los
componentes entorno a información totalmente nueva.

Props

Al igual que cualquier otro componente, es posible pasar propiedades a los Route,
los cuales son transmitidos a los componentes mediante
this.props.route.{prop-name}, donde {prop-name} es el nombre de la propiedad.
Veamos un ejemplo:

1. <Route path="followers" component={Followers} myProp="any-value"/>

En el caso de este ejemplo, la propiedad myProp se podría recuperar del


componente Followers mediante this.props.route.myProp.

Ahora bien, existe un pequeño problema con las propiedades de los Route, y es
que no es posible pasar como propiedades los objetos creados dentro de los
componentes. Por ejemplo, el componente TwitterApp es el encargado de
autenticar al usuario, y es en este componente donde se crea el objeto del Perfil.
El Perfil es necesario prácticamente para todas los componentes que tenemos en
la aplicación, pues mediante este objeto sabes los datos del usuario autenticado,
pero si, por ejemplo, requerimos pasar este objeto a los Route hijos, no será
propiedades. En su lugar, tendremos que clonar los componentes hijos y definir
las propiedades en ese momento. Puede sonar extraño, pero es la manera que
propone React-router hacerlo en este momento.

React.cloneElement es la función utilizada para clonar un element, la cual recibe


hasta 3 parámetos:

 ReactElement: Representa el elemento a clonar.


 Props: recibe como un objeto todas las propiedades que debe tener el
objeto clonado.
 Children: Si queremos que el nuevo componente clonado tenga hijos,
aquí se deberá de poner.

Página | 222
Por lo general con los dos primeros parámetros es suficiente, como podemos ver
en este ejemplo:

1. React.cloneElement(this.props.children, { profile: this.state.profile })

En este caso, estamos clonando el componente hijo y pasándole como prop el


objeto profile que no fue posible enviar mediante el Route.

Cuando retomemos el proyecto Mini Twitter veremos cómo utilizar correctamente


las props y como clonar elementos para transmitir las props de un padre a un
hijo, por lo que no te preocupes si de momento no te queda muy claro cómo y
cuándo se hace esto.

URL params

Debido a que las aplicaciones cada vez requieren de la generación de URL más
amigables, hemos llegado al punto en que las URL pueden representar
parámetros para las aplicaciones, y de esta forma, saber qué información debe
de mostrar. Por ejemplo, la siguiente URL: http://localhost:8080/oscar o
http://localhost:8080/maria, estas dos URL deberían de llevarnos al perfil de
oscar y maría, y la página debería ser la misma, con la diferencia de la
información que muestra. Esto se hace debido a que oscar y maria, son
parámetros que React-router puede identificar y pasar como prop al componte.
Para lograr esto, utilizamos Route con el siguiente formato:

1. <Route path=":user" component={UserPage} >

Dónde (:user) es el parámetro y React es capaz de identificarlo, también


podemos tener Route como los siguientes:

1. <Route path=":user/tweet/:id" component={UserPage} >

Esta URL la podríamos usar para ver un Tweet especifico de un usuario por medio
del ID del Tweet, por ejemplo, http://localhost:8080/oscar/tweet/110, donde
oscar es el parámetro (:user) y 110 es el parámetro (:id). Los parámetros
pueden ser recuperados mediante this.props.route.{prop-name}.

Hasta este punto hemos analizado lo más importante de react-router, pero sin
duda hay más cosas por explorar, por lo que, si quieres profundizar más en el
tema, te deje la documentación oficial para que lo análisis con más calma:

223 | Página
Mini Twitter (Continuación 3)

Después de una larga teoría acerca del funcionamiento de React Router, ha


llegado el momento de implementarlo en nuestro proyecto. Pero antes de
continuar, te recuerdo que es necesario instalar el módulo react-router e history
mediante npm. En el capítulo pasado lo instalamos para poder avanzar, sin
embargo, no lo habíamos explicado. Por ahora te cuerdo como lo habíamos
instalado, por si te brincaste este paso:

 npm install --save [email protected]


 npm install --save [email protected]

Preparando el servidor para BrowserHistory

Como ya lo habíamos mencionado anteriormente, es necesario preparar nuestro


servidor para atender cualquier URL entrante y no solo responder en la raíz del
dominio.

En realidad, preparar, no sería el termino adecuado, pues lo que vamos hacer


es crear nuestro propio servidor utilizando NodeJS + Express, para tener un
control más fuerte sobre nuestro servidor, que, si utilizáramos solamente
Webpack. Como lo hemos estado haciendo hasta el momento. No quiero entrar
en detalles acerca de NodeJS, Express ni la forma en que los vamos a
implementar, pues ya entraremos en los detalles cuando abordemos de frente
NodeJS y del desarrollo de API’s.

Por lo pronto te adelante que Express es:

Express es una infraestructura de aplicaciones web


Node.js mínima y flexible que proporciona un conjunto
sólido de características para las aplicaciones web y
móviles.

-- http://expressjs.com

En pocas palabras, Express es el framework para desarrollo web por excelencia


en NodeJS, por lo que sin duda, lo utilizaremos de aquí en adelante para construir
nuestro servidor y el API.

Iniciaremos instalando las dependencias necesarias, las cuales son:

Página | 224
 npm install --save [email protected]
 npm install --save [email protected]
 npm install --save [email protected]
 npm install -g nodemon

Los módulos body-parse, express y webpack-dev-middleware los analizaremos


más adelante, pues en este momento no quisiera adelantarme, sin embargo,
quisiera explicar un poco el módulo nodemon. Nodemon es un servidor únicamente
para desarrollo, pues permite tomar los nuevos cambios a medida que
desarrollamos sin la necesidad de reiniciar el server cada vez que guardamos
cambios, algo similar a lo que hace Webpack.

Hasta el momento Nodemon tiene un pequeño detalle que es importante


mencionarlo, cuando nosotros reiniciamos la aplicación o presionamos Ctrl+c
para matar el server, este continúa ejecutándose, por lo que será necesario
matar el proceso en caso de requerir un reinicio total, en Windows lo podrás
hacer desde el administrador de tareas y en Linux puede lanzar un comando Kill.

Explicado esto, procederemos con los cambios en la aplicación. Lo primero será


crear el archivo server.js en la raíz del proyecto (/), es decir a la misma altura
del archivo package.json, el cual deberá tener la siguiente estructura:

1. var express = require('express');


2. var app = express();
3. var bodyParser = require("body-parser")
4. var path = require('path')
5. var webpack = require('webpack')
6. var config = require('./webpack.config')
7. var compiler = webpack(config)
8.
9. app.use('/public',express.static('public'));
10. app.use(bodyParser.urlencoded({extended: false}));
11. app.use(bodyParser.json({limit:'10mb'}));
12.
13. app.use(require('webpack-dev-middleware')(compiler, {
14. noInfo: true,
15. publicPath: config.output.publicPath
16. }));
17.
18. app.get('/*', function (req, res) {
19. res.sendFile(path.join(__dirname, 'index.html'));
20. });
21.
22. app.listen(8080, function () {
23. console.log('Example app listening on port 8080!');
24. });

En el archivo package.json solo cambiaremos la sección “scripts” para que se vea


de la siguiente manera:

1. "scripts": {
2. "start": "nodemon server.js"
3. },

225 | Página
Con esto hemos hecho que, de ahora en adelante, cuando ejecutemos el
comando npm start, este ejecute el archivo server.js con nodemon, en lugar de
crear el servidor por default de Webpack

Con estos pequeños cambios hemos terminado de implementar nuestro server,


al menos por ahora, ya que cuando entremos a la construcción del API deberemos
hacer más cosas.

Para comprobar los cambios tendremos que asegurarnos de apagar el servidor


de Webpack, para liberar el puerto 8080, seguido ejecutamos en la consola el
comando npm start. Deberemos ver lo siguiente en la consola:

Fig. 114 - inicio de nuestro servidor custom con Nodemon.

Para asegurarnos de que todo funciona bien, deberemos de entrar a la aplicación


para ver si se sigue viendo correctamente (http://localhost:8080), adicional,
deberemos entrar a http://localhost:8080/login.

Fig. 115 - Comprobación de nuestro servidor custom.

Si pudiste entrar a la página de login escribiendo la URL completa en la barra de


navegación, quiere decir que hemos hecho todo bien.

Página | 226
Implementando Routing en nuestro proyecto

Recordarás que, en el capítulo anterior modificamos el archivo App.js para


agregar React-router; pero no entramos en detalles, pues no era el momento,
ahora que ya hemos abordado React-router, no deberíamos de tener ningún
problema en entenderlo. Veamos como quedo el archivo:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import TwitterApp from './TwitterApp'
4. import Signup from './Signup'
5. import Login from './Login'
6. import { Router, Route, browserHistory } from "react-router";
7.
8. render((
9. <Router history={ browserHistory }>
10. <Route path="/" component={TwitterApp}>
11. <Route path="signup" component={Signup}/>
12. <Route path="login" component={Login}/>
13. </Route>
14. </Router>
15. ), document.getElementById('root'));

Podemos apreciar claramente que, hasta el momento, tenemos tres reglas de


ruteo, la primera, es para el root (/) de la aplicación, que está ligado al
componente TwitterApp, por otro lado, tenemos como hijos los paths /signup y
/login ligados a los componentes Signup y Login respectivamente. Es posible
comprobar que estas reglas de ruteo funcionan entrando a
http://localhost:8080/login y http://localhost:8080/signup.

El componente Toolbar

El componente Toolbar es la barra que podemos apreciar en la parte superior de


la aplicación, desde ella podemos ver una foto de nuestro perfil, ir a nuestro perfil
o cerrar sesión.

Iniciaremos con la creación del archivo Toolbar.js en el path /app, el cual deberá
tener la siguiente estructura:

1. import React from 'react'


2. import { browserHistory,Link } from 'react-router'
3. import PropTypes from 'prop-types'
4.
5. class Toolbar extends React.Component{
6. constructor(props){
7. super(props)
8. this.state= {}
9. }
10.
11. logout(e){
12. e.preventDefault()
13. window.localStorage.removeItem("token")
14. window.localStorage.removeItem("username")

227 | Página
15. window.location = '/login';
16. }
17.
18. render(){
19.
20. return(
21. <nav className="navbar navbar-default navbar-fixed-top">
22. <span className="visible-xs bs-test">XS</span>
23. <span className="visible-sm bs-test">SM</span>
24. <span className="visible-md bs-test">MD</span>
25. <span className="visible-lg bs-test">LG</span>
26.
27. <div className="container-fluid">
28. <div className="container-fluid">
29. <div className="navbar-header">
30. <a className="navbar-brand" href="#">
31. <i className="fa fa-twitter" aria-hidden="true"></i>
32. </a>
33. <ul id="menu">
34. <li id="tbHome" className="selected">
35. <Link to="/">
36. <p className="menu-item"><i
37. className="fa fa-home menu-item-icon" aria-hidden="true">
38. </i> <span className="hidden-xs hidden-sm">Inicio</span>
39. </p>
40. </Link>
41. </li>
42. </ul>
43. </div>
44. <If condition={this.props.profile != null} >
45. <ul className="nav navbar-nav navbar-right">
46. <li className="dropdown">
47. <a href="#" className="dropdown-toggle"
48. data-toggle="dropdown" role="button"
49. aria-haspopup="true" aria-expanded="false">
50. <img className="navbar-avatar"
51. src={this.props.profile.avatar}
52. alt={this.props.profile.userName}/>
53. </a>
54. <ul className="dropdown-menu">
55. <li><a href={"/"+this.props.profile.userName}>
56. Ver perfil</a></li>
57. <li role="separator" className="divider"></li>
58. <li><a href="#" onClick={this.logout.bind(this)}>
59. Cerrar sesión</a></li>
60. </ul>
61. </li>
62. </ul>
63. </If>
64. </div>
65. </div>
66. </nav>
67. )
68. }
69. }
70.
71. Toolbar.propTypes = {
72. profile: PropTypes.object
73. }
74.
75. export default Toolbar;

Este componte es sin duda uno de los más simples, pues en realidad solo muestra
un botón de inicio (línea 35 a 40) la cual regresará al usuario al inicio de la
aplicación ( / ).

Página | 228
Por otra parte, mostramos una foto del avatar del usuario (línea 50), que al
presionarse arrojará un menú (línea 45 a 62) con dos opciones, la primera nos
lleva al nuestro perfil de usuario (/:user), y la segunda opción es cerrar la sesión.
La opción de cerrar sesión detonará en la ejecución de la función logout.

La función logout remueve de la sesión el token y el username (líneas 13 y 14) y


redirecciona al usuario a la pantalla de login (línea 15).

Para concluir, será necesario regresar al archivo TwitterApp,js y modificar la


función render para agregar el componente Toolbar. Veamos cómo quedaría:

1. render(){
2. return (
3. <div id="mainApp">
4. <Toolbar profile={this.state.profile} />
5. <Choose>
6. <When condition={!this.state.load}>
7. <div className="tweet-detail">
8. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
9. </div>
10. </When>
11. <When condition={this.props.children == null
12. && this.state.profile != null}>
13. <TwitterDashboard profile={this.state.profile}/>
14. </When>
15. <Otherwise>
16. {this.props.children}
17. </Otherwise>
18. </Choose>
19. <div id="dialog"/>
20. </div>
21. )
22. }

No olvidemos agregar el importo correspondiente al archivo Toolbar:

1. import Toolbar from './Toolbar'

Finalmente, quedaría solo agregar las clases de estilo correspondientes a este


componente, para ello agregaremos las siguientes clases de estilo al final del
archivo styles.css:

1. /** Toolbar componente **/


2.
3. .bs-test{
4. display: inline-block;
5. position: absolute;
6. }
7.
8. #dashboard{
9. margin-top: 60px;
10. }
11.
12. .navbar-brand{
13. font-size: 24px;
14. position: absolute;
15. left: 50%;
16. transform: translateX(50%);
17. }

229 | Página
18.
19. .navbar-brand i{
20. color: #1DA1F2;
21. }
22.
23.
24. .navbar{
25. background-color: #FFFFFF;
26. height: 48px;
27. }
28.
29. .navbar-nav>li>a{
30. padding: 0px;
31. }
32.
33. .navbar-avatar{
34. height: 32px;
35. width: 32px;
36. border-radius: 50%;
37. margin: 9px 10px;
38. }
39.
40. #menu{
41. padding: 0px;
42. margin: 0px;
43. position: relative;
44. }
45.
46. #menu li{
47. display: inline-block;
48. color: #666;
49. position: relative;
50. }
51.
52. #menu li::before{
53. content: "";
54. position: absolute;
55. display: block;
56. left: 0px;
57. right: 0px;
58. height: 0px;
59. background-color: #1B95E0;
60. bottom: -2px;
61. z-index: 1000;
62. transition: 0.5s;
63. }
64.
65.
66.
67. #menu li:hover::before{
68. height: 5px;
69. }
70.
71. #menu li:hover{
72. color: #1DA1F2;
73. }
74.
75.
76. #menu li.selected::before{
77. height: 5px!important;
78. }
79.
80. #menu li.selected{
81. color: #1DA1F2;
82. }
83.

Página | 230
84.
85. #menu li a{
86. display: inline-block;
87. padding: 13px 18px 0px 0px;
88. font-size: 12px;
89. font-weight: bold;
90. color: inherit;
91. }
92.
93. #menu li a:focus{
94. text-decoration: none;
95. }
96.
97.
98. #menu li a span{
99. color: inherit;
100. }
101.
102.
103. #menu li .menu-item{
104. color: inherit;
105. padding: 0px 10px;
106. }
107.
108. #menu li .menu-item-icon{
109. font-size: 18px;
110. font-size: 24px;
111. vertical-align: sub;
112. padding-right: 5px;
113. color: inherit;
114. }
115.
116. @media (max-width: 576px) {
117. #menu li .menu-item{
118. padding: 0px 5px;
119. }
120.
121. #menu li a{
122. padding: 13px 0px 0px 0px;
123. margin-right: 5px;
124. }
125. }

Guardamos todos los cambios y actualizamos el navegador para observar los


cambios:

Fig. 116 - Componente Toolbar terminado.

231 | Página
Toques finales al componente Login

Ahora que ya hemos aprendido a utilizar React-router, podemos agregar los


toques finales, con los cuales podremos redirigir al usuario a la pantalla de Signup
para crear una cuenta en caso de que no tenga. Para ello abriremos el archivo
Signup.js y modificaremos la función render para dejarla de la siguiente manera:

1. render(){
2.
3. return(
4. <div id="signup">
5. <div className="container" >
6. <div className="row">
7. <div className="col-xs-12">
8. </div>
9. </div>
10. </div>
11. <div className="signup-form">
12. <form onSubmit={this.login.bind(this)}>
13. <h1>Iniciar sesión en Twitter</h1>
14.
15. <input type="text" value={this.state.username}
16. placeholder="usuario" name="username" id="username"
17. onChange={this.handleInput.bind(this)}/>
18. <label ref="usernameLabel" id="usernameLabel"
19. htmlFor="username"></label>
20.
21. <input type="password" id="passwordLabel"
22. value={this.state.password} placeholder="Contraseña"
23. name="password" onChange={this.handleInput.bind(this)}/>
24. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
25.
26. <button className="btn btn-primary btn-lg " id="submitBtn"
27. onClick={this.login.bind(this)}>Regístrate</button>
28. <label ref="submitBtnLabel" id="submitBtnLabel" htmlFor="submitBtn"
29. className="shake animated hidden "></label>
30. <p className="bg-danger user-test">Crea un usuario o usa el usuario
31. <strong>test/test</strong></p>
32. <p>¿No tienes una cuenta? <Link to="/signup">Registrate</Link> </p>
33. </form>
34. </div>
35. </div>
36. )
37. }

Agregamos solo la línea 32 al archivo que ya tenemos, con esto, habilitaremos


un link para llevar al usuario a Signup.

Página | 232
Fig. 117 - Nuevo link para crear una cuenta.

No olvidemos importar al componente Link:

1. import { Link } from 'react-router'

Toques finales al componente Signup

En el caso del componente Signup pasa algo similar al componente Login, pues
en este tendremos que agregar una ligar para redireccionar al usuario a la
pantalla de login en caso de que ya tenga una cuenta, para esto abriremos el
archivo Signup.js y modificaremos la función render para dejarla de la siguiente
manera:

1. render(){
2.
3. return (
4. <div id="signup">
5. <div className="container" >
6. <div className="row">
7. <div className="col-xs-12">
8.
9. </div>
10. </div>
11. </div>
12. <div className="signup-form">
13. <form onSubmit={this.signup.bind(this)}>
14. <h1>Únite hoy a Twitter</h1>
15. <input type="text" value={this.state.username}
16. placeholder="@usuario" name="username" id="username"
17. onBlur={this.validateUser.bind(this)}

233 | Página
18. onChange={this.handleInput.bind(this)}/>
19. <label ref="usernameLabel" id="usernameLabel"
20. htmlFor="username"></label>
21.
22. <input type="text" value={this.state.name} placeholder="Nombre"
23. name="name" id="name" onChange={this.handleInput.bind(this)}/>
24. <label ref="nameLabel" id="nameLabel" htmlFor="name"></label>
25.
26. <input type="password" id="passwordLabel"
27. value={this.state.password} placeholder="Contraseña"
28. name="password" onChange={this.handleInput.bind(this)}/>
29. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
30.
31. <input id="license" type="checkbox" ref="license"
32. value={this.state.license} name="license"
33. onChange={this.handleInput.bind(this)} />
34. <label htmlFor="license" >Acepto los terminos de licencia</label>
35.
36. <button className="btn btn-primary btn-lg " id="submitBtn"
37. onClick={this.signup.bind(this)}>Regístrate</button>
38. <label ref="submitBtnLabel" id="submitBtnLabel" htmlFor="submitBtn"
39. className="shake animated hidden "></label>
40. <p className="bg-danger user-test">Crea un usuario o usa el usuario
41. <strong>test/test</strong></p>
42. <p>¿Ya tienes cuenta? <Link to="/login">Iniciar sesión</Link> </p>
43. </form>
44. </div>
45. </div>
46. )
47. }

Podemos ver los resultados en la siguiente imagen:

Fig. 118 - Nuevo link para iniciar sesión.

Página | 234
Otros de los cambios está en la función Signup, el cual se encargar de crear la
cuenta. El cambio consiste en utilizar el módulo History para redireccionar al
usuario de forma automática a la pantalla de login, solo cuando el registro del
usuario se realizó exitosamente.

1. signup(e){
2. e.preventDefault()
3.
4. if(!this.state.license){
5. this.refs.submitBtnLabel.innerHTML =
6. 'Acepte los términos de licencia'
7. this.refs.submitBtnLabel.className = 'shake animated'
8. return
9. }else if(!this.state.userOk){
10. this.refs.submitBtnLabel.innerHTML =
11. 'Favor de revisar su nombre de usuario'
12. this.refs.submitBtnLabel.className = ''
13. return
14. }
15.
16. this.refs.submitBtnLabel.innerHTML = ''
17. this.refs.submitBtnLabel.className = ''
18.
19. let request = {
20. "name": this.state.name,
21. "username": this.state.username,
22. "password": this.state.password
23. }
24.
25. APIInvoker.invokePOST('/signup',request, response => {
26. browserHistory.push('/login');
27. alert('Usuario registrado correctamente');
28. },error => {
29. console.log("Error al cargar los Tweets");
30. this.refs.submitBtnLabel.innerHTML = response.error
31. this.refs.submitBtnLabel.className = 'shake animated'
32. })
33. }

La función push de BrowserHistory (línea 26) nos ayuda a redireccionar al usuario


a la URL que se le envíe como parámetro.

No olvidemos importar el componente Link y browserHistory de React-router:

1. import { browserHistory,Link } from 'react-router'

Podemos realizar una prueba ahora mismo, para esto, creemos un nuevo usuario
desde la pantalla de Signup y si el registro sale bien, te debería de llevar
automáticamente a la pantalla de login.

El componente UserPage

235 | Página
El componente UserPage es el que utilizaremos para mostrar el perfil del usuario,
es decir, cuando entremos al path /:user. Este componente es sin duda unos de
los más complejos, pues está compuesto de otros componentes que juntos,
hacen que esta página toma forma. Además de esto, el componente tiene dos
estados, uno es de solo lectura y el otro es de edición.

El modo de solo lectura permitirá a los usuarios ver su perfil o el de otros


usuarios, que está compuesto de: sus tweets, las personas que sigue y que lo
siguen, además de poder visualizar sus datos básicos, como lo son el nombre,
nombre de usuario, avatar, banner y la descripción.

Por otro lado, el modo de edición, solo se podrá habilitar cuando el usuario está
viendo su propio perfil, de esta forma, podrá modificar sus datos básicos, que
son: banner, avatar, nombre y la descripción.

View mode

El modo de solo lectura es el más simple, ya que no solo presentamos la


información del usuario y no hay ningún tipo de interacción, más que el botón de
Seguir. veamos la siguiente imagen:

Fig. 119 - Apariencia final del componente UserPage terminado.

La estructura de componentes se puede apreciar mejor en el siguiente diagrama:

Página | 236
Fig. 120 - Estructura de componentes de UserPage.

Los componentes TweetsContainer y SuggestedUser ya los conocemos, pues ya


los hemos utilizado en el pasado, pero el componente MyTweets es un nuevo
componente que más adelante analizaremos.

Ya con esta breve introducción, podemos comenzar a desarrollarlo, crearemos el


archivo UserPage.js en el path /app, el cual deberá tener el siguiente contenido:

1. import React from 'react'


2. import update from 'react-addons-update'
3. import APIInvoker from './utils/APIInvoker'
4. import { Link } from 'react-router'
5.
6. class UserPage extends React.Component{
7.
8. constructor(props){
9. super(props)
10. this.state = {
11. edit: false,
12. profile:{
13. name: "",
14. description: "",
15. avatar: null,
16. banner: null,
17. userName: ""
18. }
19. }
20. }
21.
22. componentWillMount(){
23. let user = this.props.params.user
24. APIInvoker.invokeGET('/profile/' + user, response => {
25. this.setState({
26. edit:false,
27. profile: response.body
28. });
29. },error => {
30. window.location = '/'
31. })

237 | Página
32. }
33.
34. follow(e){
35. let request = {
36. followingUser: this.props.params.user
37. }
38. APIInvoker.invokePOST('/secure/follow', request, response => {
39. if(response.ok){
40. this.setState(update(this.state,{
41. profile:{
42. follow: {$set: !response.unfollow}
43. }
44. }))
45. }
46. },error => {
47. console.log("Error al actualizar el perfil");
48. })
49. }
50.
51. render(){
52. let profile = this.state.profile
53. let storageUserName = window.localStorage.getItem("username")
54.
55. let bannerStyle = {
56. backgroundImage: 'url(' + (profile.banner) + ')'
57. }
58.
59. return(
60. <div id="user-page" className="app-container">
61. <header className="user-header">
62. <div className="user-banner" style={bannerStyle}>
63. </div>
64. <div className="user-summary">
65. <div className="container-fluid">
66. <div className="row">
67. <div className="hidden-xs col-sm-4 col-md-push-1
68. col-md-3 col-lg-push-1 col-lg-3" >
69. </div>
70. <div className="col-xs-12 col-sm-8 col-md-push-1
71. col-md-7 col-lg-push-1 col-lg-7">
72. <ul className="user-summary-menu">
73. <li className={this.props.route.tab === 'tweets' ?
74. 'selected':''}>
75. <Link to={"/" + profile.userName}>
76. <p className="summary-label">TWEETS</p>
77. <p className="summary-value">{profile.tweetCount}</p>
78. </Link>
79. </li>
80. <li className={this.props.route.tab === 'followings' ?
81. 'selected':''}>
82. <Link to={"/" + profile.userName + "/following" }>
83. <p className="summary-label">SIGUIENDO</p>
84. <p className="summary-value">{profile.following}</p>
85. </Link>
86. </li>
87. <li className={this.props.route.tab === 'followers' ?
88. 'selected':''}>
89. <Link to={"/" + profile.userName + "/followers" }>
90. <p className="summary-label">SEGUIDORES</p>
91. <p className="summary-value">{profile.followers}</p>
92. </Link>
93. </li>
94. </ul>
95.
96. <If condition={profile.follow != null &&
97. profile.userName !== storageUserName} >

Página | 238
98. <button className="btn edit-button"
99. onClick={this.follow.bind(this)} >
100. {profile.follow
101. ? (<span><i className="fa fa-user-times"
102. aria-hidden="true"></i> Siguiendo</span>)
103. : (<span><i className="fa fa-user-plus"
104. aria-hidden="true"></i> Seguir</span>)
105. }
106. </button>
107. </If>
108. </div>
109. </div>
110. </div>
111. </div>
112. </header>
113. <div className="container-fluid">
114. <div className="row">
115. <div className="hidden-xs col-sm-4 col-md-push-1 col-md-3
116. col-lg-push-1 col-lg-3" >
117. <aside id="user-info">
118. <div className="user-avatar">
119. <div className="avatar-box">
120. <img src={profile.avatar} />
121. </div>
122. </div>
123. <div>
124. <p className="user-info-name">{profile.name}</p>
125. <p className="user-info-username">@{profile.userName}</p>
126. <p className="user-info-description">
127. {profile.description}</p>
128. </div>
129. </aside>
130. </div>
131. <div className="col-xs-12 col-sm-8 col-md-7
132. col-md-push-1 col-lg-7">
133. </div>
134. </div>
135. </div>
136. </div>
137. )
138. }
139. }
140. export default UserPage;

Empezaremos con el constructor, pues será lo primero que se ejecutará. En el


podemos observar únicamente la creación del estado inicial, el cual solo tiene
dos partes, la propiedad profile que contiene los datos del usuario a mostrar en
pantalla y la propiedad edit, la cual usaremos para saber si el componente
UserPage está en modo edición o lectura. Al inicio, la propiedad edit es false,
indicando que iniciará en modo solo lectura. Por otra parte, los datos del perfil
inician en blanco y será actualizados más adelante.

Los siguiente en ejecutarse, es la función componentWillMount, en la cual vamos


a cargar los datos del perfil por medio del API. El servicio utilizado para cargar
los datos del perfil es /profile/{user}, donde {user} es el nombre de usuario del
perfil a buscar.

Este servicio tiene doble propósito, pues si lo consumimos por medio del método
GET nos arrojara los datos del perfil, pero si lo ejecutamos por el método PUT,

239 | Página
estaremos haciendo una update. Por ahora estaremos estamos utilizando el
método GET, pues solo requerimos consultar los datos del usuario (línea 24) para
mostrarlos en pantalla. Si la consulta sale correctamente actualizamos el estado
con el nuevo perfil (línea 25); mientras que, si el perfil no se encuentra,
regresamos al usuario a la pantalla de inicio (/) (línea 30).

Documentación: Consulta del perfil de usuario


Mediante este servicio es posible recuperar el perfil de
un usuario determinado (/profile/:username)

Los siguiente que analizaremos es la función render. En ella tenemos las


variables profile y storageUserName las cuales vamos a estar utilizando de aquí
en adelante. La variable profile (línea 52) no es más que un atajo a
this.state.profile, con la intención de no hacer el código más verboso de lo
que ya es. La variable storageUserName (línea 53) contiene el nombre de usuario
autenticado en la aplicación, que obtenemos del LocalStorage.

El resto de la pantalla lo vamos a dividir en 3 secciones para explicarlo mejor, el


banner que es solo la imagen superior, los datos del usuario, que contempla:
avatar, nombre de usuario, nombre y descripción. Y por último la barra de
navegación que se encuentra debajo del banner.

Iniciaremos con el banner, pues es lo primero que aparece al cargar la página.


El banner no es más que un div (línea 62) con la imagen del banner como
background, el fondo es puesto por medio de estilos, que definimos previamente
en variable bannerStyle (línea 55).

Lo segundo por implementar sería la barra de navegación que está justo por
debajo del banner. Esta barra es implementada mediante una lista <ul>, donde
cada ítem será una opción. Las opciones disponibles son:

Página | 240
 Tweet: Lleva al usuario a la URL /{user}, es decir, al perfil del usuario.
Por default, en esta URL debemos ver solo los Tweets del usuario,
podemos como esta implementado en las líneas (75 a 78).
 Siguiendo: Lleva al usuario a la URL /{user}/following, es decir, nos
lleva al perfil del usuario, pero nos muestra a los usuarios que sigue.
Podemos ver como quedo implementado en las líneas (82 a 85).
 Seguidores: lleva al usuario a la URL /{user}/followers, es decir, nos
lleva al perfil del usuario, pero nos muestra las personas que lo siguen.
Podemos ver como quedo implementado en las líneas (89 a 92).

Fig. 121 - Barra de navegación del Perfil.

Fuera de la lista, pero siendo parte de la barra de navegación, tenemos un botón


que nos permite seguir a un usuario. Para que este botón se muestre, el perfil
mostrado debe de ser diferente al usuario que se encuentra autenticado (línea
96). Si el botón se muestra, puede variar su comportamiento, pues si es un
usuario que no seguimos, se mostrará a leyenda “Seguir”, pero si ya lo seguimos,
dirá “Siguiendo”. La propiedad profile.follow, nos indicará de forma booleana
si seguimos al usuario. Este dato es retornado por el API.

Este botón mandará llamar la función follow (línea 34), la cual es la encargada
de comunicarse con el API para seguir o dejar de seguir a un usuario. El servicio
utilizado para seguir o dejar de seguir a un usuario es /secure/follow, la cual
únicamente necesita que le enviemos el usuario al que queremos seguir, el API
determinará si ya seguimos al usuario o no y en base a eso, será la operación a
realizar.

Documentación: Seguir o dejar de seguir a un


usuario
Mediante este servicio es posible seguir o dejar de
seguir a un usuario (/secure/follow)

Para concluir con el modo de solo lectura, solo nos quedaría la parte de los datos
básicos del usuario, compuestos por los campos que podemos ver a continuación:

241 | Página
Fig. 122 - Datos básicos del usuario.

La implementación de esta sección la puedes ver en las líneas (117 a 129),


podrás observar que solo se trata de una <img> para el avatar y una serie de
<span> para los datos del usuario. Creo que a estas alturas no tendría mucho
caso entrar en los detalles, pues es algo muy simple.

Con esta última sección hemos concluido el estado de solo lectura, por lo que el
siguiente paso será agregar la regla /{user} a nuestras reglas de ruteo del
archivo App.js, para lo cual agregaremos las líneas marcadas:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import TwitterApp from './TwitterApp'
4. import Signup from './Signup'
5. import Login from './Login'
6. import UserPage from './UserPage'
7. import { Router, Route, browserHistory } from "react-router"
8.
9. render((
10. <Router history={ browserHistory }>
11. <Route path="/" component={TwitterApp} >
12. <Route path="signup" component={Signup}/>
13. <Route path="login" component={Login}/>
14.
15. <Route path=":user" component={UserPage} >
16.
17. </Route>
18. </Route>
19. </Router>
20. ), document.getElementById('root'));

Hemos agregado el Route con el path (:user) para atender las peticiones /{user}
y lo hemos ligado al componente UserPage, así como hemos agregado el import
correspondiente de este nuevo componente.

Página | 242
En este punto solo restaría agregar las clases de estilo para que los componentes
se vea correctamente, por lo que regresaremos al archivo styles.css y
agregaremos los siguientes estilos al final del archivo:

1. /** UserPage **/


2.
3. .app-container{
4. margin-top: 50px;
5. }
6.
7. #user-page{
8. }
9.
10. #user-page .user-header{
11. margin-bottom: 10px;
12. }
13.
14. #user-page .user-header .user-banner{
15. position: relative;
16. background-color: #fafafa;
17. height: 280px;
18. background-size: cover;
19. background-position: center;
20. }
21.
22. #user-page .user-header .select-banner{
23. position: absolute;
24. left: 0px;
25. right: 0px;
26. bottom: 0px;
27. top: 0px;
28. padding-top: 100px;
29. font-size: 20px;
30. font-weight: bold;
31. }
32.
33. .select-banner:hover{
34. padding-top: 90px;
35. border: 10px solid tomato;
36. }
37.
38. #user-page .user-header .user-summary{
39. border-bottom: 1px solid #dadada;
40. position: relative;
41. }
42.
43. #user-page .user-header .user-summary .user-avatar{
44. position: absolute;
45. display: inline-block;
46. height: 200px;
47. width: 200px;
48. border-radius: 10px;
49. left: 50px;
50. top: -100px;
51. overflow: hidden;
52. border-radius: 12px;
53. box-sizing: content-box;
54. border: 5px solid #fafafa;
55. box-shadow: 0 0 3px #999;
56. }
57.
58. #user-page .user-avatar .avatar-box{
59. position: relative;
60. height: 100%;
61. width: 100%;

243 | Página
62. }
63.
64. #user-page .user-header .user-summary .user-avatar img{
65. height: 100%;
66. width: 100%;
67. }
68.
69. .select-avatar{
70. position: absolute;
71. left: 0px;
72. right: 0px;
73. bottom: 0px;
74. top: 0px;
75. padding-top: 50px;
76. font-size: 20px;
77. font-weight: bold;
78. }
79.
80. .select-avatar:hover{
81. padding-top: 40px;
82. border: 5px solid tomato;
83. }
84.
85. #user-page .user-avatar{
86. display: inline-block;
87. height: 200px;
88. width: 200px;
89. border-radius: 10px;
90. left: 50px;
91. top: -100px;
92. overflow: hidden;
93. border-radius: 12px;
94. box-sizing: content-box;
95. border: 5px solid #fafafa;
96. box-shadow: 0 0 3px #999;
97. }
98.
99. #user-page .user-avatar img{
100. height: 100%;
101. width: 100%;
102. }
103.
104. #user-page .user-header .user-summary .user-summary-menu{
105. margin: 0px;
106. padding: 0px;
107. display: inline-block;
108. }
109.
110. #user-page .user-header .user-summary .user-summary-menu li{
111. text-align: center;
112. padding: 0px;
113. display: inline-block;
114. position: relative;
115.
116. }
117.
118. #user-page .user-header .user-summary .user-summary-menu li a{
119. padding: 15px 15px 0px;
120. display: inline-block;
121. position: relative;
122. }
123.
124. #user-page .user-header .user-summary .user-summary-menu li a::before{
125. content: "";
126. display: block;
127. position: absolute;

Página | 244
128. left: 0px;
129. right: 0px;
130. height: 0px;
131. bottom: 0px;
132. background: #1B95E0;
133. transition: 0.3s;
134.
135. }
136.
137. #user-page .user-header .user-summary .user-summary-menu li a:hover::before{
138. display: block;
139. height: 5px;
140. }
141.
142. #user-page .user-header .user-summary .user-summary-
menu li.selected a::before{
143. display: block;
144. height: 5px;
145. }
146.
147. #user-page .user-header .user-summary .user-summary-menu li .summary-label{
148. font-size: 11px;
149. margin: 0px;
150. }
151.
152. #user-page .user-header .user-summary .user-summary-menu li .summary-value{
153. font-weight: bold;
154. font-size: 18px;
155. color: #666;
156.
157. }
158.
159. #user-page .user-header .user-summary .edit-button{
160. margin: 15px 0px;
161. float: right;
162. }
163.
164. .tweet-footer{
165. padding-top: 10px;
166. }
167.
168. #user-info{
169. top: -180px;
170. position: absolute;
171. display: block;
172. position: relative;
173. padding: 10px;
174. margin-left: 35px;
175. max-width: 350px;
176. width: 280px;
177. max-width: 100%;
178. float: right;
179.
180. }
181.
182. #user-info .user-info-edit{
183. padding: 10px;
184. background-color: #E8F4FB;
185.
186. }
187.
188. #user-info .user-info-edit .user-info-username{
189. color: #1B96E0;
190. margin-top: 10px;
191. }
192.

245 | Página
193. #user-info .user-info-edit textarea,
194. #user-info .user-info-edit input{
195. display: block;
196. width: 100%;
197. border: 1px solid #A3D4F2;
198. outline: none;
199. border-radius: 5px;
200. padding: 5px;
201. }
202.
203. #user-info .user-info-edit textarea{
204. resize: none;
205. height: 220px;
206. }
207.
208. #user-info .user-info-name{
209. font-size: 22px;
210. font-weight: bold;
211. margin: 0px;
212. }
213.
214. #user-info .user-info-username{
215. font-size: 14px;
216. }
217.
218. #user-info .user-info-description{
219.
220. }
221.
222. @media (min-width: 576px) {
223. #user-page .user-avatar{
224. width: 150px;
225. height: 150px;
226. }
227.
228. #user-info{
229. top: -150px;
230. }
231. }
232.
233. @media (min-width: 1200px) {
234. #user-info{
235. top: -180px;
236. }
237.
238. #user-page .user-avatar{
239. width: 200px;
240. height: 200px;
241. }
242. }
243.
244. @media (min-width: 1000px) {
245. #user-page .user-header .user-banner{
246. height: 300px;
247. }
248. }
249.
250. @media (min-width: 1400px) {
251. #user-page .user-header .user-banner{
252. height: 400px;
253. }
254. }
255.
256. @media (min-width: 1800px) {
257. #user-page .user-header .user-banner{
258. height: 500px;

Página | 246
259. }
260. }

Guardamos los cambios y ya deberíamos de poder observar los resultados, para


esto, puedes entrar a la URL de un usuario tengas registrado o el usuario test,
http://localhost:8080/test:

Fig. 123 - UserPage en modo solo lectura.

Una pausa: probablemente en este momento te estés preguntando, como es


que el componente UserPage, Login y Signup se pueden ver dentro del
componente TwitterApp. Si recuerdas, todos estos están definidos como un Route
hijo de TwitterApp del archivo App.js. Bueno, al hace eso, estamos diciendo que
estos componentes estarán en la propiedad this.props.children del componente
TwitterApp:

1. <Choose>
2. <When condition={!this.state.load}>
3. <div className="tweet-detail">
4. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
5. </div>
6. </When>
7. <When condition={this.props.children == null
8. && this.state.profile != null}>
9. <TwitterDashboard profile={this.state.profile}/>
10. </When>
11. <Otherwise>
12. {this.props.children}
13. </Otherwise>
14. </Choose>

247 | Página
Observa como en la función render mostramos los componentes hijos enviados
por React-router (línea 12). Los componentes solo se muestran si la propiedad
this.state.load es true, lo que indica que terminamos de cargar la página. Con
ayuda de un <Choose> podemos determinar si mostrar el componente
<TwitterDashboard> o this.props.children, haciendo dinámica la página.

Edit mode

Cuando un usuario se encuentra en la página de su perfil, tendrá la opción de


editar su perfil, con esto, la vista tendrá que cambiar considerablemente,
remplazando los elementos visuales por controles en los que pueda interactuar
el usuario.

En la imagen podemos observar los cambios al pasar a modo edición. El nombre


cambia de ser un span a un Input text, la descripción cambia a un textarea y
tanto el banner como el avatar se habilitan permitir seleccionar una imagen con
tan solo hacer click sobre ellas.

Fig. 124 - UserPage en modo edición.

Para no hacer tan compleja la explicación, iremos agregando la funcionalidad


paso a paso, al mismo tiempo que la explicamos, para esto, iniciaremos con el
orden natural en que se dan los pasos, es decir, iniciaremos con el botón que
habilita el modo edición.

Agregaremos el bloque <If> justo después de la lista <ul> correspondiente al


menú de navegación (línea 181 aprox). Este If permitirá validar si el usuario del

Página | 248
perfil es el mismo usuario que esta autenticado en la aplicación, si los dos
coinciden, entonces el botón para editar perfil de habilita.

1. ... </ul>
2.
3. <If condition={profile.userName === storageUserName}>
4. <button className="btn btn-primary edit-button"
5. onClick={this.changeToEditMode.bind(this)} >
6. {this.state.edit ? "Guardar" : "Editar perfil"}</button>
7. </If>

Cuando el usuario presione el botón, se llamará a la función changeToEditMode la


cual también deberemos de agregar:

1. changeToEditMode(e){
2. if(this.state.edit){
3. let request = {
4. username: this.state.profile.userName,
5. name: this.state.profile.name,
6. description: this.state.profile.description,
7. avatar: this.state.profile.avatar,
8. banner: this.state.profile.banner
9. }
10.
11. APIInvoker.invokePUT('/secure/profile', request, response => {
12. if(response.ok){
13. this.setState(update(this.state,{
14. edit: {$set: false}
15. }))
16. }
17. },error => {
18. console.log("Error al actualizar el perfil");
19. })
20. }else{
21. let currentState = this.state.profile
22. this.setState(update(this.state,{
23. edit: {$set: true},
24. currentState: {$set: currentState}
25. }))
26. }
27. }

Esta función tiene dos propósitos, por un lado, habilita el modo edición, pero por
el otro lado, guarda los cambios si se ejecuta estando en modo edición. Veamos
cómo funciona.

La función inicia con if, el cual valida la propiedad this.state.edit, que si


recordamos, se inicializa en false desde el constructor. Esto quiere decir que la
primera vez que se ejecute esta función, la condición no se cumplirá y entraremos
en el else, el cual únicamente cambia la propiedad edit a true. Lo que disparará
la actualización del componente. Por otro lado, si la condición se cumple,
entonces actualizaremos el perfil con los nuevos valores capturados mediante el
servicio /secure/profile y concluye actualizando la propiedad edit a false y
regresando el componente a modo solo lectura.

Documentación: Actualización del perfil de


usuario

249 | Página
Mediante este servicio es posible actualizar el perfil del
usuario (/secure/profile)

Una vez que el componente se encuentra en modo edición, se dispara la


actualización de todo el componente, y con ello la función render, en done
tendremos que agregar los cambios marcados:

1. <div className="user-banner" style={bannerStyle}>


2. <If condition={this.state.edit}>
3. <div>
4. <label htmlFor="bannerInput" className="btn select-banner">
5. <i className="fa fa-camera fa-2x" aria-hidden="true"></i>
6. <p>Cambia tu foto de encabezado</p>
7. </label>
8. <input href="#" className="btn"
9. accept=".gif,.jpg,.jpeg,.png"
10. type="file" id="bannerInput"
11. onChange={this.imageSelect.bind(this)} />
12. </div>
13. </If>
14. </div>

Se tendrá que agregar el bloque <If> que comprende de las líneas 2 a 11 dentro
del <div> que contiene el banner. Esto habilitará que el banner permita cambiar
la imagen mediante un click.

Para permitir la carga de la imagen, vamos a utilizar la misma estrategia que


utilizamos para la imagen del componente Reply, es decir, crearemos un Input
file oculto y un label que será lo que el usuario podrá ver. Al momento de realizar
el click, la función imagenSelect se disparará:

1. imageSelect(e){
2. let id = e.target.id
3. e.preventDefault();
4. let reader = new FileReader();
5. let file = e.target.files[0];
6.
7. if(file.size > 1240000){
8. alert('La imagen supera el máximo de 1MB')
9. return
10. }
11.
12. reader.onloadend = () => {
13. if(id == 'bannerInput'){
14. this.setState(update(this.state,{
15. profile: {
16. banner: {$set: reader.result}
17. }
18. }))
19. }else{
20. this.setState(update(this.state,{
21. profile: {
22. avatar: {$set: reader.result}
23. }
24. }))
25. }
26. }
27. reader.readAsDataURL(file)
28. }

Página | 250
Esta función hace exactamente lo mismo que la función de carga de imagen del
componente Reply, por lo que no nos detendremos a explicar, solo basta resumir
que la función carga una imagen seleccionada y la guarda en la propiedad avatar
o banner del estado. Esto quiere decir que la reutilizamos para cargar la foto del
banner y avatar.

NOTA: Podrías crear una función externa que pueda ser reutilizada tanto en
UserPage como en Reply y así evitar repetir código, sin embargo, eso te lo puedes
llevar de tarea si sientes confiado en poder hacer el cambio.

El siguiente cambio que realizaremos es referente a la imagen del avatar, el cual


consiste en habilitar el cambio de imagen mediante un click, para ello deberemos
remplazar el siguiente fragmento de código:

1. <div className="avatar-box">
2. <img src={profile.avatar} />
3. </div>

Y lo remplazaremos por el siguiente:

1. <Choose>
2. <When condition={this.state.edit} >
3. <div className="avatar-box">
4. <img src={profile.avatar} />
5. <label htmlFor="avatarInput"
6. className="btn select-avatar">
7. <i className="fa fa-camera fa-2x"
8. aria-hidden="true"></i>
9. <p>Foto</p>
10. </label>
11. <input href="#" id="avatarInput"
12. className="btn" type="file"
13. accept=".gif,.jpg,.jpeg,.png"
14. onChange={this.imageSelect.bind(this)}
15. />
16. </div>
17. </When>
18. <Otherwise>
19. <div className="avatar-box">
20. <img src={profile.avatar} />
21. </div>
22. </Otherwise>
23. </Choose>

Este cambio hace que existan dos posibles resultados. Si el componente está en
solo lectura, solo se verá el <img> que ya teníamos (línea 18 a 22). Por otro lado,
si estamos en modo edición, se verá la misma imagen, pero con el input file y
el label que ya conocemos. En caso de seleccionar una imagen, vamos a
reutilizar la función imageSelect.

251 | Página
El siguiente cambio es referente a los controles para capturar los datos básicos
del perfil, por lo cual, tendremos que eliminar la siguiente sección:

1. <div>
2. <p className="user-info-name">{profile.name}</p>
3. <p className="user-info-username">@{profile.userName}</p>
4. <p className="user-info-description">
5. {profile.description}</p>
6. </div>

Y será remplaza por esta otro:

1. <Choose>
2. <When condition={this.state.edit} >
3. <div className="user-info-edit">
4. <input maxLength="20" type="text" value={profile.name}
5. onChange={this.handleInput.bind(this)} id="name"/>
6. <p className="user-info-username">@{profile.userName}</p>
7. <textarea maxLength="180" id="description"
8. value={profile.description}
9. onChange={this.handleInput.bind(this)} />
10. </div>
11. </When>
12. <Otherwise>
13. <div>
14. <p className="user-info-name">{profile.name}</p>
15. <p className="user-info-username">@{profile.userName}</p>
16. <p className="user-info-description">
17. {profile.description}</p>
18. </div>
19. </Otherwise>
20. </Choose>

Este cambio añade una condición para agregar un input text y un textarea en
caso de estar en edición y respeta el funcionamiento anterior en caso de estar
en modo solo lectura.

También podemos observar que utilizamos la función handleInput para gestionar


la captura de datos del usuario.

1. handleInput(e){
2. let id = e.target.id
3. this.setState(update(this.state,{
4. profile: {
5. [id]: {$set: e.target.value}
6. }
7. }))
8. }

Esta función no tiene nada de especial, pues solo actualiza el campo name o
userName, según el ID del control que genera los eventos.

En este punto, el usuario podrá decidir guardar los cambios, presionando el botón
“Guardar”, el cual cambio de nombre de “Editar perfil” al momento de entrar en
modo edición.

Por otra parte, el usuario podría decidir cancelar la operación y dejar el perfil tal
y como estaba antes de la edición, para ello, tendremos que agregar un nuevo
botón:

Página | 252
1. <If condition= {this.state.edit}>
2. <button className="btn edit-button" onClick=
3. {this.cancelEditMode.bind(this)} >Cancelar</button>
4. </If>

Este fragmento de código deberá quedar después de terminar el bloque <If> para
agregar el botón Siguiendo/Seguir.

Adicional, se agregará la función cancelEditMode, que se encargar de restaurar


los cambios.

1. cancelEditMode(e){
2. let currentState = this.state.currentState
3. this.setState(update(this.state,{
4. edit: {$set: false},
5. profile: {$set: currentState}
6. }))
7. }

Quiero que pongas atención en la línea 2, pues en ella vemos que obtiene la
propiedad currentState del estado. Esta propiedad se establece en la función
changeToEditMode antes de actualizar la propiedad edit a true, a la cual le asigna
el valor del estado actual. Con esto logramos respaldar en currentState los
valores antes de ser actualizados.

Con estos cambios, nuestro componente está terminado y ya podemos realizar


nuestros primeros cambios, para ello, solo guardamos los cambios, actualiza el
navegador y podremos empezar a editar nuestro perfil.

Fig. 125 - Estado actual del componente UserPage.

253 | Página
El componente MyTweets

El componente MyTeweets es utilizado como contenedor para mostrar el contenido


central de la página de perfil del usuario. Dentro del componente veremos solo
los Tweets del usuario y del lado derecho un listado de los usuarios sugeridos.

Con la finalidad de no reinventar la rueda, vamos a reutilizar los componentes


que ya tenemos, como lo son SuggestedUser y TweetsContainer.

Fig. 126 - MyTweets component.

Iniciemos creando el archivo MyTweets.js en el path /app, el cual deberá quedar


de la siguiente manera:

1. import React from 'react'


2. import TweetsContainer from './TweetsContainer'
3. import SuggestedUser from './SuggestedUser'
4. import PropTypes from 'prop-types'
5.
6. class MyTweets extends React.Component{
7.
8. constructor(props){
9. super(props)
10. }
11.
12. render(){
13. return(
14. <div className="row">
15. <div className="col-xs-12 col-sm-12 col-md-12
16. col-lg-8 no-padding-right">
17. <TweetsContainer profile={this.props.profile} onlyUserTweet={true}/>
18. </div>
19. <div className="hidden-xs hidden-sm hidden-md col-lg-4">
20. <SuggestedUser/>
21. </div>
22. </div>
23. )
24. }
25. }
26.

Página | 254
27. MyTweets.propTypes = {
28. profile: PropTypes.object
29. }
30.
31. export default MyTweets;

Podemos ver rápidamente que este nuevo componente no tiene nada nuevo que
aportar a nuestro conocimiento, pues todo lo que utilizamos aquí ya lo hemos
aprendido, por lo que explicaré rápidamente las partes claves y sin entrar en los
detalles.

Podemos apreciar que la función render solo muestra los componentes


TweetsContainer y SuggestedUser, estos componentes ya los conocemos por lo
que no hay nada que explicar, sin embargo, en la creación del componente
TweetsContainer, podemos apreciar que lo estamos creando con la propiedad
onlyUserTweet en true. Esta propiedad hacer que se solo se vean los tweets del
usuario y no todos los Tweets en orden cronológico, como pasa en la pantalla
principal.

Esto lo logramos por medio de la función loadTweets del componente


TweetsContainer, el cual ejecuta el API con la siguiente URL:

1. let url = '/tweets' + (onlyUserTweet ? "/" + username : "")

Es decir, cuando la propiedad onlyUserTweet es false, la URL generada es /tweets,


pero cuando la propiedad es true, la URL generada es /tweets/{user}. En la
primera URL el API entiendo que se requiere todos los Tweets y en la segunda,
entiende que requieres solo Tweets del usuario en cuestión. También podemos
ver que le envía el objeto profile, que recibirá como prop.

Para que el componente se vea reflejado dentro del componente UserPage, hacen
falta dos cambios, el primero sería en el archivo App.js y el otro en el componente
UserPage. Iniciemos con el archivo App.js:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import TwitterApp from './TwitterApp'
4. import Signup from './Signup'
5. import Login from './Login'
6. import UserPage from './UserPage'
7. import MyTweets from './MyTweets'
8. import { Router, Route, browserHistory, IndexRoute } from 'react-router'
9.
10. render((
11. <Router history={ browserHistory }>
12. <Route path="/" component={TwitterApp} >
13. <Route path="signup" component={Signup}/>
14. <Route path="login" component={Login}/>
15. <Route path=":user" component={UserPage} >
16. <IndexRoute component={MyTweets} tab="tweets" />
17. </Route>
18. </Route>
19. </Router>
20. ), document.getElementById('root'));

255 | Página
Hemos agregado un IndexRoute dentro del Route de UserPage, con la finalidad
que al activarse el componente UserPage, le envíe por default el componente
MyTweet como children.

El otro cambio será en el archivo UserPage, al cual tendremos que agregar la


variable childs, que va dentro del método render y antes del return:

1. render(){
2. let profile = this.state.profile
3. let storageUserName = window.localStorage.getItem("username")
4.
5. let bannerStyle = {
6. backgroundImage: 'url(' + (profile.banner) + ')'
7. }
8.
9. let childs = this.props.children
10. && React.cloneElement(this.props.children, { profile: profile })
11.
12. return(

Probablemente te estés preguntando que estamos haciendo aquí, pues no es una


instrucción muy normal de ver. Al principio de este capítulo dijimos que una de
los detalles de react-router, es que no es posible pasar propiedades de un
componente a otro hijo, pues cuando definimos el Router en el archivo App.js
esta propiedad no existe, es por ello que lo que hacemos es clonar los children
con ayuda de la función cloneElement que nos proporciona el objeto React.

Debido a que MyTweet y los demás elementos hijos que vamos a definir requieren
el objeto perfil, es necesario clonar los children y asignarles la propiedad
profile. La función cloneElement va a clonar el componente this.props.children
y le asignará la propiedad profile (líneas 9 y19).

Una vez clonado los elementos con las propiedades requeridas, vamos a proceder
con agregarlas a la función render, con la finalidad de que estas se vean en
pantalla:

1. </Otherwise>
2. </Choose>
3. </aside>
4. </div>
5. <div className="col-xs-12 col-sm-8 col-md-7
6. col-md-push-1 col-lg-7">
7. {childs}
8. </div>
9. </div>
10. </div>
11. </div>
12. )
13. }
14. }
15. export default UserPage;

La línea marcada deberá ser agrega hasta el final del archivo, justo en el <div>
que se ve en el código.

Página | 256
También deberemos de agregar la siguiente clase de estilo al archivo styles.css:

1. .tweet-container-header{
2. padding: 10px;
3. font-size: 19px;
4. }

Finalmente deberemos de guardar los cambios y actualizar el navegador para ver


los cambios:

Fig. 127 - MyTweets integrado con UserPage.

En este punto hemos terminado el componente MyTweets y la página del perfil del
usuario ya se ve más completa, sin embargo, todavía tenemos un blug que
resolver. El bug se presenta cuando vemos el listado de Tweets mostrado,
primero que nada, revisemos si todos los Tweets que vemos en pantalla son solo
del usuario sobre el que estamos en el perfil (Si no tiene ninguno, creemos unos
cuantos Tweets). Luego, naveguemos al perfil de otro usuario por medio de los
usuarios sugeridos del lado derecho. A medida que cambiemos de perfil, veremos
que los Tweets se mesclan y no se muestra solo los del usuario en cuestión.

Para solucionar este problema, será necesario actualizar el archivo


TweetsContainer para agregar las siguientes líneas:

1. componentDidUpdate(prevProps, prevState) {
2. if(prevProps.profile.userName !== this.props.profile.userName){
3. let username = this.props.profile.userName
4. let onlyUserTweet = this.props.onlyUserTweet
5. this.loadTweets(username, onlyUserTweet)
6. }
7. }
8.

La función componentDidUpdate es importante, ya que como reutilizamos el


componente TweetsContainer, React no lo va a crear cada vez que cambiamos
de perfil, en su lugar, React para mejorar el performance, va actualizar la
componente existente con las nuevas props. Es por ese motivo que validamos si

257 | Página
el userName previo (prevProps) es diferente a al nuevo userName (this.props), si
fueran diferentes, esto indica que debemos actualizar los Tweets al nuevo usuario
y es allí cuando llamamos de nuevo la función loadTweets.

Actualización de los componentes


React a la medida de lo posible, buscará actualizar los
componentes en lugar de crear nuevos, En estos
casos, las funciones del ciclo de vida
componentWillMount y componentDidMount no serán
llamados.

Entender como React actualiza los componentes es de las partes más avanzadas,
pues requiere una comprensión completa del ciclo de vida de los componentes,
por lo que, si no logras entender esta parte, te recomiendo regresar al capítulo
de Ciclo de vida de los componentes para repasar este tema.

Una vez implementados estos cambios, podemos realizar la misma prueba para
validar que los Tweets no se mesclan con los de otros usuarios.

Página | 258
Resumen

En este capítulo hemos aprendido a crear aplicaciones que siga el patrón de


Single Page App, el cual consiste en crear aplicaciones que son creadas en su
mayoría desde FrontEnd y solo requieren del servidor para recuperar los datos.

Por otra parte, nos hemos apoyado de la librería react-router para gestionar la
forma en que React interpreta las URL para determinar los componentes que
debe mostrar, a la vez que hemos visto como crear URL amigables apoyándonos
de URL Params.

Con respecto al proyecto Mini Twitter hemos avanzado bastante, pues hemos
desarrollado uno de los componentes centrales de la aplicación, me refiero a
UserPage, el cual muestra el perfil de los usuarios y permite editar nuestro perfil.

259 | Página
Interfaces interactivas
Capítulo 10

Una de las características que más agradecen los usuarios además de que
funcione bien la aplicación, es que sea ve bien y que tenga algunos efectos o
animaciones que las haga más agradable a la vista, como lo son las animaciones
y las transacciones.

React ofrece el módulo react-transition-group que intenta ser de ayuda para


crear animaciones más fluidas para los componentes. Este módulo no intenta ser
la panacea para resolver todos los problemas de animación, en su lugar es
módulo que se ajusta al ciclo de vida de los componentes, para ayudar a dar
animación cuando un componente es creado, activado y retirado de la vista, por
lo que cualquier otro tipo de animación no está cubierto.

Es muy probable te desanime escuchar la limitación de este módulo, sin embargo,


verás que tiene sus bondades. Pues será de gran utilidad.

Antes de iniciar con la explicación del módulo react-transition-group, es


necesario entender los conceptos CSS Transition y las animaciones mediante CSS
Keyframe.

Qué son las transiciones

Lo primero que debemos de saber, es que las transiciones no son una


característica propia de React o del módulo react-transition-group, sino que
son parte del estándar de CSS y fueron agregadas en la especificación de CSS 3,
por lo que pueden ser utilizadas en desarrollo de cualquier sitio web que uso o
no React.

Las transiciones se logran mediante la definición de la propiedad transition en


una clase de estilo o como un estilo en línea, por ejemplo:

1. .card{
2. background-color: black;
3. transition: background-color 500ms;
4. }
5.
6. .card:hover{
7. background-color: blue;

Página | 260
8.
9. }

La clase de estilo .card, define que el color de fondo del elemento deberá ser
negro (#000) y establece la propiedad transition, la cual recibe dos parámetros:

 Propertie: define la o las propiedades que va a afectar


 Time: Tiempo que dura la transición, podría ser en segundo (s) o
milisegundos (ms).

En este caso, estamos indicando que la transición solo debe de afectar la


propiedad background-color y la animación debe durar 500ms.

Fig. 128 - CSS Transition.

En la imagen podemos ver como se llevaría a cabo una animación del background,
suponiendo que ponemos la clase de estilo .card a un div. Podemos apreciar que
el color cambia de negro a azul y esta transición debería de ocurrir en 500
milisegundos.

Debido a que este no es un libro de CSS, no quisiera salirme del tema central,
que es aprender React, por lo que, si no estás al tanto de CSS transition, te
recomiendo ver la documentación que nos ofrece Mozilla, la cual está muy
completa y en español.

Qué son las animaciones

Las animaciones son más complejas que las transiciones, pero al mismo tiempo
son mucho más potentes, pues permiten crear animaciones más sofisticadas. Las
animaciones se crean mediante la instrucción @keyframe en CSS. Los key frame
permiten definir una animación partiendo de un puto a otro o dividir la animación
en fragmentos, los cuales pueden cambiar el comportamiento de la animación.

1. @keyframes card-animation{
2. from {background-color: black;}
3. to {background-color: blue;}
4. }

261 | Página
El fragmento de CSS anterior crea una animación la cual cambia el background
de negro a azul. Este tipo de animación solo tiene un estado inicial y uno final.

1. @keyframes example {
2. 0% {background-color: black;}
3. 25% {background-color: green;}
4. 50% {background-color: red;}
5. 100% {background-color: blue;}
6. }

También podemos definir animaciones por sección como la anterior, la cual crea
una animación que cambia el background inicialmente a Negro, luego al 25% de
la animación lo cambia Verde, luego a 50% a rojo y al final queda en azul.

Finalmente, para que la animación se active, se deberá definir la propiedad


animation, con el nombre de la animación (keyframe) y la duración de la
animación

1. .card {
2. animation: card-animation 5s
3. }

La clase de estilo anterior provocará que el keyframe crad-animation se ejecute


en 5 segundo, por lo que, suponiendo que se aplica a un div, tendremos una
animación como la siguiente:

Fig. 129 - Keyframe animation.

Como vemos, las 3 primeras partes de la animación se llevan a cabo en un mismo


periodo, pues tiene asignados un 25% de la animación, sin embargo, de la
tercera a la cuarta parte, tenemos un 50%, por lo que la transición de rojo a azul
va a ser más tardada (2.5 segundos).

Nuevamente, las animaciones no son el tema central de este libro, por lo que, si
quieres aprender más acerca de las animaciones, te dejo la documentación en
español de Mozilla.

Página | 262
Introducción a CSSTranstionGroup

Seguramente te estarás preguntando que tiene que ver las transiciones y las
animaciones con CSSTransactionGroup, si al final, estas dos son características de
CSS y no de React. Aunque ese argumento puede ser verdad, la realidad es que
el módulo react-transition-group se apoya de estas características de CSS para
llevar a cabo las animaciones.

1. <CSSTransitionGroup
2. transitionName="card"
3. transitionEnter={true}
4. transitionEnterTimeout={500}
5. transitionAppear={false}
6. transitionAppearTimeout={0}
7. transitionLeave={false}
8. transitionLeaveTimeout={0}>
9.
10. <AnyComponent/>
11.
12. </CSSTransitionGroup>

Para crear una animación, es necesario anidar el componente a animar dentro


del componente CSSTransactionGroup, adicional, es necesario definir la propiedad
transactionName, el nombre que definamos servirá como base para determinar
las clases de estilo que se utilizará según la animación a realizar.

La animación se lleva a cabo en tres pasos:

 Appear: Se activa para todos los hijos de CSSTransactionGroup cuando


este es creado inicialmente.
 Enter: Se activa para todos los hijos de CSSTransactionGroup que se
agregan después de que CSSTransactionGroup fue creado.
 Leave: Se activa para todos los hijos de CSSTransactionGroup que son
eliminados.

Lo siguiente es definir que transiciones se deben de ejecutar y cuáles no, esto se


hace mediante las siguientes propiedades booleanas:

 transitionAppear
 transitionEnter
 transitionLeave

Finalmente, es necesario definir cuánto tiempo durará la animación en


Milisegundos, con las propiedades numéricas:

263 | Página
 transitionAppearTimeout
 transitionEnterTimeout
 transitionLeaveTimeout

Ya con todo este configurado, solo resta tener las clases de estilo
correspondientes, las cuales deben de cumplir una sintaxis muy estricta en su
nombre, de lo contrario las transiciones no se llevarán a cabo.

Lo primero que debemos de saber, es que cada transición (appear, enter y leave)
tiene dos estados (inactivo y activo), en el primero, se deberá definir como se
deberá ver el componente al iniciar la transición y en el segundo, como debería
de terminar el componente una vez que la transición termino.

Dicho esto, entonces se entiende que podría existir 3 paredes de clases de estilo,
las cuales deberá tener el siguiente formato:

 {transitionName}-{appear|enter|leave}
 {transitionName}-{appear|enter|leave}.{transitionName}-
{appear|enter|leave}-active.

El primero representa el estado inactivo y el segundo el activo, pero veamos un


ejemplo para entender cómo quedaría, suponiendo que el valor de
transitionName = card.

1. .card-enter{
2. }
3.
4. .card-enter.card-enter-active{
5. }
6.
7. .card-leave {
8. }
9.
10. .card-leave.card-leave-active {
11. }
12.
13. .card-appear {
14. }
15.
16. .card-appear.card-appear-active {
17. }

Las clases de estilo pertenecientes al estado inactivo se establecerá primero, es


decir, las de la línea 1, 7 y 13. Esto es así para preparar el componente antes de
la animación. Seguido, las animaciones se activarán, mediante las clases de estilo
de las líneas 4, 10 y 16. Esto quiero decir que los elementos quedarán con los
estilos que aquí hemos definido una vez que la animación ha concluido.

Veamos un ejemplo de cómo quedaría una transición para un componente, el


cual cambie de color de negro a azul al crearse por primera vez.

Página | 264
1. .card-appear {
2. background-color: black;
3. }
4.
5. .card-appear.card-appear-active {
6. background-color: blue;
7. transition: background-color 0.5s;
8. }

Notemos que usamos el atributo transition para determinar la propiedad que


debe de animarse y el tiempo de la misma (línea 7).

Cabe resaltar no es requerido definir los estilos para todas las transiciones, si no
solo las que habilitemos desde el componente CSSTransactionGroup.

Mini Twitter (Continuación 4)

Como parte de la continuación del proyecto Mini Twitter, vamos a implementar


lo que hemos aprendido en esta unidad, con la intención de fortalecer los
conocimientos que hemos adquirido

El componente UserCard

Una de las partes que no hemos abordado en la página del perfil del usuario, es
ver sus seguidores y las personas a las que seguimos. Dado que en estas dos
secciones los usuarios se representan de la misma forma. Hemos decidido crear
un componente que represente a un usuario, y ese es UserCard. Veamos como
se ve este componente:

Fig. 130 - UserCard component.

Iniciaremos creando el archivo UserCard.js en el path /app, el cual deberá quedar


de la siguiente manera:

1. import React from 'react'

265 | Página
2. import { Link } from 'react-router'
3. import PropTypes from 'prop-types'
4.
5. class UserCard extends React.Component{
6. constructor(props){
7. super(props)
8. }
9.
10. render(){
11.
12. let user = this.props.user
13. let css = {
14. backgroundImage: 'url(' +user.banner + ')'
15. }
16.
17. return(
18. <article className="user-card" >
19. <header className="user-card-banner" style={css}>
20. <img src={user.avatar} className="user-card-avatar"/>
21. </header>
22. <div className="user-card-body">
23. <Link to={"/" + user.userName} className="user-card-name" >
24. <p>{user.name}</p>
25. </Link>
26. <Link to={"/" + user.userName} className="user-card-username">
27. <p>@{user.userName}</p>
28. </Link>
29. <p className="user-card-description">{user.description}</p>
30. </div>
31. </article>
32. )
33. }
34. }
35.
36. UserCard.propTypes = {
37. user: PropTypes.object.isRequired
38. }
39.
40. export default UserCard;

Con tan solo observar el componente te darás cuenta que no hay nada nuevo
que analizar, pues el componente es muy simple y utiliza cosas que ya sabes
hasta este momento, por lo que solo mencionare las cosas relevantes. Primero
que nada, observemos que el componente recibe como prop el objeto user (línea
37), el cual es obligatorio.

Cabe mencionar que el objeto user es en realidad el mismo objeto profile que
ya hemos venido utilizando a lo largo de todo este proyecto, solo que lo
nombramos de esta forma para darle un nombre más acorde para el componente.

Cuando presionamos algún Link nos deberá llevar al perfil de ese usuario
(/{username}).

Para concluir, solo faltaría agregar las clases de estilo correspondientes al archivo
styles.css:

1. /** UserCard component **/


2. .user-card{
3. margin-bottom: 20px;
4. position: relative;

Página | 266
5. border: 1px solid #E6ECF0;
6. border-radius: 5px;
7. overflow: hidden;
8. }
9.
10. .user-card .user-card-banner{
11. background-position: center;
12. background-size: cover;
13. height: 100px;
14. border-bottom: 1px solid ##E6ECF0;
15. }
16.
17. .user-card .user-card-avatar{
18. width: 70px;
19. height: 70px;
20. border-radius: 5px;
21. position: absolute;
22. top: 70px;
23. left: 10px;
24. }
25.
26.
27. .user-card .user-card-body{
28. padding-top: 100px;
29. background-color: #FFF;
30. padding: 10px 10px 20px;
31.
32. }
33.
34. .user-card .user-card-body .user-card-username p{
35. font-size: 12px;
36. color: #66757f;
37. }
38.
39. .user-card .user-card-body .user-card-name{
40. font-weight: bold;
41. font-size: 18px;
42. display: block;
43. position: relative;
44. margin-top: 40px;
45. }
46.
47. .user-card .user-card-body .user-card-name p {
48. margin: 0px;
49. }
50.
51. .user-card .user-card-body .user-card-description{
52. font-size: 14px;
53. color: #66757f;
54. }
55.
56.
57. .user-card .user-card-body .user-card-username:hover,
58. .user-card .user-card-body .user-card-name:hover{
59. text-decoration: underline;
60. }

El componente Followings

Ya con el objeto UserCard podemos empezar con la creación de la sección de las


personas a las que seguimos (Following), la cual tiene la siguiente apariencia:

267 | Página
Fig. 131 - Followings Component.

Antes de empezar con la implementación, será necesario instalar el módulo


React-transition-group mediante el comando

npm install --save [email protected]

Ya con eso, vamos a crear el archivo Followings.js en el path /app, el cual deberá
tener la siguiente estructura:

1. import React from 'react'


2. import UserCard from './UserCard'
3. import APIInvoker from './utils/APIInvoker'
4. import PropTypes from 'prop-types'
5. import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
6.
7. class Followings extends React.Component{
8.
9. constructor(props){
10. super(props)
11. console.log(props);
12. this.state={
13. users: []
14. }
15. }
16.
17. componentWillMount(){
18. this.findUsers(this.props.profile.userName)
19. }
20.
21. componentWillReceiveProps(props){
22. this.setState({
23. tab: props.route.tab,
24. users: []
25. })
26. this.findUsers(props.profile.userName)
27. }
28.
29. findUsers(username){
30. APIInvoker.invokeGET('/followings/' + username, response => {
31. this.setState({

Página | 268
32. users: response.body
33. })
34. },error => {
35. console.log("Error en la autenticación");
36. })
37. }
38.
39. render(){
40. return(
41. <section>
42. <div className="container-fluid no-padding">
43. <div className="row no-padding">
44. <CSSTransitionGroup
45. transitionName="card"
46. transitionEnter = {true}
47. transitionEnterTimeout={500}
48. transitionAppear={false}
49. transitionAppearTimeout={0}
50. transitionLeave={false}
51. transitionLeaveTimeout={0}>
52. <For each="user" of={ this.state.users }>
53. <div className="col-xs-12 col-sm-6 col-lg-4"
54. key={this.state.tab + "-" + user._id}>
55. <UserCard user={user} />
56. </div>
57. </For>
58. </CSSTransitionGroup>
59. </div>
60. </div>
61. </section>
62. )
63. }
64. }
65.
66. Followings.propTypes = {
67. profile: PropTypes.object
68. }
69.
70. export default Followings;

En este componente si podemos observar algunas cosas nuevas, pues estamos


utilizando el componente CSSTransactionGroup (línea 44) para englobar una serie
de componentes UserCard (línea 55). La idea es que cuando los UserCard
aparezcan en pantalla, se vea como que aparecen gradualmente, en lugar de
aparecer de golpe.

Para ello hemos habilitado únicamente las transiciones de entrada


(transitionEnter=true), y el resto las hemos deshabilitado (false), pues no las
vamos a requerir.

Con respecto a los UserCard, estos deben de recibir como parámetro el usuario
que van a representar, el cual es obtenido como un array en la función
componentWillMount. Para recuperar a las personas que siguen un usuario, se
utiliza el servicio /followings/{user} mediante el método GET.

Documentación: Consulta de personas que


seguimos
El siguiente servicio es utilizado para recuperar el perfil
de los usuario que estamos siguiendo
(/followings/:username)

269 | Página
El siguiente paso es crucial para que la animación se lleve a cabo, en el cual
tendremos agregar las clases de estilo para la animación en el archivo styles.css:

1. .card-enter{
2. opacity: 0;
3. }
4.
5. .card-enter.card-enter-active{
6. opacity: 1;
7. transition: opacity 500ms ease-in;
8. }
9.
10. /*.card-leave {
11. opacity: 0;
12. }
13.
14. .card-leave.card-leave-active {
15. opacity: 1;
16. transition: opacity 500ms ease-in;
17. }
18.
19. .card-appear {
20. opacity: 0;
21. }
22.
23. .card-appear.card-appear-active {
24. opacity: 1;
25. transition: opacity 500ms ease-in;
26. }*/

Para este ejemplo solo requerimos las dos primeras clases de estilo (líneas 1 a
8), sin embargo, he dejado comentadas las clases necesarias para leave y appear
en caso de que quieres realizar algunos experimentos.

Te explico cómo funciona, cuando el UserCard entre en escena, tomara el estilo


de la clase (.card-enter), lo que implica que se le establezca una opacidad de 0
(Totalmente transparente). Seguido de eso, la animación iniciará y le establecerá
los estilos de (.card-enter.card-enter-active) lo que implica establecer una
opacidad de 1 (Totalmente visible), pero adicional, se establece transition para
crear una transición de 500 milisegundos. Esto quiere decir que pasará de ser
totalmente transparente a totalmente visible en 0.5 segundos.

Finalmente, solo restaría agregar el componente Followings a nuestras reglas de


Router en el archivo App.js:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import TwitterApp from './TwitterApp'
4. import Signup from './Signup'
5. import Login from './Login'
6. import UserPage from './UserPage'
7. import MyTweets from './MyTweets'
8. import Followings from './Followings'
9. import { Router, Route, browserHistory, IndexRoute } from "react-router"
10.

Página | 270
11. render((
12. <Router history={ browserHistory }>
13. <Route path="/" component={TwitterApp} >
14. <Route path="signup" component={Signup}/>
15. <Route path="login" component={Login}/>
16.
17. <Route path=":user" component={UserPage} >
18. <IndexRoute component={MyTweets} tab="tweets" />
19. <Route path="following" component={Followings} tab="followings"/>
20. </Route>
21. </Route>
22. </Router>
23. ), document.getElementById('root'));

Tan solo es necesario agregar el Route de la línea 19, adicional, agregamos un


prop llamado tab, el cual le indicará a la aplicación que tab está seleccionado.

Todavía no probaremos los cambios, sino hasta que tengamos el componente


Followers que sigue a continuación.

El componente Followers

Este componente es exactamente igual al anterior, sin embargo, este consume


un servicio diferente para recuperar los seguidores.

Crearemos el archivo Followers.js en el path /app, el cual deberá tener el


siguiente contenido:

1. import React from 'react'


2. import UserCard from './UserCard'
3. import APIInvoker from './utils/APIInvoker'
4. import PropTypes from 'prop-types'
5. import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
6.
7. class Followers extends React.Component{
8.
9. constructor(props){
10. super(props)
11. this.state={
12. users: []
13. }
14. }
15.
16. componentWillMount(){
17. this.findUsers(this.props.profile.userName)
18. }
19.
20. componentWillReceiveProps(props){
21. this.setState({
22. tab: props.route.tab,
23. users: []
24. })
25. this.findUsers(props.profile.userName)
26. }
27.
28. findUsers(username){

271 | Página
29. APIInvoker.invokeGET('/followers/' + username, response => {
30. this.setState({
31. users: response.body
32. })
33. },error => {
34. console.log("Error en la autenticación");
35. })
36.
37. }
38.
39. render(){
40. return(
41. <section>
42. <div className="container-fluid no-padding">
43. <div className="row no-padding">
44. <CSSTransitionGroup
45. transitionName="card"
46. transitionEnter = {true}
47. transitionEnterTimeout={500}
48. transitionAppear={false}
49. transitionAppearTimeout={0}
50. transitionLeave={false}
51. transitionLeaveTimeout={0}>
52. <For each="user" of={ this.state.users }>
53. <div className="col-xs-12 col-sm-6 col-lg-4"
54. key={this.state.tab + "-" + user._id}>
55. <UserCard user={user} />
56. </div>
57. </For>
58. </CSSTransitionGroup>
59. </div>
60. </div>
61. </section>
62. )
63. }
64. }
65.
66. Followers.propTypes = {
67. profile: PropTypes.object
68. }
69.
70. export default Followers;

Nos ahorraremos las explicaciones de este componente, pues es exactamente


igual al anterior, con la única diferencia que este consume el servicio de
seguidores (followers)

Documentación: Consulta de seguidores


El servicio de seguidores permite consultar el perfil de
todos los usuarios que nos siguiente
(/followeres/:username)

También requerirá de actualizar el archivo App.js para agregar un Route a este


componente:

Página | 272
1. import React from 'react'
2. import { render } from 'react-dom'
3. import TwitterApp from './TwitterApp'
4. import Signup from './Signup'
5. import Login from './Login'
6. import UserPage from './UserPage'
7. import MyTweets from './MyTweets'
8. import Followings from './Followings'
9. import Followers from './Followers'
10. import { Router, Route, browserHistory, IndexRoute } from "react-router"
11.
12. render((
13. <Router history={ browserHistory }>
14. <Route path="/" component={TwitterApp} >
15. <Route path="signup" component={Signup}/>
16. <Route path="login" component={Login}/>
17.
18. <Route path=":user" component={UserPage} >
19. <IndexRoute component={MyTweets} tab="tweets" />
20. <Route path="followers" component={Followers} tab="followers"/>
21. <Route path="following" component={Followings} tab="followings"/>
22. </Route>
23. </Route>
24. </Router>
25. ), document.getElementById('root'));

Finalmente, solo quedaría guardar los cambios y ver los resultados. Mediante una
imagen es complicado ver una animación, por lo que te pido que te dirijas al
perfil del usuario que gustes y cambies entre las pestañas de Seguidores y
Siguiendo para comprobar los resultados.

273 | Página
Resumen

Sé que este capítulo no ha llegado a ser tan impresionante como pensabas, pues
a lo mejor esperabas librerías de interacción más sofisticadas, pero la realidad es
que con CSS es posible hacer que las aplicaciones luzcan realmente bien, incluso
con animaciones. Sin embargo, buscare que en próximas ediciones esta sección
se pueda ampliar con más cosas interesantes y ese es el motivo por el cual he
decidido crear esta pequeña sección por separado.

En este capítulo hemos hablado acerca de las animaciones y transiciones de CSS,


que si bien, no es el tema central de este libro, sí que te hemos dado las bases
para que tú mismo puedas profundizar en el tema.

Hemos hablado del módulo react-transition-group que nos ayuda a crear


animaciones cuando un componente es creado, agregado o eliminado, cosa que
es muy difícil mediante CSS.

Página | 274
Componentes modales
Capítulo 11

Existen ocasiones en las que requerimos crear componentes que se vean en


forma modal, es decir, que se vea sobre toda la aplicación y que impiden que el
usuario realice otra acción hasta que atienda lo que el componente modal está
solicitando.

Este tipo de comportamiento suelen ser lo más complicados para el programador


novato de React, no porque sea complicado en sí, si no porque no saben cómo
hacerlo, y recupera a diversos ejemplos de internet que nos confunde incluso
más de lo que ya estábamos.

En la actualidad existen diversos módulos para crear componentes modales o los


famosos alerts con estilos gráficos más modernos. Pero en esta ocasión quiero
explicarte cómo hacerlo por ti mismo de una forma muy simple

Algunas librerías existentes

En la actualidad existe una gran cantidad de librerías listas para ser usadas, las
cuales solo requieren de su instalación con npm y posteriormente su
implementación. Algunas librerías son fáciles de usar otras más complejas, pero
más configurables.

Debido a que estas librerías no son el punto focal de este libro y que evolucionan
constantemente, es complicado enseñarte a usar cada una de ellas, por lo que,
en su lugar, te vamos a nombrar algunas con su respectiva documentación para
que seas tú mismo quien determine que librerías se adapta mejor a tus
necesidades. La lista es:

 react-modal (https://github.com/reactjs/react-modal)
 react-modal-dialog (https://www.npmjs.com/package/react-modal-
dialog)
 react-modal-bootstrap: (https://www.npmjs.com/package/react-modal-
bootstrap)

275 | Página
Estas son las 3 librerías más populares para la implementación de componentes
modales, pero sin duda hay muchísimos más. Te invito a que los revises y veas
si alguno te llama la atención para usarlas como parte de tu pila de librerías.

Implementando modal de forma nativa.

Adicional a la creación de componentes modales por medio de librerías, es posible


crear tus propios componentes modales sin necesidad de usar ninguna librería
existente, con la ventaja de que la puedes crear a tu gusto y que se adapte mejor
a la necesidad de tu proyecto.

En este punto podrías pensar, si ya existen librerías para hacerlo, para que
reinventar la rueda implementando mi propio sistema de componentes modales,
y puede que tengas razón, incluso, te alentamos a elegir una librería existente,
si esta cumple a la perfección lo que requieres, sin embargo, crear tus propias
pantallas modales te da más control sobre los compontes, además que, es tan
simple que realmente no requiera mucho esfuerzo.

Como te comenté hace un momento, si decides utilizar una librería está bien, sin
embargo, como este es un libro para aprender React, queremos enseñarte a
hacerlo por ti mismo, para que de esta forma puedes dominar mejor la
tecnología. Como siempre, la decisión es solo tuya.

Antes de comenzar a crear componentes modales, es necesario conocer la teoría,


la cual se base en su totalidad en CSS y estilos, por lo que, si no te sientes
confiado, no te preocupes, aquí si vamos a detendremos a explicar lo necesario
de CSS para implementarlo.

Primer principio, un componente modal debe verse sobre todos los


componentes de la pantalla. Si solo agregáramos un componen dentro de otro,
este se vería como un hijo de este otro componte:

Fig. 132 - Agregar un componente dentro de otro.

Página | 276
Para lograr que mi componente se vea por encima, incluso de su componente
padre, es necesario encapsular nuestro componente dentro de elemento con
posición fija en la pantalla (position: fixed), el cual, abarcara toda el área visible
con ayuda de las propiedades (top, right, left, bottom) en cero:

Fig. 133 - Fixed element

Para asegurarnos que el elemento fixed se encuentre por encima del resto de
componentes, será necesario utiliza la propiedad z-index en un número muy
elevado. Esta propiedad moverá nuestro elemento en el eje Z, es decir lo traerá
hasta el frente.

Adicional, agregamos las propiedades height y overflow en auto, con la finalidad


de que, si nuestro componente es más grande que la pantalla, se pueda realizar
un Scroll.

Y eso es todo, lo que seguirá es solo agregar nuestro componente dentro del
elemento fixed y listo. Solo una cosa más, te sugiero que el contendedor de tu
componente (primer elemento) utilice la propiedad margin: auto, con la finalidad
de realizar un centrado horizontal perfecto en la pantalla:

Si estás pensando que esto es muy complicado y que mejor optarás por una
librería, espera a ver como lo implementamos en el proyecto Mini Twitter, para
que veas lo fácil que es.

Mini Twitter (Continuación 5)

Ya con la teoría fundamental para crear nuestras propias componentes modales,


no hay nada que nos detenga para empezar a implementarlo en nuestro
proyecto.

El componente TweetReply

TweetReply es un componente modal que se muestra cuando el usuario presiona


el botón de contestar a un Tweet, el cual es una pequeña fecha azul en la parte

277 | Página
inferior. La idea de este componente, es darle la oportunidad a un usuario de
contestar o agregar un comentario a un Tweet existente. Veamos cómo se ve:

Fig. 134 - Componente TweetReply en forma modal.

Observa como nuestro componente se observa centrado y justo en sima de toda


la página. Además, obscurecemos el fondo para que el usuario preste mucha más
atención al componente y no pueda realizar ninguna tara en la aplicación hasta
que cierre el componente modal.

Es importante observar los 3 niveles que tiene la aplicación, en el primer nivel se


encuentra toda la aplicación, en el segundo nivel tenemos el elemento fixed que
hace que la pantalla se vea oscurecida, y en el tercer nivel, tenemos nuestro
componente TweetReply.

Para iniciar, deberemos crear el archivo TwitterReply.js en el path /app, el cual


deberá tener la siguiente estructura:

1. import React from 'react'


2. import Reply from './Reply'
3. import Tweet from './Tweet'
4. import update from 'react-addons-update'
5. import APIInvoker from './utils/APIInvoker'
6. import PropTypes from 'prop-types'
7.
8. class TweetReply extends React.Component{
9.
10. constructor(props){
11. super(props)
12. }
13.
14. handleClose(){
15. $( "html" ).removeClass( "modal-mode");
16. $( "#dialog" ).html( "");
17. }
18.
19. addNewTweet(newTweet){
20. let request = {
21. tweetParent: this.props.tweet._id,
22. message: newTweet.message,
23. image: newTweet.image

Página | 278
24. }
25.
26. APIInvoker.invokePOST('/secure/tweet', request, response => {
27. this.handleClose()
28. },error => {
29. console.log("Error al cargar los Tweets");
30. })
31. }
32.
33. render(){
34.
35. let operations = {
36. addNewTweet: this.addNewTweet.bind(this)
37. }
38.
39. return(
40. <div className="fullscreen">
41. <div className="tweet-detail">
42. <i className="fa fa-times fa-2x tweet-close" aria-hidden="true"
43. onClick={this.handleClose.bind(this)}/>
44. <Tweet tweet={this.props.tweet} detail={true} />
45. <div className="tweet-details-reply">
46. <Reply profile={this.props.profile} operations={operations}/>
47. </div>
48. </div>
49. </div>
50. )
51. }
52. }
53.
54. TweetReply.propTypes = {
55. tweet: PropTypes.object.isRequired,
56. profile: PropTypes.object.isRequired,
57. }
58.
59. export default TweetReply;

Dado que este componente requiere mostrar un Tweet en específico y mostrar


nuestro avatar en el componente Reply, es necesario enviarle como prop dos
objetos obligatorios (líneas 55 y 56), el objeto tweet a mostrar y el profile del
usuario autenticado.

Ahora bien, analicemos como se hace la magina, deberás notar que en la línea
40 hay un <div> con la clase fullscreen. Esta div es el contendor fixed, ya que
con ayuda de la clase fullscreen hacemos que tome toda la pantalla. Analicemos
la clase fullcreen:

1. .fullscreen{
2. position: fixed;
3. background-color: rgba(0,0,0,0.5);
4. top: 0;
5. right: 0;
6. left: 0;
7. height: auto;
8. z-index: 999999;
9. overflow: auto;
10. bottom: 0;
11. }

Veamos que esta clase tiene lo que ya habíamos mencionado al principio, tiene
un display: fixed, para asegurar que este elemento se posicione con respecto
a la ventana del navegador, nos aseguramos que tomo toda la pantalla mediante

279 | Página
top:0, right:0, left:0 y bottom: 0. Establecemos la altura (height) en auto,
para que se calcule el tamaña basado en el contenido, overflow en auto, para
asegurarnos de que el componente tenga scroll en caso de desbordamiento, z-
index en 999999 para que sea el elemento más arriba en toda la aplicación. Y
finalmente, un background negro con un alfa de 0.5, lo que indica que tendrá una
transparencia.

Lo que tenemos dentro del div con fullscreen es lo que queremos que se vea
en forma modal. En este caso, el div hijo (línea 41) será el componente que
vemos en pantalla. Como nota. La clase fullscreen debe de tener un
posicionamiento relativo (position: relative) de lo contrario no se verá bien:

1. .fullscreen .tweet-detail{
2. max-width: 700px;
3. overflow: hidden;
4. border-radius: 5px;
5. margin: auto;
6. margin-top: 50px;
7. margin-bottom: 100px;
8. position: relative;
9. width: 60%;
10. background-color: #fff;
11. }

Como puedes observar, solo lo marcado es lo estrictamente necesario para el


componente modal, lo demás es solamente para que se vea mejor.

Una vez explicado cómo funciona la parte modal, solo nos resta analizar el
funcionamiento del componente.

Observa que estamos reutilizando los componentes Tweet y Reply. Por una parte,
el componente recibe el objeto tweet para mostrarlo y en esta ocasión recibe una
nueva propiedad llamada detail, esta propiedad le indicará al componente Tweet
como debe comportarse y verse cuando está siendo mostrado de forma modal.
Vamos a analizar esto un poco más adelante.

Por la otra parte, tenemos el componente Reply, el cual se comporta exactamente


igual, por lo que no tenemos nada que agregar, salvo dos cosa, recordarás que
el componente Reply recibe como propiedad la función para crear el nuevo Tweet,
en este caso, le mandamos la función newTweet (líneas 35 y 44), el cual es
prácticamente igual a la función que definimos en el componente TweetContainer
para la creación de un nuevo Tweet, sin embargo, este agrega un parámetro
adicional, el tweetParent, el cual le indica al API, si el Tweet que vamos a crear
es hijo de otro, con esto, se sabe que es una réplica a un Tweet existente. La
segunda diferencia es que cierra la pantalla modal (línea 27) una vez que hemos
respondido al Tweet con la ayuda de la función handleClose (la analizaremos más
adelante).

Para cerrar este componente, solo nos resta agregar las clases de estilo en el
archivo styles.css:

Página | 280
1. html.modal-mode{
2. overflow-y: hidden;
3. }
4.
5. .fullscreen{
6. position: fixed;
7. background-color: rgba(0,0,0,0.5);
8. top: 0;
9. right: 0;
10. left: 0;
11. height: auto;
12. z-index: 999999;
13. overflow: auto;
14. bottom: 0;
15. }
16.
17. .fullscreen .tweet-detail{
18. max-width: 700px;
19. overflow: hidden;
20. border-radius: 5px;
21. margin: auto;
22. margin-top: 50px;
23. margin-bottom: 100px;
24. width: 60%;
25. background-color: #fff;
26. }
27.
28. .fullscreen .tweet-detail .tweet-close{
29. position: absolute;
30. display: inline-block;
31. right: 15px;
32. top: 10px;
33. }
34.
35. .fullscreen .tweet-detail .tweet-close:hover{
36. cursor: pointer;
37. }
38.
39. .tweet-detail .tweet-detail-responses{
40. list-style: none;
41. margin: 0px;
42. padding: 0px;
43. }
44.
45. .tweet-detail .tweet-details-reply{
46. border-bottom: 1px solid #E6ECF0;
47. }

Quisiéramos que ya fuera todo para ver como quedo, pero aun nos faltan algunas
cosas más por implementar en el archivo Tweet.js. Como ya mencionamos, el
componente TweetReply se muestra cuando presionamos el botón de responder
(flecha) que se encuentra debajo de cada Tweet, es por esta razón que tenemos
que regresar al componente Tweet para implementar esta funcionalidad. Para
ello agregaremos estos tres cambios:

1. import TweetReply from './TweetReply'

Agregaremos el import al archivo TweetReply.

1. handleReply(e){
2. $( "html" ).addClass( "modal-mode");
3. e.preventDefault()

281 | Página
4.
5. if(!this.props.detail){
6. render(<TweetReply tweet={this.props.tweet}
7. profile={this.state._creator} />,
8. document.getElementById('dialog'))
9. }
10. }

Agregaremos la función handleReply, la cual se encargará de mostrar el


componente TweetReply cuando presionamos el botón.

Observemos que agregamos la clase de estilo model-mode directamente a la


etiqueta HTML, esto lo hacemos con la finalidad de quitarle el scroll a la aplicación
y solo dejarlo para el modal. Sin esto, tanto el Modal como la aplicación tuvieran
scroll y se viera doble barra de desplazamiento.

El siguiente paso es mostrar el componente TweetReply, para ello, hacemos uso


de la función render del módulo react-dom (la misma que usamos en App.js) con
la finalidad de crear el componente TweetReply sobre el elemento dialog.

Seguramente te estas preguntado, que diablos es dialog, la respuesta es fácil es


un <div id=”dialog”> que definimos en el componente TwitterApp con la finalidad
de ser utilizando para mostrar diálogos. Observa el método render de
TwitterApp.js:

1. render(){
2. return (
3. <div id="mainApp">
4. <Toolbar profile={this.state.profile} />
5. <Choose>
6. <When condition={!this.state.load}>
7. <div className="tweet-detail">
8. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
9. </div>
10. </When>
11. <When condition={this.props.children == null
12. && this.state.profile != null}>
13. <TwitterDashboard profile={this.state.profile}/>
14. </When>
15. <Otherwise>
16. {this.props.children}
17. </Otherwise>
18. </Choose>
19. <div id="dialog"/>
20. </div>
21. )
22. }

Este div no tiene nada de especial, es un elemento más, sin embargo, el ID nos
ayuda a referenciarlo para agregarle dinámicamente contenido. En este caso, los
diálogos son introducidos como hijos de este elemento.

Este es el momento perfecto para regresar a la función handleClose de


TweetReply que dejamos pendiente. Esta función hace dos cosas, primero que
nada, le quita la clase de estilo modal-mode al elemento HTML, con la finalidad de
que aparezca nuevamente el scroll y lo segunda, es vaciar el contenido de dialog,
con la finalidad de que el componente modal activo desaparezca.

1. <If condition={!this.props.detail} >

Página | 282
2. <a className="reply-icon"
3. onClick={this.handleReply.bind(this)}
4. data-ignore-onclick>
5. <i className="fa fa-reply " aria-hidden="true"
6. data-ignore-onclick></i> {this.state.replys}
7. </a>
8. </If>

El último cambio al componente Tweet, es agregar el evento onClick al enlace del


reply, para que cuando lo presionemos, mande llamar a la función handleReply.

Con esto, habremos finalizado los cambios necesarios y solo restaría guardar
todos los cambios, actualizar el navegador y ver cómo funciona. No te preocupes
si envías una respuesta y no la puedes ver, eso lo veremos en el siguiente
componente.

El componente TweetDetail

El último componente que nos falta por ver es TweetDetail y con el estaríamos
cerrando el proyecto, al menos por ahora. Así que sin más continuemos para
terminarlo.

El componente TweetDetail es un componente modal que permite ver todo el


detalle de un Tweet, es decir, podemos ver el Tweet y todos los comentarios que
han hecho los demás usuarios. Veamos cómo quedaría:

Fig. 135 - Componente TweetDetail terminado.

Si observamos la imagen, te darás cuenta es muy parecido al componente


TweetReply, sin embargo, en este puedes ver el historial de respuestas por parte
de los otros usuarios, incluso, puedes agregar nuevos comentarios si quieres.

Para iniciar con este componente, vamos a crear el archivo TweetDetail.js en el


path /app, el cual deberá tener el siguiente contenido:

283 | Página
1. import React from 'react'
2. import Reply from './Reply'
3. import Tweet from './Tweet'
4. import APIInvoker from './utils/APIInvoker'
5. import update from 'react-addons-update'
6. import { browserHistory } from 'react-router'
7. import PropTypes from 'prop-types'
8.
9. class TweetDetail extends React.Component{
10.
11. constructor(props){
12. super(props)
13. }
14.
15. componentWillMount(){
16. let tweet = this.props.params.tweet
17. APIInvoker.invokeGET('/tweetDetails/'+tweet, response => {
18. this.setState( response.body)
19. },error => {
20. console.log("Error al cargar los Tweets");
21. })
22. }
23.
24. addNewTweet(newTweet){
25. let oldState = this.state;
26. let newState = update(this.state, {
27. replysTweets: {$splice: [[0, 0, newTweet]]}
28. })
29. this.setState(newState)
30.
31. let request = {
32. tweetParent: this.props.params.tweet,
33. message: newTweet.message,
34. image: newTweet.image
35. }
36.
37. APIInvoker.invokePOST('/secure/tweet', request, response => {
38. },error => {
39. console.log("Error al crear los Tweets");
40. })
41. }
42.
43. handleClose(){
44. $( "html" ).removeClass( "modal-mode");
45. browserHistory.goBack()
46. }
47.
48. render(){
49. $( "html" ).addClass( "modal-mode");
50.
51. let operations = {
52. addNewTweet: this.addNewTweet.bind(this)
53. }
54.
55. return(
56. <div className="fullscreen">
57. <Choose>
58. <When condition={this.state == null}>
59. <div className="tweet-detail">
60. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
61. </div>
62. </When>
63. <Otherwise>
64. <div className="tweet-detail">
65. <i className="fa fa-times fa-2x tweet-close"
66. aria-hidden="true"

Página | 284
67. onClick={this.handleClose.bind(this)}/>
68. <Tweet tweet={this.state} detail={true} />
69. <div className="tweet-details-reply">
70. <Reply profile={this.state._creator}
71. operations={operations}
72. key={"detail-" + this.state._id} newReply={false}/>
73. </div>
74. <ul className="tweet-detail-responses">
75. <If condition={this.state.replysTweets != null} >
76. <For each="reply" of={this.state.replysTweets}>
77. <li className="tweet-details-reply" key={reply._id}>
78. <Tweet tweet={reply} detail={true}/>
79. </li>
80. </For>
81. </If>
82. </ul>
83. </div>
84. </Otherwise>
85. </Choose>
86. </div>
87. )
88. }
89. }
90. export default TweetDetail;

Lo primero que debemos de saber para entender cómo funciona este


componente, es que requiere de un parámetro de URL que lo proporcionamos
mediante react-router, este parámetro corresponde al ID del Tweet que
queremos visualizar, por ello, para llegar a este componente, debemos estar en
una URL como la siguiente:

http://localhost:3000/{username}/{tweet-id}

mediante estos dos parámetros, es posible recuperar el Tweet de un usuario en


específico, ya que conocemos el ID del tweet y el usuario por medio de la URL.

Cuando el componente se crea, la función componenteWillMount se ejecuta, y


carga el Tweet mediante el servicio /tweetDetails/{tweet-id}. Este servicio
difiere de los que hemos usado hasta el momento, porque este regresa solo un
Tweet con todo su detalle. Es decir, el Tweet padre y todo los Tweet hijos
(respuestas).

Documentación: Consultar el detalle de un Tweet


Este servicio consulta un solo Tweet por medio del ID
y retorna el Tweet solicitado junto con todas las
respuestas (/tweetDetails/:tweetId)

Dentro de la función render, podemos ver que se establece la ya conocida clase


de estilo modal-mode al elemento HTML para después continuar con la generación
del componente. Podemos apreciar que el primer elemento del componente es
un <div> con la clase de estilo fullscreen, la cual habilita el modo modal.

Lo que sigue es la creación del componente como tal, aquí hay dos posibles
vistas, si el estado es null, quiere decir que no se ha terminado de cargar el

285 | Página
tweet desde el API, por lo que se muestra un ícono de “cargando”. Por otro lado,
si no es null, el tweet ya se cargó y lo mostramos. Quiero que observes que
nuevamente reutilizamos el componente Tweet y Reply, de la misma forma que
lo utilizamos en TweetReply, pero adicional, creamos una lista (línea 74) de los
tweets hijos. Si un Tweet tiene hijos, los podemos encontrar en el array
this.state.replysTweets y cada reply es representada mediante un componente
Tweet.

Las funciones addNewTweet y handleClose hacen lo mismo que ya conocemos, por


lo que evitaremos redundar en la explicación.

Como este componente es accedido mediante la URL descrita anteriormente, es


necesario crear un Route en el archivo App.js, el cual quedaría de la siguiente
manera:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import TwitterApp from './TwitterApp'
4. import Signup from './Signup'
5. import Login from './Login'
6. import UserPage from './UserPage'
7. import MyTweets from './MyTweets'
8. import Followings from './Followings'
9. import Followers from './Followers'
10. import TweetDetail from './TweetDetail'
11. import { Router, Route, browserHistory, IndexRoute } from "react-router"
12.
13. render((
14. <Router history={ browserHistory }>
15. <Route path="/" component={TwitterApp} >
16. <Route path="signup" component={Signup}/>
17. <Route path="login" component={Login}/>
18.
19. <Route path=":user" component={UserPage} >
20. <IndexRoute component={MyTweets} tab="tweets" />
21. <Route path="followers" component={Followers} tab="followers"/>
22. <Route path="following" component={Followings} tab="followings"/>
23. <Route path=":tweet" component={TweetDetail}/>
24. </Route>
25. </Route>
26. </Router>
27. ), document.getElementById('root'));

En este punto ya hemos terminado de implementar el componente TweetDetail,


sin embargo, falta hacer un ajuste en el componente Tweet, para que nos lleve
al detalle al momento de hacer click sobre él:

1. handleClick(e){
2. if(e.target.getAttribute("data-ignore-onclick")){
3. return
4. }
5. let url = "/" + this.state._creator.userName + "/" + this.state._id
6. browserHistory.push(url)
7. let tweetId = e.target.id
8. }

Página | 286
Lo primero será agregar esta función al componente Tweet.js, el cual nos
permitirá cachar el evento y lanzar el componente TweetDetail. No olvidemos el
import correspondiente al objeto BrowserHistory:

1. import { browserHistory } from 'react-router'

El segundo cambio es sobre la función render, con la finalidad de mandar llamar


la función handleClick al dar click sobre el Tweet.

1. render(){
2. let tweetClass = null
3. if(this.props.detail){
4. tweetClass = 'tweet detail'
5. }else{
6. tweetClass = this.state.isNew ? 'tweet fadeIn animated' : 'tweet'
7. }
8.
9. return (
10. <article className={tweetClass} onClick={this.props.detail ? '' :
11. this.handleClick.bind(this)} id={"tweet-" + this.state._id}>
12. <img src={this.state._creator.avatar} className="tweet-avatar" />
13. <div className="tweet-body">
14. <div className="tweet-user">

Últimos retoques al proyecto

Links en Tweet

Algo que nos faltó en el componente Tweet, es habilitar que el nombre del usuario
que creo el Tweet sea un link a su perfil, por lo que podemos remplazar las
siguientes líneas para implementarlo:

1. <Link to={"/" + this.state._creator.userName} >


2. <a href="#">
3. <span className="tweet-name" data-ignore-onclick>
4. {this.state._creator.name}</span>
5. </a>
6. </Link>

No olvidemos el import correspondiente al componente Link:

1. import { Link } from 'react-router'

Implementando la funcionalidad de Like

Otra funcionalidad que no implementamos, es darle like a los Tweet, por lo que
si quieres terminar de implementar esta funcionalidad solo falta hacer los
siguientes ajustes al componente Tweet.

287 | Página
1. <a className={this.state.liked ? 'like-icon liked' : 'like-icon'}
2. onClick={this.handleLike.bind(this)} data-ignore-onclick>
3. data-ignore-onclick>
4. <i className="fa fa-heart " aria-hidden="true"
5. data-ignore-onclick></i> {this.state.likeCounter}
6. </a>

Al enlace del botón de like (corazón) le agregaremos la llamada a la función


handleLike.

1. handleLike(e){
2. e.preventDefault()
3. let request = {
4. tweetID: this.state._id,
5. like: !this.state.liked
6. }
7.
8. APIInvoker.invokePOST('/secure/like', request, response => {
9. let newState = update(this.state,{
10. likeCounter : {$set: response.body.likeCounter},
11. liked: {$apply: (x) => {return !x}}
12. })
13. this.setState(newState)
14. },error => {
15. console.log("Error al cargar los Tweets", error);
16. })
17. }

La función handleLike se encarga de llamar al API para registrar el like al Tweet


mediante el servicio /secure/like, el cual se ejecuta mediante el método POST y
recibe como parámetro el ID del Tweet y un booleando que indica si es un Like o
Dislike.

No olvidemos el import de la función update:

1. import update from 'react-addons-update'

Documentación: Like/Dislike
Este servicio se utiliza para darle like a un Tweet, o en
su caso, retirar el like (/secure/like).

Página | 288
Resumen

En este punto deberíamos estar muy contentos pues hemos terminado nuestro
proyecto Mini Twitter en su totalidad. Después de un gran trabajo se ve
recompensado nuestro esfuerzo, pues hemos terminado de principio a fin un
proyecto completo y en lo personal yo diría que no es NADA amateur, pues ha
sido una aplicación completa que ha aplicado absolutamente todo lo que hemos
aprendido en el libro.

Si en este punto del libro eres capaz de comprender todo lo que hemos aprendido,
seguramente ya estás listo para enfrentarte a un proyecto real. Pues cuentas con
todas las bases y el conocimiento requerido para desarrollar una aplicación
estándar.

Si bien, en este punto hemos terminado todo el proyecto, todavía existen mejoras
que implementar, como es el caso de Redux, el cual estaremos abordando en el
siguiente capítulo.

289 | Página
Redux
Capítulo 12

Una de las mayores problemáticas de React es la forma en que se administran


los estados y como los cambios pueden afectar a otros componentes. Como ya
hemos venido viendo, React propaga los cambios entre sus componentes de
arriba-abajo y de abajo-arriba, es decir, un componente solo se debería de
comunicar con sus hijos directos y con su padre.

Fig. 136 - React change state

Como podemos ver en la imagen, un cambio realizado en el componente “J”


implicaría que si padre “E” sea actualizado, como consecuencia, “E” actualizaría
a sus hijos (J, K, L) y luego a su padre (A), La actualización de “A” provocaría
una actualización en cascada de componente “B” el cual actualizará a su hijo “G”.
En toda esta serie de actualizaciones, es muy probable que solo requiriéramos
actualizar “G”, pero para hacer esto, tuvimos que recorrer toda la jerarquía hacia
arriba y luego hacia abajo. Otra alternativa hubiera sido actualizar el componente
“G” directamente:

Página | 290
Fig. 137 - Actualización directa de un componente.

Sin embargo, cualquier comunicación que se brinque esta jerarquía es


consideraba una mala práctica, pues fomenta el anti-patrón de código espagueti.
Pues hace tremendamente difícil saber desde donde un componente está siendo
afectado. Es allí donde pedimos el cuidado de las “ref”, pues mediante una
referencia podríamos modificar un componente desde cualquier otro componente
sin importar su jerarquía.

Actualización directa de componentes


La actualización directa de un componente es
considerada una mala práctica, por que fomenta el
código espagueti, por lo que debe de ser evitado a toda
costa, salvo casos estrictamente necesarios (y
documentados).

Introducción a Redux
Dicho lo anterior, Redux es una herramienta que nos ayuda a gestionar la forma
en que accedemos y actualizamos el estado. La idea principal de Redux es
centralizar todo el estado de la aplicación en algo llamado “Store”, o también
conocido en Redux como “single source of truth” (única fuente de la verdad). Con
esto, liberamos a los componentes de gestionar un estado independiente.

De esta forma, cuando el componente quiere realizar un cambio en el estado,


tendrá que enviar una solicitud expresando sus intenciones. El Store realizará los
cambios al estado y notificará a los interesados acerca de los cambios producidos:

291 | Página
Fig. 138 - Cambio del estado con Redux.

Seguramente te estarás preguntando como es que el componente “G” recibe las


actualizaciones del estado, y la respuesta es muy simple. En realidad, los
componentes se suscriben al Store con la finalidad de ser notificado cuando hay
algún cambio que les sea de interés. De esta forma, mantenemos un estado único
en toda la aplicación y que elimina las restricciones de actualización jerárquica.
¿Interesante no?

En este punto podrías estarte haciendo la pregunta, ¿pero que no es una mala
práctica actualizar un componente directamente sin pasar por la jerarquía de
componentes? La cual es una excelente pregunta; y la respuesta es simple, la
realidad es que no se realiza una actualización directa del estado de un
componente, si no que se actualiza el Store, el cual tiene el estado de toda la
aplicación. Además, como los componentes se registran al Store, es fácil detectar
de donde proviene los cambios al estado.

Por otra parte, la utilización de Stores para guardar el estado proviene de un


patrón de diseño llamado Flux. En este sentido, Redux se parece mucho a Flux.

Flux como tal es patrón de diseño, por lo que nos podemos descargarlo como tal
y utilizarlo en React, sin embargo, existe un módulo llamado react-flux, el cual
implementa este patón. Ahora bien, React-flux fue desarrollado primero que
Redux y por un tiempo fue la mejor alternativa, sin embargo, hoy por hoy, Redux
es superior, tal es así, que Facebook contrato a su principal desarrollador Redux
(Dan Abramov).

Componentes de Redux

Antes que nada, quiero que sepas que Redux es una herramienta para cualquier
aplicación basada en JavaScript, por lo que puede ser utilizada en aplicaciones
100% JavaScript, Angular, aplicaciones basadas en JQuery, React, etc. Es por

Página | 292
este motivo que debemos de entender cómo funciona Redux por sí mismo, antes
de implementarlo con React.

Para implementar Redux en cualquier aplicación es necesario implementar 4


cosas:

1. Store: Es donde vive todo el estado de la aplicación.


2. Reducer(s): Son funciones que permiten controlar las acciones (action)
para actualizar el estado.
3. Action(s): Son objetos JavaScript que describen una intención para
cambiar el estado, las cuales son atendidas por los reducers.
4. Dispatch: El dispatch es el mecanismo por el cual lanzamos las acciones
(action) para ser atendidas por el reducer(s).

No te preocupes si de momento no entendiste nada, vamos a profundizar en la


explicación. Pero antes, quiero que veas el siguiente ejemplo:

Fig. 139 - Ciclo de vida de Redux

La imagen anterior representa mucho mejor el ciclo de vida de Redux y cómo es


que las diferentes partes se conectan para actualizar el estado del Store y
actualizar la vista. Veámoslo más detenidamente.

293 | Página
1. La vista (Componentes) envía una solicitud de cambio al store mediante la
función dispatch que proporciona el mismo Store.
2. El Action es un objeto JavaScript de describe los cambios que quiere
realizar sobre el estado del Store.
3. El Store recibe el Action y lo envía a los diversos reducer para actualizar el
estado.
4. El reducer son funcionas que implementamos nosotros mismos, los cuales
tienen la lógica para actualizar el Estado basado en los Actions. El reduce
actualiza el estado y lo retorna al Store para sobrescribir el estado actual
con el nuevo.
5. Finalmente, el Store notifica a todos los componentes suscritos. Los
componentes pueden recuperar el nuevo estado para realizar las
actualizaciones correspondientes.

Los tres principios de Redux

Adicional al ciclo de vida, es necesario conocer los 3 principios de Redux, los


cuales se deberán cumplir siempre:

Una única fuente de la verdad

Redux funciona únicamente con un solo Store para toda la aplicación, es por eso
que se le conoce como la única fuente de la verdad. La estructura que manejemos
dentro del Store, dependerá totalmente de nosotros, por lo que somos libre de
diseñarla a como se acomode mejor a nuestra aplicación. Debido a esto, suele
ser una estructura con varios niveles de anidación.

Las personas que esta familiarizadas con Flux, notará que esta es una de las
principales diferencias entre Redux y Flux, pues flux permite la creación de
múltiples Stores.

El estado es de solo lectura (read-only)

Una de las restricciones de Redux, es que no existe una forma para actualizar el
estatus directamente, en su lugar, es necesario enviar un Action al Store,
describiendo las intenciones de actualizar el estado. Por su parte, el store solo
proporciona los siguientes métodos:

 getState()
 replaceReducer(reducer)
 dispatch(action)
 subscribe(listener)

Página | 294
Como se puede observar, no existe ninguna función como setState() que nos
permite actualizar el Estado, dejándonos como única alternativa, el envío de una
acción por medio de la función dispatch.

Los cambios se realizan con funciones puras

Como ya lo habíamos mencionado, solo es posible actualizar el estado mediante


un action, el cual manifiesta la intención de cambiar el estado al mismo tiempo
que describe el cambio que quiere realizar. Cuando la acción llega al Store, este
redirige la petición a los reducers.

Los reducers deberán de escribirse siempre como funciones puras. Para que una
función sea pura, debe de cumplir con las siguientes características:

 La función no deberá de llamar a ningún recurso externo, como pueden


ser bases de datos o servicios web.
 Los valores que retorna la función solo dependerá de sus parámetros de
entrada, lo que nos lleva al siguiente punto:
 El resultado con el mismo conjunto de parámetros, deberá dar siempre
el mismo resultado.
 Los argumentos deben de considerarse inmutables, lo que implica que
no podemos actualizarlos por ninguna razón.

Estos se llaman "puros", porque no hacen más que devolver un valor basado en
sus parámetros. Además, no tienen efectos secundarios en ninguna otra parte
del sistema.

Algo que probablemente no quedo muy claro con respecto al cuarto punto, es
que, dado que el estado actual es un parámetro de entrada del reducer, no
deberíamos modificarlo, en su lugar, tendríamos que hacer una copia de él y
sobre ese agregar los nuevos cambios. Esto es exactamente lo mismo que
hacíamos con la función update del módulo react-addons-update por lo que no
debería de presentar una sorpresa para nosotros.

Otro de los puntos a tomar en cuenta, es que los Action deben de tener una
estructura mínima, en la cual debe de existir la propiedad type, seguido de esto,
puede venir lo que sea, incluso, podríamos mandar solo la propiedad type, por
ejemplo:

1. //Option 1
2. {
3. type: "LOGIN"
4. }
5.
6. //Option 2
7. {
8. type: "LOGIN",

295 | Página
9. profile: {
10. id: "1234",
11. userName: "oscar",
12. name: "oscar blancarte"
13. }
14. }

La propiedad type es importante, porque le dice a los reducers la acción que


quieres realizar sobre el estado.

Como funciona Redux

Con toda esta teoría, vamos a ver un pequeño ejemplo de cómo funciona Redux,
para esto, te pido que analices el siguiente fragmento de código:

1. const initialState = {
2. load: false,
3. profile: {
4. id: "",
5. name: "",
6. userName: "",
7. avatar: "",
8. banner: ""
9. }
10. }
11.
12. //Login Reducer
13. export const loginReducer = (state = initialState, action) => {
14. switch (action.type) {
15. case LOGIN_SUCCESS:
16. return {
17. load: true,
18. profile: action.profile
19. }
20. default:
21. return state
22. }
23. }
24. //Create Redux Store
25. var store = Redux.createStore(loginReducer)

Lo primero que vemos es una constante llamada initialState, la cual


utilizaremos para iniciar el estado del Store.

Lo segundo que podemos observar es la función loginReducer, la cual es la


implementación de nuestro primero reducer, observa que es un función común y
corriente, la cual recibe como parámetro el estado y el action, también notarás
que el parámetro estado esta igualado la variable initialState, lo cual nos ayuda
a asignar el estado al momento de crear el Store.

Esta función tiene un switch que nos ayuda a identificar el tipo de action, y
vasado en él sabrá como actualizar el estado. Ahora bien, en este ejemplo
tenemos dos opciones, si el tipo es LOGIN_SUCCESS o no lo es, en caso de serlo,
se creará un nuevo objeto, indicando la propiedad load en true y el valor del
profile que es igual al valor contenido en el action. Podemos observar que este

Página | 296
nuevo objeto es retornado por el reducer, lo que implica que el Store lo reciba y
lo establezca como el nuevo estado del Store.

Finalmente, en la línea 25 tenemos la creación del Store mediante la función


createStore del objeto Redux. Podemos observar que la creación del Store se crea
recibiendo como parámetro la función del reducer.

Por otra parte, deberemos implementar una función en la vista que ejecute el
dispatch para iniciar la actualización de la vista:

1. export const relogin = () => (dispatch,getState) => {


2.
3. let token = window.localStorage.getItem("token")
4. APIInvoker.invokeGET('/secure/relogin', response => {
5. window.localStorage.setItem("token", response.token)
6. window.localStorage.setItem("username", response.profile.userName)
7. dispatch({
8. type: LOGIN_SUCCESS,
9. profile: response.profile
10. })
11. },error => {
12. window.localStorage.removeItem("token")
13. window.localStorage.removeItem("username")
14. browserHistory.push('/login');
15. })
16. }

Te pido que imagines que esta función es ejecutada por un componente, por lo
que no deberíamos preocupar de donde llegaron los parámetros de entrada. Por
ahora, el parámetro dispatch es una referencia que nos permitirá enviar una
action al store, y getState es una referencia para obtener el estado actual del
store.

Ahora bien, ya que hemos expuesto todos los partes que componente una
aplicación con Redux, vamos a explicar el orden de ejecución. Tras la creación
del Store en la línea 25 del código anterior (var store =
Redux.createStore(loginReducer) ), lo primero que pasará es que el store
llamará al reducer con la finalidad de inicializar el estado, por lo que en la primera
ejecución del reducer, establecerá el estado al valor de la variable initialState
(state = initialState). Una vez hecho esto, la aplicación quedará en espera de
una nueva acción.

Seguido de esto, la aplicación ejecuta la función relogin para autenticar al


usuario, lo que detonará en la llamada de un servicio del API, al retornar
correctamente, este mandará llamar la función dispatch con un action, el cual
define el tipo de acción y el mensaje que describe el cambio, es decir, manda el
perfil recuperado del API.

Cuando la función dispatch es ejecutada, el store manda llamar al reducer:

1. export const loginReducer = (state = initialState, action) => {


2. switch (action.type) {
3. case LOGIN_SUCCESS:
4. return {
5. load: true,
6. profile: action.profile

297 | Página
7. }
8. default:
9. return state
10. }
11. }

Mediante el switch identificamos que la acción es LOGIN_SUCCES y lo que pasa es


que un nuevo objeto es creado y retornado, esto provoca que el store tome este
objeto y lo establezca como el nuevo estado.

Cuando el estado es actualizado, los componentes interesados son notificados y


una función como la siguiente; dentro del componente es llamada, con la
intención de actualizar el estado del componente:

1. const mapStateToProps = (state) => {


2. return {
3. load: state.loginReducer.load,
4. profile: state.loginReducer.profile
5. }
6. }

Por ahora no nos preocupemos como es que esta función es llamada, solo
imaginemos que está dentro de nuestro componente y es llamada en automático.

La función mapStateToProps tiene como finalidad mapear el estado del store a


propiedades (props) del componente interesado. De esta forma, los componentes
no requieren del estado para almacenar la información, y en su lugar, el store la
almacena.

Sé que suena raro que ahora te diga que ya no vamos a utilizar el estado, pues
durante todo el libro hemos hablado de su importancia. Pero verá que todo cobra
sentido una vez que lo expliquemos.

Debido a que el principio de Redux es tener solo una fuente de la verdad, tener
estados distribuidos por todos los componentes rompería ese principio, pues cada
componente tuviera su propia fuente de la verdad a. Para prevenir esto, Redux
se apoya de funciones como la anteriores, con la finalidad de mapear el estado
del Store a propiedades del componente.

Esto quiere decir que si antes obteníamos el userName del estado de la siguiente
manera (this.state.profile.userName) ahora lo obtendremos así
(this.props.profile.userName), el nombre de las props dependerá de la forma
en que creamos el objeto en la función mapStateToProps. En este ejemplo,
tendríamos dos props, this.props.load y this.props.profile. Ahora bien, fíjate
que los valores de estas props se obtienen directamente del parámetro state, el
cual es el estado actual del Store.

Ahora bien, la importancia de utilizar props, en lugar del estado es simple, ya


que el estado puede cambiarse, mientras que las propiedades son inmutables.
Lo que implica que por ninguna circunstancia podríamos modificar los datos del
componente a menos que las propiedades sean actualizadas.

Página | 298
Ahora bien, como es que cuando hay un cambio en el Store, el componente es
notificado. Para esto existe un módulo llamado react-redux, el cual nos facilita
enormemente la vida. Este módulo nos proporciona la función connect, la cual se
utiliza para conectar un componente con Redux. Este módulo funciona de la
siguiente manera:

1. export default TwitterApp;


2.
3. export default connect(mapStateToProps, { relogin })(TwitterApp);

En la línea 1, podemos ver la forma en que hemos exportado los componentes


hasta el momento, pero en línea 3 vemos como deberemos exportar los
componentes de ahora en adelante para utilizar Redux.

La función connect, recibe al menos un parámetro, que puede ser un objeto o


una función que regrese un objeto, por claridad, siempre es aconsejable utilizar
una función como mapStateToProps para convertir el estado del Store en props
para el componente.

El segundo parámetro nos permite definir un objeto o una función que regrese
un objeto (mapDispatchToProps), en este objeto deberá estar las funciones
disponibles por el componente, las cuales disparar los actions. En este caso solo
hemos pasado la función relogin que definimos más arriba. Esta función recibirá
el dispatch y getState de los cuales ya hablamos.

Ahora bien, te llamará la atención la sintaxis de connect, pues tiene dobles


paréntesis, es decir connect()(), esto es en realidad muy simple, la función
connect regresa otra función, la cual requiere como parámetro un Component.

Finalmente, si estamos usando react-router, solo nos quedaría definir un objeto


provider, que englobe toda la aplicación:

1. render((
2. <Provider store={ store }>
3. <Router history={ browserHistory }>
4. <Router component={TwitterApp} path="/">
5. <Route path="/signup" component={Signup}/>
6. <Route path="/login" component={Login}/>
7.
8. <Route path="/:user" component={UserPage} >
9. <IndexRoute component={MyTweets} tab="tweets" />
10. <Route path="followers" component={Followers} tab="followers"/>
11. <Route path="following" component={Followings} tab="followings"/>
12. <Route path=":tweet" component={TweetDetail}/>
13. </Route>
14. </Router>
15. </Router>
16. </Provider>
17. ), document.getElementById('root'));

Este objeto recibe como parámetro el único Store de toda la aplicación.

299 | Página
Sé que en este punto te está por explotarte la cabeza, pues hemos cambiado
drásticamente la forma en que hemos venido trabajando. Pero te pido que no te
desesperes, pues una vez que retomemos el proyecto Mini Twitter verás que es
más fácil de implementar de lo que parece.

Implementando Redux Middleware

En la sección pasada analizamos la función connect, la cual nos permitía enviarle


dos parámetros, el primero es un objeto para mapear el estado del Store en
propiedades (mapStateToProps) del componente y el segundo era un objeto con
las funciones para despachar Actions (mapDispatchToProps).

Hasta aquí todo está bien, sin embargo, nos faltó mencionar algo, el objeto
mapDispatchToProps no acepta por default funciones, y menos, que están tenga
una respuesta asíncrona, en pocas palabras, podríamos enviarle puros objetos.
El problema de estos, es que en aplicaciones donde todo el API se consume de
forma asíncrona, puede ser un gran inconveniente.

Para solucionar este problema, Redux nos ofrece la alternativa de utilizar


Middleware, el cual es una función que se ejecuta entre el dispatcher y el Store:

Fig. 140 - Redux Middleware

En este nuevo diagrama ya podemos ver más claramente en que parte se sitúa
el Middleware, la importancia que tiene en la arquitectura, es que intercepta el
mensaje del dispatch antes de que llegue a los reducers, espera que las
funciones asíncronas terminen y entonces manda llamar a los reducers.

Redux soporta el uso de Middleware, pero requiere de una implementación


concreta para hacerlo, esta implementación no viene por default en Redux, por
lo que es necesario instalarla. En este libro hemos optado por utiliza el módulo
redux-thunk, el cual es bastante fácil de implementar. Veamos mediante un
ejemplo como implementarlo:

Página | 300
1. import { createStore, applyMiddleware } from 'redux'
2. import thunk from 'redux-thunk'
3.
4. export const store = createStore(
5. reducer,
6. applyMiddleware(thunk),
7. )
8.
9. render((
10. <Provider store={ store }>
11. <Router history={ browserHistory }>
12. </Router>
13. </Provider>
14. ), document.getElementById('root'));

Para registrar un middleware en Redux solo es necesario utilizar el método


applyMidleware que nos proporciona el mismo módulo de redux, este método
recibe como parámetro el Middleware que queremos agregar, en este caso le
mandamos el Middleware que nos proporciona thunk. Con estos simples pasos
hemos terminado de implementar redux-thunk.

Debugging Redux

Como ya mencionamos, los Middleware interceptan las llamadas entre el


dispatcher y el store, esto quieres decir que podríamos utilizarlos para depurar
la aplicación, pues podríamos saber el estado como estaba el estado antes y
después de realizar una acción.

Para nuestra suerte, ya existe un módulo que nos ayuda a depurar en tiempo
real como es que el estado se va actualizando, a medida que las acciones lo van
cambiando. El módulo es redux-logger y para implementarlo solo se requiere
hacer un ajuste un simple:

1. import { createStore, applyMiddleware } from 'redux'


2. import thunk from 'redux-thunk'
3. import { createLogger } from 'redux-logger'
4.
5. const middleware = [ thunk ];
6. if (process.env.NODE_ENV !== 'production') {
7. middleware.push(createLogger());
8. }
9.
10. export const store = createStore(
11. reducer,
12. applyMiddleware(...middleware),
13. )
14.
15. render((
16. <Provider store={ store }>
17. <Router history={ browserHistory }>
18. </Router>
19. </Provider>
20. ), document.getElementById('root'));

301 | Página
El primer cambio será importar redux-loger, lo segundo será crear un array para
pasar los dos middlewares (thunk y createLogger). Luego en la línea 6,
validaremos si el ambiente NO es productivo, con la finalidad de agregar el
middlware createLogger a nuestro array de middlewares. Finalmente, creamos
el store como siempre. No es recomendado activar el log en ambiente
producción, debido a que le carga más trabajo a React.

El módulo redux-logger no viene por default en la instalación de Redux, por lo


que será necesario instalarlo mediante npm:

npm install --save -D [email protected]

Con este pequeño cambio podremos ver en el log del navegados como va
cambiando el store en cada action:

Fig. 141 - Vistazo rápido a React-logger

En la imagen anterior podemos ver como se ve cada vez que un action es


despachado. A simple vista podemos ver datos muy interesantes, como lo son el
Action Type, la fecha y hora en que se recibió la acción, el estado anterior, el
objeto Action que cambio el estado y el nuevo estado una vez que fue procesado
por los reducers. Pero eso no es todo, cada sección que vemos la podemos
expandir para ver más detalle:

Página | 302
Fig. 142 - Detalle de Redux-logger

En esta nueva imagen ya podemos ver cómo podemos expandir el log para ver
la estructura completa de los objetos, así como todos sus valores. En esta imagen
podemos ver solo un nivel de profundidad, pero es posible seguir expandiendo
los objetos hasta llegar al más mínimo detalles.

Implementando Redux con React

En esta sección prepararemos nuestro proyecto para ser migrado a Redux, por
lo que crearemos las estructuras de carpetas necesarias y explicaremos la
migración de uno de los componentes del proyecto Mini Twitter a modo de
explicación. La intención de esta sección es que, al terminar, tengamos un
ejemplo de react-redux funcionando en nuestro proyecto.

Estrategia de migración

Debido a que migrar toda la aplicación puede llegar a ser una tarea tardada y
complicada, necesitamos tener una estrategia clara de cómo llevaremos la
migración. En esta estrategia explicaremos los pasos que es necesario realizar
para la migración y las consideraciones que debemos de tener en cuenta.

Migrar los componentes es una tarea muy repetitiva, por lo que evitaremos lo
más posible detenernos para explicar lo que estamos haciendo en cada paso, en
lugar de eso, quiero explicarte el proceso que llevaremos para todos los
compontes, dicho proceso será el mismo para todos.

303 | Página
El proceso de migración por componente implica 5 etapas las cuales son:

 Migrar la lógica de los componentes (funciones) al archivo Actions.js y


crear los actions que servirán para actualizar el estado por medio de los
reducers.
 Crear constantes que identifiquen las acciones lanzadas al store.
 Crear los reducers que procesarán los actions despachados por las
funciones.
 Registrar el reducer en el combineReducers.
 Conectar el componente con Redux.

Etapa 1: Migrar funciones y crear actions

En la primera etapa, todas las funciones que utilizamos para actualizar el estado
o consultar servicios del API, deberá ser migradas al archivo Actions.js. Estas
nuevas funciones tendrán el siguiente formato:

1. export const dispatchFunction = ( customParams ) => (dispatch, state) => {


2. //Any action
3. dispatch( action(params) )
4. }

Las funciones deberán ser constantes y deberá ser exportadas, pues las
consumiremos desde el componte. Las funciones podrán recibir parámetros
custom y los parámetros dispath y state, estos dos últimos son inyectados por
Redux y juegan un papel clave. El parámetro state contiene el estado actual del
store y dispatch es una referencia a la función para disparar actions al store.

Las funciones podrán hacer lo que sea en su implementación y en caso de


requerir actualizar el estado, deberán lanzar actions, como podemos ver en la
línea 3.

Los actions, son objetos que lanzamos al store para indicar nuestras intenciones
de actualizar el estado, dichos objetos pueden tener la estructura que más nos
convenga, pero al menos debe de tener el atributo type. Veamos el siguiente
ejemplo.

1. const updateLoginFormRequest = ( params ) => ({


2. type: UPDATE_LOGIN_FORM_REQUEST,
3. value: params.value,
4. })

Página | 304
Fig. 143 - Etapa 1: migrando funciones y creando los actions.

Etapa 2: Crear las constantes

Para tener un mejor control de los tipos de acciones (types) que podemos lanzar
a través del dispatcher, es necesario crear una serie de constantes que
utilizaremos en la propiedad type de los actions. Estas constantes se crean en el
archivo const.js y tiene un formato como el siguiente:

1. export const SIGNUP_RESULT_FAIL = 'SIGNUP_RESULT_FAIL'

Etapa 3: Creación de los reducers

En esta etapa, crearemos los archivos que atenderán los actions lanzados por
nuestras funciones del archivo Actions.js. Como ya comentamos, los reducer
son funciones puras que actualizan el estado a partir de los actions.

Etapa 4: Registro de los reducers en el combineReducer

Cuando trabajamos con más de un reducer, es necesario registrarlos al store por


medio de un combineReducer, el cual lo manejaremos en un nuevo archivo
llamado index.js que veremos más adelante.

Etapa 5: Conectar el componente a Redux

En esta epata tenemos que referencias las funciones que creamos en el archivo
Actions.js y mapear el estado del store en props del componente.

Cuando un componente requiere consultar un servicio o consumir el API, este


consumirá una función del archivo Actions.js, el cual a su vez despachará un

305 | Página
action para actualizar el estado. Cuando la actualización del estado se lleve a
cabo, el componente se actualizará para tomar los nuevos cambios

Fig. 144 - Etapa 4: conectar el componente con Redux

Instalar las dependencias necesarias

Iniciaremos con las instalaciones de los módulos necesarias para utilizar Redux
junto con los Middleware, por ello, ejecutaremos los siguientes comandos:

 npm install --save [email protected]


 npm install --save [email protected]
 npm install --save [email protected]
 npm install -D --save [email protected]

El nombre de los módulos es bastante descriptivo como para darnos cuenta de


que se trata cada uno, pero por las dudas, aquí te dejo la explicación. redux es
el modulo que contiene la herramienta estándar, es decir, que funciona para
cualquier aplicación JavaScript. react-redux, es módulo que facilita el desarrollo
de aplicaciones React que utilizan Redux. redux-thunk es el middleware que nos
permite ejecutar funciones asíncronas como parte de las funciones de dispath
(mapDispatchToProp). y redux-logger es el módulo que contiene el middleware
para habilitar el log o debug de Redux, agregamos el parámetro -D debido a que
el logger solo necesitamos en modo desarrollo.

Estructura del proyecto con Redux

Debido a que Redux cuenta con elementos que antes no teníamos, es necesario
mejorar nuestra estructura de carpetas con la finalidad de tener un mejor orden.

Página | 306
Iniciaremos con la creación de dos carpetas nuevas dentro del path /app, las
cuales se llamarán actions y reducers. La idea es que actions estén los objetos
que serán despachados por el dispatcher y tengamos con constantes todos los
types para ser reutilizados donde sea necesario.

Fig. 145 - Estructura de archivos con Redux.

Vamos a iniciar con la migración del componente App.js y TwitterApp.js para


comprender como funciona Redux, por lo que trataremos de ir despacio por ser
el primer ejemplo, pero una vez migrados estos componentes, nos iremos más
rápido, pues migrar la aplicación completo es prácticamente lo mismo para cada
componente.

Creando nuestro archivo de acciones

Como ya lo hablamos, los actions son objetos JavaScript simples, que representa
la intención de cambiar el estado dentro del store, también comentamos que
estos objetos pueden tener la estructura que sea, sin embargo, debe de tener al
menos la propiedad type, pues es necesario para que los reducers entienda que
acción hay que realizar sobre el estado.

De hora en adelante, vamos a definir todos los actions que necesitemos en un


nuevo archivo llamado Actions.js el cual deberá estar en el path /app/actions.
Este archivo lo iniciaremos de la siguiente manera:

1. import {
2. LOGIN_SUCCESS,
3. } from './const'
4.
5. import APIInvoker from '../utils/APIInvoker'
6. import { browserHistory } from 'react-router'
7. import update from 'react-addons-update'
8.
9. const loginSuccess = profile => ({
10. type: LOGIN_SUCCESS,
11. profile: profile
12. })
13.

307 | Página
14. const loginFail = () => ({
15. type: LOGIN_SUCCESS,
16. profile: null
17. })

De momento no vamos a necesitar todo lo que hemos importado, sin embargo,


dentro de poco vamos a ir ampliando este archivo, por lo que será más simple
definir todo lo que vamos a necesitar de una vez, en lugar ir agregándolo en cada
paso.

El import que vemos en la línea 1, es sobre una constante llamada LOGIN_SUCCES,


la cual todavía no definimos. Pero es importante notar que es una constate de
tipo String. También estamos importante los clásicos, update para actualizar el
estado, APIInvoker para consumir servicios REST y browserHistory, para poder
redirigir al usuario.

Lo importante en este archivo son las siguientes dos constantes, loginSuccess y


loginFails, estas dos constantes será utilizadas para enviarlas como mensajes
al store por medio del dispatcher. Lo interesante en estos dos objetos, es que
describen una intención para cambiar el estado, por ejemplo, loginSuccces define
como type LOGIN_SUCCESS, y la propiedad profile será igual al parámetro
entrante. Por otro parte loginFail, también define el type como LOGIN_SUCCESS,
pero este define el profile en null.

Podemos ver claramente que loginSuccess intenta actualizar el estado con un


nuevo perfil, mientras que loginFail, intenta forzar el perfil a null, lo que
indicaría que no habría un usuario autenticado.

La sintaxis que estamos utilizando para definir los actions son arrow functions
(funciones flecha) por lo que, si no estás familiarizado con ellas, te dejo este link
a la documentación de mozilla.

Por otra parte, vamos a crear el archivo const.js en el path /app/actions, en el


cuál, vamos a definir los tipos de acciones (types) con la intención de poder
utilizarlas en donde sea necesario sin repetir código. El archivo tendrá por el
momento solo la siguiente línea:

1. export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'

A medida que tengamos más actions tendremos que ir creciendo el número de


actions types.

Creando el primer reducer

Una vez que hemos definido los actions, continuaremos desarrollando el reducer
que atenderá a los actions loginSuccess y loginFail. Para ello deberemos de
crear un nuevo archivo llamado LoginReducer.js en el path /app/reducers, el
cual deberá tener el siguiente contenido:

Página | 308
1. import {
2. LOGIN_SUCCESS
3. } from '../actions/const'
4.
5. const initialState = {
6. load: false,
7. profile: null
8. }
9.
10. //Login Reducer
11. export const loginReducer = (state = initialState, action) => {
12. switch (action.type) {
13. case LOGIN_SUCCESS:
14. return {
15. load: true,
16. profile: action.profile
17. }
18. default:
19. return state
20. }
21. }
22.
23. export default loginReducer

La función loginReducer será la que se convierta en un reducer al momento crear


al store, mientras eso no pase, esta es solo una función más en nuestra
aplicación. Notemos que la función es exportada, debido a que será necesario
para que el store pueda acceder ella.

Otra de las cosas que podemos apreciar, es que hemos inicializado el estado con
la constante initalState (línea 10). Un error es creer que la variable state
siempre valdrá igual que initalState, sin embargo, este valor solamente lo toma
cuando se inicializa el store, después de esto, el parámetro state deberá ser
enviado por el store en cada invocación.

El segundo parámetro represente la acción enviada, en este caso, podría ser un


de los dos actions que ya definimos, que son loginSuccess y loginFail.

Una vez ejecutado el reducer, este deberá tener un switch que nos ayuda a
identificar el tipo (type) de acción, ya que a partir de él, sabrá como actualizar el
estado. Notemos que el case del switch hace uso de la constante LOGIN_SUCCES
del archivo const.js.

Una vez que el tipo de mensaje ha sido identificado hay que tomar acciones,
como podemos ver el caso (case) de LOGIN_SUCCES, se crea un nuevo objeto,
indicando que la carga ha terminado y actualizando la propiedad profile con el
valor que nos enviaron en el action.

En el caso de que el type no corresponda con ninguno de las acciones que puede
controlar el reducer es muy importante regresar el mismo estado que entro
como parámetro, pues solo así, Rudex sabe si el estado no cambios y, por ende,
no notifica a los suscriptores. Por esa misma razón es importante no mutar el
estado, pues al retornar la misma instancia, Redux lo puede interpretar como si
no hubiera cambios.

309 | Página
Debido a que la aplicación tendrá más de un reducer, es necesario crear algo
llamado combineReducers, lo cual permite a Redux trabajar con más de un
reducer. Para esto, tendremos que crear un archivo llamado index.js en el path
/app/reducers, el cual deberá tener el siguiente contenido:

1. import { combineReducers } from 'redux'


2. import loginReducer from './LoginReducer'
3.
4. export default combineReducers({
5. loginReducer
6. })

De momento solo tenemos un reducer, por lo que no toma mucho sentido utilizar
un combineReducers, pero a medida que avancemos en el proyecto, esto vendrá
creciendo.

Funciones de dispatcher

En este punto ya tenemos creado los actions y los stores que los procesarán,
sin embargo, falta implementar la lógica que disparara (dispatcher) las acciones
hasta el store. Para esto, vamos a estar creando las funciones de dispatcher
dentro del archivo Actions.js.

Como estamos migrando el componente TwitterApp, es importante recordar que


este ejecuta el servicio relogin, el cual permitía validar si el usuario estaba
autenticad o no, de lo contrario, redireccionaba al usuario a la página de login.
Veamos cómo lo implementamos:

1. componentWillMount(){
2. let token = window.localStorage.getItem("token")
3. if(token == null){
4. browserHistory.push('/login')
5. this.setState({
6. load: true,
7. profile: null
8. })
9. }else{
10. APIInvoker.invokeGET('/secure/relogin', response => {
11. this.setState({
12. load: true,
13. profile: response.profile
14. });
15. window.localStorage.setItem("token", response.token)
16. window.localStorage.setItem("username", response.profile.userName)
17. },error => {
18. console.log("Error al autenticar al autenticar al usuario " );
19. window.localStorage.removeItem("token")
20. window.localStorage.removeItem("username")
21. browserHistory.push('/login');
22. })
23. }
24. }

Página | 310
Quiero que observes algo clave en esta función, y es que el estado se está
actualizando una vez que el servicio relogin responde. Lo cual rompe con el
principio de Redux de una única fuente de la verdad, por tal motivo, es necesario
cambiar el comportamiento.

Para corregir este problema, regresaremos al archivo Actions.js y definiremos


una función llamada relogin, la cual se verá así:

1. export const relogin = () => (dispatch,getState) => {


2.
3. let token = window.localStorage.getItem("token")
4. if(token == null){
5. dispatch(loginFail())
6. browserHistory.push('/login');
7. }else{
8. APIInvoker.invokeGET('/secure/relogin', response => {
9. window.localStorage.setItem("token", response.token)
10. window.localStorage.setItem("username", response.profile.userName)
11. dispatch(loginSuccess( response.profile ))
12. },error => {
13. console.log("Error al autenticar al autenticar al usuario " );
14. window.localStorage.removeItem("token")
15. window.localStorage.removeItem("username")
16. browserHistory.push('/login');
17. })
18. }
19. }

Quiero que observes detenidamente esta nueva función. Notarás que es


exactamente igual a lo que tenemos definido componentWillMount de TwitterApp,
con dos pequeñas diferencias, en lugar de establecer el estado con setState(),
estamos despachando las acciones loginSucces en el caso de éxito y loginFail
en caso de que el token sea null.

Observa que loginFail no requiere parámetros, pues establecerá el perfil en null,


pero loginSuccess toma el perfil que obtuvimos del servicio y lo mando como
parámetro para la creación del action (línea 11).

Por otra parte, podemos ver que esta nueva función recibe como parámetro las
funciones dispatch y getState. La función dispath es una referencia que inyecta
Redux para poder despachar los actions y getState es la función que nos
regresará el estado actual del Store.

Implementar la connect en TwitterApp

Una vez lista la función relogin, es hora de entrar de frente con el componente
TwitterApp, para esto, tendremos que hacer algunos cambios:

1. import React from 'react'


2. import APIInvoker from "./utils/APIInvoker"
3. import Toolbar from './Toolbar'
4. import { browserHistory } from 'react-router'
5. import TwitterDashboard from './TwitterDashboard'
6. import { connect } from 'react-redux'
7. import { relogin } from './actions/Actions'

311 | Página
8.
9. class TwitterApp extends React.Component{
10.
11. constructor(props){
12. super(props)
13. this.state = {
14. load: true,
15. profile: null
16. }
17. }
18.
19. componentWillMount(){
20. this.props.relogin()
21. let token = window.localStorage.getItem("token")
22. if(token == null){
23. browserHistory.push('/login')
24. this.setState({
25. load: true,
26. profile: null
27. })
28. }else{
29. APIInvoker.invokeGET('/secure/relogin', response => {
30. this.setState({
31. load: true,
32. profile: response.profile
33. });
34. window.localStorage.setItem("token", response.token)
35. window.localStorage.setItem("username", response.profile.userName)
36. },error => {
37. console.log("Error al autenticar al autenticar al usuario " );
38. window.localStorage.removeItem("token")
39. window.localStorage.removeItem("username")
40. browserHistory.push('/login');
41. })
42. }
43. }
44.
45. render(){
46. return (
47. <div id="mainApp">
48. <Toolbar profile={this.props.profile} /> {this.state.profile}
49. <Choose>
50. <When condition={!this.props.load}> {¡this.state.load}
51. <div className="tweet-detail">
52. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
53. </div>
54. </When>
55. <When condition={this.props.children == null
56. && this.props.profile != null}>
57. <TwitterDashboard profile={this.props.profile}/>
58. </When>
59. <Otherwise>
60. {this.props.children}
61. </Otherwise>
62. </Choose>
63. <div id="dialog"/>
64. </div>
65. )
66. }
67. }
68.
69. const mapStateToProps = (state) => {
70. return {
71. load: state.loginReducer.load,
72. profile: state.loginReducer.profile
73. }

Página | 312
74. }
75.
76. export default connect(mapStateToProps, { relogin })(TwitterApp);

En este caso, lo mejor será explicar de abajo hacia arriba, pues tendrá más
sentido. Por lo que lo primero que veremos es como usamos la función connect
en la línea 76, veamos que como primer parámetro le hemos mandando la
función mapStateToProps, de la cual ya hemos hablado antes, pero la
analizaremos un poco más adelante nuevamente.

El segundo parámetro lo creamos para mapear las funciones dispatcher a las


propiedades. De esta forma, es posible utilizar las funciones definidas en
Actions.js pero referenciadas por medio de propiedades. En este caso, estamos
mapeando la función relogin para tenerla disponible en this.props.relogin().

Ahora bien, regresando un poco a la función mapStateToProps, esta función tiene


como propósito mapear el estado del store a props del componente, de esta
forma, podemos mapear solamente las partes del estado que nos interesa. El
parámetro state es el estado completo del store, por lo que tenemos que conocer
su estructura para poder acceder solo a las partes que nos interesa. Vamos
analizar su estructura más adelante, pero por ahora, podrás observar que
utilizamos el nombre del reducer.

Dentro de la función render, es muy importante notar que hemos cambiado todas
las dependencias del estado (this.state) por las nuevas propiedades definidas
en la mapStateToProps. Recuerda que con Redux, el estado ahora vivirá en el
Store.

En la función componentWillMount, verás que hemos remplazado todo el código


por únicamente la llamada a la función relogin, la cual se mapea a la propiedad
this.props.relogin que está definida como el segundo parámetro de connect().

Finalmente, eliminamos la inicialización del estado en el constructor e


importamos las funciones connect y relogin.

Crear el Store de la aplicación

El único paso que resta pare terminar de implementar Redux es crear nuestro
store, para ello, será necesario regresar al archivo App.js y realizar las siguientes
modificaciones:

1. import React from 'react'


2. import { render } from 'react-dom'
3. import TwitterApp from './TwitterApp'
4. import Signup from './Signup'
5. import Login from './Login'
6. import UserPage from './UserPage'
7. import MyTweets from './MyTweets'

313 | Página
8. import Followings from './Followings'
9. import Followers from './Followers'
10. import TweetDetail from './TweetDetail'
11. import { Router, Route, browserHistory, IndexRoute } from "react-router"
12. import { createStore, applyMiddleware } from 'redux'
13. import { Provider } from 'react-redux'
14. import thunk from 'redux-thunk'
15. import { createLogger } from 'redux-logger'
16. import reducers from './reducers'
17.
18. const middleware = [ thunk ];
19. if (process.env.NODE_ENV !== 'production') {
20. middleware.push(createLogger());
21. }
22.
23. export const store = createStore(
24. reducers,
25. applyMiddleware(...middleware)
26. )
27.
28. render((
29. <Provider store={ store }>
30. <Router history={ browserHistory }>
31. <Route path="/" component={TwitterApp} >
32. <Route path="signup" component={Signup}/>
33. <Route path="login" component={Login}/>
34.
35. <Route path=":user" component={UserPage} >
36. <IndexRoute component={MyTweets} tab="tweets" />
37. <Route path="followers" component={Followers} tab="followers"/>
38. <Route path="following" component={Followings} tab="followings"/>
39. <Route path=":tweet" component={TweetDetail}/>
40. </Route>
41. </Route>
42. </Router>
43. </Provider>
44. ), document.getElementById('root'));

Lo primero que agregaremos son todos los imports necesario para hacer
funcionar Redux.

En las líneas 18 a 21 hemos agregado el middleware thunk y redux-logger con la


finalidad de depurar nuestra aplicación en tiempo de ejecución. Solo se agregará
cuando no estamos en producción.

Lo siguiente es crear nuestro store. Fíjate que lo hemos creado a partir del
reducer, el cual no es más que el combineReducers que hemos creado en el
archivo index.js. Nota que no es necesario importar con todo y el nombre, pues
al llamarse index.js se asume, por lo que sería lo mismo que:

1. import reducers from './reducers/index.js'

Finalmente, y para terminar, será necesario agregar el componente Provider


(línea 29) como root de la función render, al cual tendremos que mandarle como
parámetro el store. Provider es un componente de orden superior que tiene como
finalidad vincular React con Redux.

Página | 314
Comprobando la funcionalidad de Redux.

Si hemos realizado todo bien hasta ahora, la aplicación ya debería estar


funcionando con Redux, al menos parcialmente, porque solo hemos migrado un
componente. Para validar que todo está bien, solo basta con entrar a la aplicación
y ver la consola:

Fig. 146 - Redux logger

En la imagen podemos ver log de la aplicación, en el cual se aprecia el log de


Redux. Lo que podemos ver, es que se lanzó el action LOGIN_SUCCESS cuando se
mandó llamar la función relogin.

315 | Página
Fig. 147 - Detalle del log

En esta nueva imagen podemos apreciar el estado anterior, el action y el nuevo


estado. Veamos que el estado anterior, tenía la propiedad load en false y el
profile en null. Luego, el action tiene el objeto profile con todos los datos del
usuario, aunque en la imagen solo se aprecia el ID. Finalmente, vemos el nuevo
estado, el cual tiene la propiedad load en true y la propiedad perfil con los datos
del usuario.

Migrando el proyecto Mini Twitter a Redux

Una vez que hemos migrado nuestro primer componente a Redux, ya podemos
iniciar la migración de todo el proyecto, por lo que a partir de esta sección nos
dedicaremos solo a migrar los componentes a Redux.

El orden con el que migramos los componentes no ha sido aleatorio, pues hemos
pensando que cada componente que migremos pueda ser probado de inmediato
y que la aplicación siga funcionando, por lo que, al terminar de actualizar cada
componente, podrás simplemente actualizar el navegador y ver los resultados.

Si al actualizar el componente la aplicación falla, deberás analizar bien los


cambios realizados, incluso compararlos con el repositorio de GitHub.

Refactorizando el componente Login

Página | 316
El componente Login es un poco diferente al caso del componente TwitterApp,
esto debido a que tienes un formulario, por lo que cada vez que el usuario captura
un dato, será necesario actualizar el store, para que este a su vez actualice la
vista.

Iniciaremos la creación de las funciones dispatcher y con los actions, pues están
en el mismo archivo, por lo que agregaremos las siguientes líneas al archivo
Actions.js.

5. export const updateLoginForm = (field, value) => (dispatch, getState) => {


6. dispatch(updateLoginFormRequest(field,value))
7. }
8.
9. export const loginRequest = () => (dispatch, getState) => {
10.
11. let credential = {
12. username: getState().loginFormReducer.username,
13. password: getState().loginFormReducer.password
14. }
15.
16. APIInvoker.invokePOST('/login',credential, response => {
17. window.localStorage.setItem("token", response.token)
18. window.localStorage.setItem("username", response.profile.userName)
19. window.location = '/';
20. },error => {
21. dispatch(loginFailForm(error.message))
22. })
23. }
24.
25. const updateLoginFormRequest = (field, value) => ({
26. type: UPDATE_LOGIN_FORM_REQUEST,
27. field: field,
28. value: value
29. })
30.
31. const loginFailForm = (loginMessage) => ({
32. type: LOGIN_ERROR,
33. loginMessage: loginMessage
34. })

Iniciaremos explicando los actions, el primero (updateLoginFormRequest) se


ejecutará cada vez que el usuario presione una tecla sobre los controles del
formulario, el segundo (loginFailForm) solo se ejecutará si la autenticación fallo.

Con respecto a las funciones de dispatcher, la primera (updateLoginForm)


disparará el action updateLoginFormRequest para actualizar el estado a medida
que el usuario interactúa con el formulario. Por otro lado, la segunda función
(loginRequest) es la que se encarga de autenticar al usuario desde el API. Si la
autenticación sale bien, solo es necesario guardar el token y usuario en la sesión
y redirigir al usuario al inicio de la aplicación, con esto, la aplicación intentará
autenticar de forma automática al usuario.

También será necesario agregar los import de las nuevas constantes utilizadas:

1. import {
2. LOGIN_SUCCESS,

317 | Página
3. LOGIN_ERROR,
4. UPDATE_LOGIN_FORM_REQUEST
5. } from './const'

Constantes

Debido a que estamos utilizando nuevas constantes, será necesarios agregarlas


en el archivo const.js:

1. export const LOGIN_ERROR = 'LOGIN_ERROR'


2. export const UPDATE_LOGIN_FORM_REQUEST = "UPDATE_LOGIN_FORM_REQUEST"

Reducer

Vamos a agregar un nuevo reducer que nos ayude a gestionar solo el componente
de Login, por lo cual, crearemos un nuevo archivo llamado LoginFormReducer.js
en el path /app/reducers, el cual tendrá el siguiente contenido:

1. import {
2. UPDATE_LOGIN_FORM_REQUEST,
3. LOGIN_ERROR
4. } from '../actions/const'
5. import update from 'react-addons-update'
6.
7.
8. const initialState = {
9. username: "",
10. password: "",
11. loginError: false,
12. loginMessage: null
13. }
14.
15.
16. export const loginFormReducer = (state = initialState, action) => {
17. switch (action.type) {
18. case UPDATE_LOGIN_FORM_REQUEST:
19. if(action.field === 'username'){
20. let value = action.value.replace(' ','').replace('@','').substring(0, 15)

21. return update(state,{


22. [action.field] : {$set: action.value}
23. })
24. }else{
25. return update(state, {
26. [action.field]: {$set: action.value}
27. })
28. }
29. case LOGIN_ERROR:
30. return update(state,{
31. loginError: {$set: true},
32. loginMessage: {$set: action.loginMessage}
33. })
34. default:
35. return state
36. }
37. }
38.
39. export default loginFormReducer

Página | 318
Este reducer solo puede procesar dos tipos de actions, el
UPDATE_LOGIN_FORM_REQUEST y el LOGIN_ERRROR, el primero se dispara con cada
tecla capturada por el usuario en alguno de los controles y solo actualiza la
propiedad del campo afectado, con excepción del username, en el cual se hace
dos cosas adicionales, quitar la @ y truncar a 15 caracteres. El segundo type, se
ejecuta solo cuando hay algún error al autenticar al usuario, por lo que se
actualiza el estado con el mensaje de error.

Agregamos el nuevo reducers al archivo index.js, dejándolo de la siguiente


manera:

1. import { combineReducers } from 'redux'


2. import loginReducer from './LoginReducer'
3. import loginFormReducer from './LoginFormReducer'
4.
5. export default combineReducers({
6. loginReducer,
7. loginFormReducer
8. })

En este momento es cuando realmente le sacamos provecho a combineReducer.

Migrando el Login Component

El componente Login puede resultar un poco confuso, ya que los controles del
formulario que está ligado al estado. En estos casos, tendremos que ligar los
controles a los props mapeados en mapStateProps y actualizar el estado por medio
de actions.

1. import React from 'react'


2. import update from 'react-addons-update'
3. import APIInvoker from './utils/APIInvoker'
4. import { Link, Router } from 'react-router';
5. import { connect } from 'react-redux'
6. import { updateLoginForm, loginRequest } from './actions/Actions'
7.
8.
9. class Login extends React.Component{
10.
11. constructor(){
12. super(...arguments)
13. this.state = {
14. username: "",
15. password: ""
16. }
17. }
18.
19. handleInput(e){
20. this.props.updateLoginForm(e.target.name, e.target.value)
21.
22. let field = e.target.name
23. let value = e.target.value
24.
25. if(field === 'username'){
26. valuevalue = value.replace(' ','').replace('@','').substring(0, 15)
27. this.setState(update(this.state,{
28. [field] : {$set: value}

319 | Página
29. }))
30. }
31.
32. this.setState(update(this.state,{
33. [field] : {$set: value}
34. }))
35. }
36.
37. login(e){
38. e.preventDefault()
39. this.props.loginRequest()
40.
41. let request = {
42. "username": this.state.username,
43. "password": this.state.password
44. }
45.
46. APIInvoker.invokePOST('/login',request, response => {
47. window.localStorage.setItem("token", response.token)
48. window.localStorage.setItem("username", response.profile.userName)
49. window.location = '/'
50. },error => {
51. this.refs.submitBtnLabel.innerHTML = error.message
52. this.refs.submitBtnLabel.className = 'shake animated'
53. console.log("Error en la autenticación")
54. })
55. }
56.
57. render(){
58.
59. return(
60. <div id="signup">
61. <div className="container" >
62. <div className="row">
63. <div className="col-xs-12">
64. </div>
65. </div>
66. </div>
67. <div className="signup-form">
68. <form onSubmit={this.login.bind(this)}>
69. <h1>Iniciar sesión en Twitter</h1>
70.
71. <input type="text" value={this.props.username}
72. placeholder="usuario" name="username" id="username"
73. onChange={this.handleInput.bind(this)}/>
74. <label ref="usernameLabel" id="usernameLabel"
75. htmlFor="username"></label>
76.
77. <input type="password" id="passwordLabel"
78. value={this.props.password} placeholder="Contraseña"
79. name="password" onChange={this.handleInput.bind(this)}/>
80. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
81.
82. <button className="btn btn-primary btn-lg " id="submitBtn"
83. onClick={this.login.bind(this)}>Regístrate</button>
84. <If condition={this.props.state.loginError}>
85. <label ref="submitBtnLabel" id="submitBtnLabel"
86. htmlFor="submitBtn"
87. className="shake animated hidden ">
88. {this.props.state.loginMessage}</label>
89. </If>
90. <p className="bg-danger user-test">Crea un usuario o usa el usuario
91. <strong>test/test</strong></p>
92. <p>¿No tienes una cuenta? <Link to="/signup">Registrate</Link> </p>
93. </form>
94. </div>

Página | 320
95. </div>
96. )
97. }
98. }
99.
100. const mapStateToProps = (state) => {
101. return {
102. state: {
103. username: state.loginFormReducer.username,
104. password: state.loginFormReducer.password,
105. loginError: state.loginFormReducer.loginError,
106. loginMessage: state.loginFormReducer.loginMessage
107. }
108. }
109. }
110.
111. export default connect (mapStateToProps,
112. {updateLoginForm, loginRequest})(Login)

Podemos ver la misma fórmula que usamos en TwitterApp, es decir, conectamos


el componente con Redux mediante connect(), mapeamos el estado del store
con la función mapStateToProps, importamos las funciones dispatcher
(updateLoginForm y loginRequest) de Actions.js y actualizamos las
dependencias del estado por props en la función render.

Observemos que hemos eliminado nuevamente toda la funcionalidad de la


función login y en su lugar, llamamos a la función loginRequest que definimos
en el archivo Actions.js. con la función handleInput pasa exactamente lo mismo.

Refactorizando el componente Signup

La migración del componente Signup es muy similar al componente Login, por lo


que nos iremos más rápido. Iniciaremos agregando los Actions y las funciones
dispatcher en el archivo Actions.js:

1. export const updateSignupForm = (field,value) => (dispatch, getState) => {


2. dispatch(updateSignupFormRequest(field,value))
3. }
4.
5. export const validateUser = (username) => (dispatch, getState) => {
6. if(username.trim() === ''){
7. return
8. }
9. APIInvoker.invokeGET('/usernameValidate/' + username, response => {
10. dispatch(validateUserRequest(response.ok, response.message))
11. },error => {
12. dispatch(validateUserRequest(error.ok, error.message))
13. })
14. }
15.
16. export const signup = () => (dispatch, getState) => {
17. let currentState = getState().signupFormReducer
18. if(!currentState.license){
19. dispatch(signupResultFail('Acepte los términos de licencia'))
20. }else if(!currentState.userOk){
21. dispatch(signupResultFail('Favor de revisar su nombre de usuario'))

321 | Página
22. }else{
23. let request = {
24. "name": currentState.name,
25. "username": currentState.username,
26. "password": currentState.password
27. }
28.
29. APIInvoker.invokePOST('/signup',request, response => {
30. browserHistory.push('/login');
31. },error => {
32. dispatch(signupResultFail(error.error))
33. })
34. }
35. }
36.
37. const updateSignupFormRequest = (field,value,fieldType) => ({
38. type: UPDATE_SIGNUP_FORM_REQUEST,
39. field: field,
40. value: value,
41. fieldType: fieldType
42. })
43.
44. const validateUserRequest = (userOk, userOkMessage) => ({
45. type: VALIDATE_USER_RESPONSE,
46. userOk: userOk,
47. userOkMessage: userOkMessage
48. })
49.
50. const signupResultFail = (signupFailMessage) => ({
51. type: SIGNUP_RESULT_FAIL,
52. signupFail: true,
53. signupFailMessage: signupFailMessage
54. })

La función updateSignupForm es la función que utilizaremos para actualizar el


store cuando el usuario captura algún dato en cualquier control del formulario y
se apoya del action updateSignupFormRequet para solicitar los cambios al store.

La función validateUser es la función que nos ayuda a validar si el nombre de


usuario está disponible, para esto, se apoya del action validateUserRequest.

Finalmente, la función sigup es la utilizada para crear un nuevo usuario, para


esto, se apoya del action signupResultFail que se dispara solo cuando existe
un error al crear el usuario.

No olvidemos importar las nuevas constantes:

1. import {
2. LOGIN_SUCCESS,
3. LOGIN_ERROR,
4. UPDATE_LOGIN_FORM_REQUEST,
5. SIGNUP_RESULT_FAIL,
6. VALIDATE_USER_RESPONSE,
7. UPDATE_SIGNUP_FORM_REQUEST
8. } from './const'

Constantes

Página | 322
Agregaremos las siguientes constantes al archivo const.js:

1. export const SIGNUP_RESULT_FAIL = 'SIGNUP_RESULT_FAIL'


2. export const VALIDATE_USER_RESPONSE = 'VALIDATE_USER_RESPONSE'
3. export const UPDATE_SIGNUP_FORM_REQUEST = 'UPDATE_SIGNUP_FORM_REQUEST'

Reducer

Deberemos agregar un nuevo reducer para controlar las acciones del


componente Signup, para lo cual, crearemos el archivo SignupFormReducer.js en
el path /app/reducers:

1. import {
2. UPDATE_SIGNUP_FORM_REQUEST,
3. VALIDATE_USER_RESPONSE,
4. SIGNUP_RESULT_FAIL
5. } from '../actions/const'
6. import update from 'react-addons-update'
7.
8. const initialState = {
9. username: "",
10. name: "",
11. password: "",
12. license: false,
13. userOk: false,
14. userOkMessage: null,
15. signupFail: false,
16. signupFailMessage: null
17. }
18.
19.
20. export const signupFormReducer = (state = initialState, action) => {
21. switch (action.type) {
22. case UPDATE_SIGNUP_FORM_REQUEST:
23. return update(state, {
24. [action.field]: {$set: action.value}
25. })
26. case VALIDATE_USER_RESPONSE:
27. return update(state,{
28. userOk: {$set: action.userOk},
29. userOkMessage: {$set: action.userOkMessage}
30. })
31. case SIGNUP_RESULT_FAIL:
32. return update(state, {
33. signupFail: {$set: action.signupFail},
34. signupFailMessage: {$set: action.signupFailMessage}
35. })
36. default:
37. return state
38. }
39. }
40.
41. export default signupFormReducer

Definimos el estado inicial (línea 8) y creamos la función signupFormReducer, la


cual funcionara como el reducer. El reducer atiende tres tipos de actions,
UPDATE_SIGNUP_REQUEST (control del formulario), VALIDATE_USER_RESPONSE
(Validación del nombre de usuario único), SIGNUP_RESULT_FAIL (Error al crear el
usuario).

323 | Página
CombineReduces

Actualizamos el archivo index.js para agregar el nuevo reducer que hemos


creado:

1. import { combineReducers } from 'redux'


2. import loginReducer from './LoginReducer'
3. import loginFormReducer from './LoginFormReducer'
4. import signupFormReducer from './SignupFormReducer'
5.
6. export default combineReducers({
7. loginReducer,
8. loginFormReducer,
9. signupFormReducer
10. })

Migrar el componente Signup

Para migrar el componente deberemos aplicar los siguientes cambios:

1. import React from 'react'


2. import update from 'react-addons-update'
3. import APIInvoker from './utils/APIInvoker'
4. import { Link } from 'react-router'
5. import {updateSignupForm, validateUser, signup} from './actions/Actions'
6. import {connect} from 'react-redux'
7.
8. class Signup extends React.Component{
9.
10. constructor(){
11. super(...arguments)
12. this.state = {
13. username: "",
14. name:"",
15. password: "",
16. userOk: false,
17. license: false
18. }
19. }
20.
21. handleInput(e){
22. let field = e.target.name
23. let value = e.target.value
24. let type = e.target.type
25.
26. if(field === 'username'){
27. valuevalue = value.replace(' ','').replace('@','').substring(0, 15)
28. }else if(type === 'checkbox'){
29. value = e.target.checked
30. }
31.
32. this.props.updateSignupForm(field,value)
33.
34. if(field === 'username'){
35. value = value.replace(' ','').replace('@','').substring(0, 15)
36. this.setState(update(this.state,{
37. [field] : {$set: value}
38. }))
39. }else if(type === 'checkbox'){
40. this.setState(update(this.state,{

Página | 324
41. [field] : {$set: e.target.checked}
42. }))
43.
44. }else{
45. this.setState(update(this.state,{
46. [field] : {$set: value}
47. }))
48. }
49. }
50.
51. validateUser(e){
52. let username = e.target.value
53. this.props.validateUser(username)
54.
55. APIInvoker.invokeGET('/usernameValidate/' + username, response => {
56. this.setState(update(this.state, {
57. userOk: {$set: true}
58. }))
59. this.refs.usernameLabel.innerHTML = response.message
60. this.refs.usernameLabel.className = 'fadeIn animated ok'
61. },error => {
62. console.log("Error al cargar los Tweets");
63. this.setState(update(this.state,{
64. userOk: {$set: false}
65. }))
66. this.refs.usernameLabel.innerHTML = error.message
67. this.refs.usernameLabel.className = 'fadeIn animated fail'
68. })
69. }
70.
71.
72. signup(e){
73. e.preventDefault()
74. this.props.signup()
75.
76. if(!this.state.license){
77. this.refs.submitBtnLabel.innerHTML =
78. 'Acepte los términos de licencia'
79. this.refs.submitBtnLabel.className = 'shake animated'
80. return
81. }else if(!this.state.userOk){
82. this.refs.submitBtnLabel.innerHTML =
83. 'Favor de revisar su nombre de usuario'
84. this.refs.submitBtnLabel.className = ''
85. return
86. }
87.
88. this.refs.submitBtnLabel.innerHTML = ''
89. this.refs.submitBtnLabel.className = ''
90.
91. let request = {
92. "name": this.state.name,
93. "username": this.state.username,
94. "password": this.state.password
95. }
96.
97. APIInvoker.invokePOST('/signup',request, response => {
98. browserHistory.push('/login');
99. alert('Usuario registrado correctamente');
100. },error => {
101. console.log("Error al cargar los Tweets");
102. this.refs.submitBtnLabel.innerHTML = response.error
103. this.refs.submitBtnLabel.className = 'shake animated'
104. })
105. }
106.

325 | Página
107. render(){
108.
109. return (
110. <div id="signup">
111. <div className="container" >
112. <div className="row">
113. <div className="col-xs-12">
114.
115. </div>
116. </div>
117. </div>
118. <div className="signup-form">
119. <form onSubmit={this.signup.bind(this)}>
120. <h1>Únite hoy a Twitter</h1>
121. <input type="text" value={this.props.state.username}
122. placeholder="@usuario" name="username" id="username"
123. onBlur={this.validateUser.bind(this)}
124. onChange={this.handleInput.bind(this)}/>
125. <If condition={!this.props.state.userOk
126. && this.props.state.userOkMessage !== null}>
127. <label ref="usernameLabel" id="usernameLabel"
128. className={this.props.state.userOk ? 'fadeIn animated ok' :
129. 'fadeIn animated fail'} htmlFor="username">
130. {this.props.state.userOkMessage}</label>
131. </If>
132.
133. <input type="text" value={this.props.state.name}
134. placeholder="Nombre"
135. name="name" id="name"
136. onChange={this.handleInput.bind(this)}/>
137. <label ref="nameLabel" id="nameLabel" htmlFor="name"></label>
138.
139. <input type="password" id="passwordLabel"
140. value={this.props.state.password} placeholder="Contraseña"
141. name="password" onChange={this.handleInput.bind(this)}/>
142. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
143.
144. <input id="license" type="checkbox" ref="license"
145. value={this.props.state.license} name="license"
146. onChange={this.handleInput.bind(this)} />
147. <label htmlFor="license" >Acepto los terminos de licencia</label>
148.
149. <button className="btn btn-primary btn-lg " id="submitBtn"
150. onClick={this.signup.bind(this)}>Regístrate</button>
151. <If condition ={this.props.state.signupFailMessage !== null}>
152. <label ref="submitBtnLabel"
153. id="submitBtnLabel" htmlFor="submitBtn"
154. className="shake animated">
155. {this.props.state.signupFailMessage}</label>
156. </If>
157. <p className="bg-danger user-test">
158. Crea un usuario o usa el usuario
159. <strong>test/test</strong></p>
160. <p>¿Ya tienes cuenta? <Link to="/login">
161. Iniciar sesión</Link> </p>
162. </form>
163. </div>
164. </div>
165. )
166. }
167. }
168.
169. const mapStateToProps = (state) => {
170. return {
171. state: state.signupFormReducer
172. }

Página | 326
173. }
174.
175. export default connect(mapStateToProps,
176. {updateSignupForm, validateUser, signup})(Signup);

Los cambios que podemos observar son los de siempre, agregar los imports,
pasar la lógica de las funciones Actions.js, remplazar las referencias al estado
por props y conectar el componente a Reduce mediante connect().

Refactorizando el componente TweetContainer

Como es costumbre, iniciaremos con los cambios al archivo Actions.js:

1. export const getTweet = (username, onlyUserTweet) =>


2. (dispatch, getState) => {
3. APIInvoker.invokeGET('/tweets' + (onlyUserTweet ? "/" + username : ""),
4. response => {
5. dispatch(loadTweetsSuccess(response.body))
6. },error => {
7. console.log("Error al cargar los Tweets");
8. })
9. }
10.
11. export const addNewTweet = (newTweet) => (dispatch, getState) => {
12. APIInvoker.invokePOST('/secure/tweet',newTweet, response => {
13. newTweet._id = response.tweet._id
14. let newState = update(getState().tweetsReducer, {
15. tweets: {$splice: [[0, 0, newTweet]]}
16. })
17.
18. dispatch(addNewTweetSuccess(newState.tweets))
19. },error => {
20. console.log("Error al cargar los Tweets");
21. })
22. }
23.
24. const loadTweetsSuccess = tweets => ({
25. type: LOAD_TWEETS,
26. tweets: tweets
27. })
28.
29. const addNewTweetSuccess = tweets => ({
30. type: ADD_NEW_TWEET_SUCCESS,
31. tweets: tweets
32. })

La función getTweet es la encargada de recuperar los Tweets desde el API y se


apoya del action loadTweetsSuccess para actualizar el estado.

La función addNewTweet nos servirá para agregar un nuevo Tweet desde el


componte Reply. Recordemos que este componente recibe como prop la función
para guardar el Tweet en el API. Cuando un nuevo Tweet es añadido el action
addNewTweetSuccess es disparado para actualizar el store y reflejar el Tweet en
el componente TweetsContainer.

No hay que olvidar importar las nuevas constantes

327 | Página
1. import {
2. LOGIN_SUCCESS,
3. LOGIN_ERROR,
4. UPDATE_LOGIN_FORM_REQUEST,
5. SIGNUP_RESULT_FAIL,
6. VALIDATE_USER_RESPONSE,
7. UPDATE_SIGNUP_FORM_REQUEST,
8. ADD_NEW_TWEET_SUCCESS,
9. LOAD_TWEETS
10. } from './const'

Constantes

Continuaremos agregando las nuevas constantes:

1. export const LOAD_TWEETS = 'LOAD_TWEETS'


2. export const ADD_NEW_TWEET_SUCCESS = 'ADD_NEW_TWEET_SUCCESS'

Reducer

El siguiente paso será crear el archivo TweetReducer.js en el path /app/reducers:

1. import {
2. LOAD_TWEETS,
3. ADD_NEW_TWEET_SUCCESS,
4. CLEAR_TWEETS,
5. LIKE_TWEET_REQUEST
6. } from '../actions/const'
7. import update from 'react-addons-update'
8.
9. const initialState = {
10. tweets: []
11. }
12.
13. export const tweetsReducer = (state = initialState, action) => {
14. switch (action.type) {
15. case LOAD_TWEETS:
16. return {
17. tweets: action.tweets
18. }
19. case ADD_NEW_TWEET_SUCCESS:
20. return {
21. tweets: action.tweets
22. }
23. default:
24. return state
25.
26. }
27. }
28.
29. export default tweetsReducer

En este reducer no hay mucho ver, ya que tan solo se actualiza el atributo tweets
con los valores obtenidos por el action.

Combine Reduces

Página | 328
Actualizaremos el archivo index.js para agregar el nuevo reducer:

1. import { combineReducers } from 'redux'


2. import loginReducer from './LoginReducer'
3. import loginFormReducer from './LoginFormReducer'
4. import signupFormReducer from './SignupFormReducer'
5. import tweetsReducer from './TweetReducer'
6.
7. export default combineReducers({
8. loginReducer,
9. loginFormReducer,
10. signupFormReducer,
11. tweetsReducer
12. })

Migrar el componente TweetsContainer:

1. import React from 'react'


2. import Tweet from './Tweet'
3. import Reply from './Reply'
4. import update from 'react-addons-update'
5. import APIInvoker from "./utils/APIInvoker"
6. import PropTypes from 'prop-types'
7. import { connect } from 'react-redux'
8. import { getTweet, addNewTweet } from './actions/Actions'
9.
10. class TweetsContainer extends React.Component{
11. constructor(props){
12. super(props)
13. this.state = {
14. tweets: []
15. }
16. }
17.
18. componentDidUpdate(prevProps, prevState) {
19. if(prevProps.profile.userName !== this.props.profile.userName){
20. let username = this.props.profile.userName
21. let onlyUserTweet = this.props.onlyUserTweet
22. this.loadTweets(username, onlyUserTweet)
23. this.props.getTweet(username, onlyUserTweet)
24. }
25. }
26.
27. componentWillMount(){
28. let username = this.props.profile.userName
29. let onlyUserTweet = this.props.onlyUserTweet
30.
31. this.loadTweets(username, onlyUserTweet)
32. this.props.getTweet(username, onlyUserTweet)
33. }
34.
35. loadTweets(username, onlyUserTweet){
36. let url = '/tweets' + (onlyUserTweet ? "/" + username : "")
37. APIInvoker.invokeGET(url, response => {
38. this.setState({
39. tweets: response.body
40. })
41. },error => {
42. console.log("Error al cargar los Tweets", error);
43. })
44. }
45.
46. addNewTweet(newTweet){
47. this.props.addNewTweet(newTweet)

329 | Página
48.
49. let oldState = this.state;
50. let newState = update(this.state, {
51. tweets: {$splice: [[0, 0, newTweet]]}
52. })
53.
54. this.setState(newState)
55.
56. Optimistic Update
57. APIInvoker.invokePOST('/secure/tweet',newTweet, response => {
58. this.setState(update(this.state,{
59. tweets:{
60. 0 : {
61. _id: {$set: response.tweet._id}
62. }
63. }
64. }))
65. },error => {
66. console.log("Error al cargar los Tweets");
67. this.setState(oldState)
68. })
69. }
70.
71. render(){
72.
73. let operations = {
74. addNewTweet: this.addNewTweet.bind(this)
75. }
76.
77. return (
78. <main className="twitter-panel">
79. <Choose>
80. <When condition={this.props.state.onlyUserTweet} >
81. <div className="tweet-container-header">
82. Tweets
83. </div>
84. </When>
85. <Otherwise>
86. <Reply operations={operations}/>
87. </Otherwise>
88. </Choose>
89. <If condition={this.props.state.tweets != null}>
90. <For each="tweet" of={this.props.state.tweets}>
91. <Tweet key={tweet._id} tweet={tweet}/>
92. </For>
93. </If>
94. </main>
95. )
96. }
97. }
98.
99. TweetsContainer.propTypes = {
100. onlyUserTweet: PropTypes.bool
101. profile: PropTypes.object
102. }
103.
104. TweetsContainer.defaultProps = {
105. onlyUserTweet: false
106. profile: {
107. userName: ""
108. }
109. }
110.
111. const mapStateToProps = (state) => {
112. return {
113. state: {

Página | 330
114. Profile: state.userPageReducer.profile,
115. tweets: state.tweetsReducer.tweets
116. }
117. }
118. }
119.
120. export default connect(mapStateToProps,
121. {getTweet, addNewTweet})(TweetsContainer);

Refactorizando el componente Tweet

El componente Tweet es el encargado de representar un Tweet en pantalla. Este


componente tiene una ligera diferencia al resto de componentes que hemos
analizado, pues este no requiere del estado del store, pues los datos a mostrar
son enviados como props el momento de la creación por parte del componente
padre. Sin embargo, si realizar operaciones que actualizan el estado.

1. //Tweet Component
2. export const likeTweet = (tweetId, like) => (dispatch, getState) => {
3. let request = {
4. tweetID: tweetId,
5. like: like
6. }
7.
8. APIInvoker.invokePOST('/secure/like', request, response => {
9. dispatch(likeTweetRequest(tweetId, response.body.likeCounter))
10. },error => {
11. console.log("Error al cargar los Tweets");
12. })
13. }
14.
15. export const likeTweetDetail = (tweetId, like) => (dispatch, getState) => {
16. let request = {
17. tweetID: tweetId,
18. like: like
19. }
20.
21. APIInvoker.invokePOST('/secure/like', request, response => {
22. dispatch(likeTweetDetailRequest(tweetId, response.body.likeCounter))
23. },error => {
24. console.log("Error al cargar los Tweets");
25. })
26. }
27.
28. const likeTweetRequest = (tweetId, likeCounter) => ({
29. type: LIKE_TWEET_REQUEST,
30. tweetId: tweetId,
31. likeCounter: likeCounter
32. })
33.
34. const likeTweetDetailRequest = (tweetId, likeCounter) => ({
35. type: LIKE_TWEET_DETAIL_REQUEST,
36. tweetId: tweetId,
37. likeCounter: likeCounter
38. })

Podemos observar que tenemos dos funciones dispatcher que hacen


exactamente lo mismo (likeTweet y likeTweetDetail). Ambas funciones tienen
como finalidad darle like a un Tweet, aumentando el contador de likes. Sin

331 | Página
embargo, hay una ligera diferencia en su funcionamiento, la primera utiliza el
action likeTweetRequest y la segunda el action likeTweetDetailRequest, los
cuales son procesados por dos reducers diferentes, lo que hace que actualicen
una sección diferente del estado.

La función likeTweet será procesado por el reducer TweetReducer, mientras que


la función likeTweetDetail es procesado por el reducer TweetDetailReducer.
Debido a esto, la sección que afectan del estado es diferente.

También deberemos agregar los imports a las nuevas constantes utilizadas:

1. import {
2. LOGIN_SUCCESS,
3. LOGIN_ERROR,
4. UPDATE_LOGIN_FORM_REQUEST,
5. SIGNUP_RESULT_FAIL,
6. VALIDATE_USER_RESPONSE,
7. UPDATE_SIGNUP_FORM_REQUEST,
8. ADD_NEW_TWEET_SUCCESS,
9. LOAD_TWEETS,
10. LIKE_TWEET_REQUEST,
11. LIKE_TWEET_DETAIL_REQUEST
12. } from './const'

Constantes

No olvidemos agregar las nuevas constantes al archivo const.js:

1. export const LIKE_TWEET_REQUEST = 'LIKE_TWEET_REQUEST'


2. export const LIKE_TWEET_DETAIL_REQUEST = 'LIKE_TWEET_DETAIL_REQUEST'

Reducers

En este componente se ven involucrados dos reducers, el TweetReducer y


TweetDetailReducer. De momento solo utilizaremos en TweetReducer, pues es el
que procesa las solicitudes para dar like a un Tweet. Cuando pasamos a migrar
el componente TweetDetail es cuando necesitaremos el segundo reducer, sin
embargo, para ir más rápido y no demorarnos mucho tiempo, vamos a dejar los
dos reducer listos.

Primero vamos a actualizar el reducer TweetReducer para controlar el nuevo type


(LIKE_TWEET_REQUEST):

1. import {
2. LOAD_TWEETS,
3. ADD_NEW_TWEET_SUCCESS,
4. CLEAR_TWEETS,

Página | 332
5. LIKE_TWEET_REQUEST
6. } from '../actions/const'
7. import update from 'react-addons-update'
8.
9. const initialState = {
10. tweets: []
11. }
12.
13. export const tweetsReducer = (state = initialState, action) => {
14. switch (action.type) {
15. case LOAD_TWEETS:
16. return {
17. tweets: action.tweets
18. }
19. case ADD_NEW_TWEET_SUCCESS:
20. return {
21. tweets: action.tweets
22. }
23. case LIKE_TWEET_REQUEST:
24. let targetIndex =
25. state.tweets.map( x => {return x._id}).indexOf(action.tweetId)
26. return update(state, {
27. tweets: {
28. [targetIndex]: {
29. likeCounter : {$set: action.likeCounter},
30. liked: {$apply: (x) => {return !x}}
31. }
32. }
33. })
34. default:
35. return state
36. }
37. }
38.
39. export default tweetsReducer

Este nuevo type solo actualiza el valor de la propiedad liked, para reflejar si el
usuario dio like o dislike.

Por otra parte, vamos a agregar un nuevo reducer llamado


TweetDetailReducer.js que nos ayudará con el componente Tweet y TweetDetail.

1. import {
2. LOAD_TWEET_DETAIL,
3. ADD_NEW_TWEET_REPLY,
4. LIKE_TWEET_DETAIL_REQUEST
5. } from '../actions/const'
6. import update from 'react-addons-update'
7.
8. let initialState = null
9.
10. export const tweetDetailReducer = (state = initialState, action) => {
11. switch (action.type) {
12. case LIKE_TWEET_DETAIL_REQUEST:
13. if(state._id === action.tweetId){
14. return update(state,{
15. likeCounter : {$set: action.likeCounter},
16. liked: {$apply: (x) => {return !x}}
17. })
18. }else{
19. let targetIndex =
20. state.replysTweets.map( x => {return x._id}).indexOf(action.tweetId)
21. return update(state, {
22. replysTweets: {

333 | Página
23. [targetIndex]: {
24. likeCounter : {$set: action.likeCounter},
25. liked: {$apply: (x) => {return !x}}
26. }
27. }
28. })
29. }
30. default:
31. return state
32. }
33. }
34.
35. export default tweetDetailReducer

Este reducer será ejecutado solo cuando se de like desde el componente Reply
pero cuando este dentro de TweetReply. La diferencia fundamental es que permite
darle like al Tweet principal (línea 13) o a sus hijos (Tweet de respuesta), línea
18.

Combine Reducers

Ahora agregaremos el nuevo reducer al combineReducers:

1. import { combineReducers } from 'redux'


2. import loginReducer from './LoginReducer'
3. import loginFormReducer from './LoginFormReducer'
4. import signupFormReducer from './SignupFormReducer'
5. import tweetsReducer from './TweetReducer'
6. import tweetDetailReducer from './TweetDetailReducer'
7.
8. export default combineReducers({
9. loginReducer,
10. loginFormReducer,
11. signupFormReducer,
12. tweetsReducer,
13. tweetDetailReducer
14. })

Migrando el componente Tweet

Para la migración del componente Tweet seguiremos los pasos de siempre.

1. import React from 'react'


2. import update from 'react-addons-update'
3. import { Link } from 'react-router'
4. import { browserHistory } from 'react-router'
5. import APIInvoker from './utils/APIInvoker'
6. import TweetReply from './TweetReply'
7. import { render } from 'react-dom';
8. import PropTypes from 'prop-types'
9. import {store} from './App'
10. import { connect } from 'react-redux'
11. import { Provider } from 'react-redux'
12. import {likeTweet, likeTweetDetail} from './actions/Actions'
13.
14. class Tweet extends React.Component{
15.
16. constructor(props){
17. super(props)

Página | 334
18. this.state = props.tweet
19. }
20.
21. handleLike(e){
22. e.preventDefault()
23.
24. if(this.props.detail){
25. this.props.likeTweetDetail(
26. this.props.tweet._id, !this.props.tweet.liked)
27. }else{
28. this.props.likeTweet(this.props.tweet._id, !this.props.tweet.liked)
29. }
30.
31. let request = {
32. tweetID: this.state._id,
33. like: !this.state.liked
34. }
35.
36. APIInvoker.invokePOST('/secure/like', request, response => {
37. let newState = update(this.state,{
38. likeCounter : {$set: response.body.likeCounter},
39. liked: {$apply: (x) => {return !x}}
40. })
41. this.setState(newState)
42. },error => {
43. console.log("Error al cargar los Tweets", error);
44. })
45. }
46.
47. handleReply(e){
48. $( "html" ).addClass( "modal-mode");
49. e.preventDefault()
50.
51. if(!this.props.detail){
52. render(
53. <Provider store={ store }>
54. <TweetReply tweet={this.props.tweet}
55. profile={this.props.tweet._creator} />
56. </Provider>,
57. document.getElementById('dialog'))
58. }
59. }
60.
61. handleClick(e){
62. if(e.target.getAttribute("data-ignore-onclick")){
63. return
64. }
65. let url = "/" + this.props.tweet._creator.userName
66. + "/" + this.props.tweet._id
67. browserHistory.push(url)
68. }
69.
70. render(){
71. let tweetClass = null
72. if(this.props.detail){
73. tweetClass = 'tweet detail'
74. }else{
75. tweetClass = this.props.tweet.isNew ? 'tweet fadeIn animated' : 'tweet'
76. }
77.
78. return (
79. <article className={tweetClass} onClick={this.props.detail ? '' :
80. this.handleClick.bind(this)}
81. id={"tweet-" + this.props.tweet._id}>
82. <img src={this.props.tweet._creator.avatar}
83. className="tweet-avatar" />

335 | Página
84. <div className="tweet-body">
85. <div className="tweet-user">
86. <Link to={"/" + this.props.tweet._creator.userName} >
87. <span className="tweet-name" data-ignore-onclick>
88. {this.props.tweet._creator.name}</span>
89. </Link>
90. <span className="tweet-username">
91. @{this.props.tweet._creator.userName}</span>
92. </div>
93. <p className="tweet-message">{this.props.tweet.message}</p>
94. <If condition={this.props.tweet.image != null}>
95. <img className="tweet-img" src={this.props.tweet.image}/>
96. </If>
97. <div className="tweet-footer">
98. <a className={this.props.tweet.liked
99. ? 'like-icon liked' : 'like-icon'}
100. onClick={this.handleLike.bind(this)} data-ignore-onclick>
101. <i className="fa fa-heart " aria-hidden="true"
102. data-ignore-onclick></i> {this.props.tweet.likeCounter}
103. </a>
104. <If condition={!this.props.detail} >
105. <a className="reply-icon"
106. onClick={this.handleReply.bind(this)}
107. data-ignore-onclick>
108. <i className="fa fa-reply " aria-hidden="true"
109. data-ignore-onclick></i> {this.props.tweet.replys}
110. </a>
111. </If>
112. </div>
113. </div>
114. <div id={"tweet-detail-" + this.props.tweet._id}/>
115. </article>
116. )
117. }
118. }
119.
120. Tweet.propTypes = {
121. tweet: PropTypes.object.isRequired,
122. detail: PropTypes.bool
123. }
124.
125. Tweet.defaultProps = {
126. detail: false
127. }
128.
129. const mapStateToProps = (state) => {
130. return {}
131. }
132.
133. export default connect(mapStateToProps,
134. {likeTweet, likeTweetDetail})(Tweet);

Podemos observar en mapStateToProps que este componente no hace uso de


ninguna sección del estado, esto se debe a que el objeto del tweet se manda
como prop al crear al component.

Solo hay un detalle en el cual vale la pena hacer una pausa, y es referente a las
líneas 53 a 56. Si recordamos el archivo App.js, todos los componentes que están
conectados a Redux necesitan ser hijos del componente <Provider>, lo que no
pasa con el componente TweetReply, pues es creado directamente desde este
componente, es por ese motivo que lo envolvemos mediante un <Provider>.

Página | 336
Refactorizando el componente Reply

El componente Reply es el que nos permite escribir un nuevo Tweet o contestar


a un Tweet existente. Para migrar este componente a Redux, lo primero será
crear las funciones dispatchers y los actions requeridos en el archivo Actions.js.

1. export const updateReplyForm = (field,value) => (dispatch, getState) => {


2. dispatch(updateReplyFormRequest(field,value))
3. }
4.
5. export const resetReplyForm = () => (dispatch, getState) => {
6. dispatch(resetReplyFormRequest())
7. }
8.
9. const updateReplyFormRequest = (field,value) => ({
10. type: UPDATE_REPLY_FORM,
11. field: field,
12. value: value
13. })
14.
15. const resetReplyFormRequest = () => ({
16. type: RESET_REPLY_FORM
17. })

La función updateReplyForm será la que utilizaremos cada vez que el formulario


sea actualizado, es decir, el textarea, la selección de una imagen.

Por otra parte, la función resetReplyForm es la encargada de restablecer los


valores iniciales del componente. Para esto, se apoya del action
resetReplyFormRequest.

En este punto te podrías estar preguntando, ¿en dónde está el action para
guardar el tweet? Recuerda que eso lo delegamos al componente contenedor.

Constantes

Agregaremos las nuevas constantes que hemos definido:

1. export const UPDATE_REPLY_FORM = 'UPDATE_REPLY_FORM'


2. export const RESET_REPLY_FORM = 'RESET_REPLY_FORM'

Reducer

Lo siguiente será agregar un nuevo reducer que atenderá los nuevos actions,
para esto crearemos el archivo ReplyReducer.js en el path /app/reducers, que
se verá de la siguiente manera:

1. import {
2. UPDATE_REPLY_FORM,
3. RESET_REPLY_FORM
4. } from '../actions/const'
5. import update from 'react-addons-update'

337 | Página
6.
7. let initialState = {
8. focus: false,
9. message: '',
10. image: null
11. }
12.
13. export const replyReducer = (state = initialState, action) => {
14. switch (action.type) {
15. case UPDATE_REPLY_FORM:
16. return update(state, {
17. [action.field]: {$set: action.value}
18. })
19. case RESET_REPLY_FORM:
20. return initialState
21. default:
22. return state
23. }
24. }
25.
26. export default replyReducer

Como podemos observar, este nuevo reducer es muy simple, pues el action
UPDATE_REPLY_FORM solo actualiza el atributo del estado que llega como parámetro
(action.field). Y el action RESET_REPLY_FORM actualiza el estado a su valor de
inicio (initialState).

Combine Reducers

Dado que hemos agregado un nuevo reducer, será necesario registrarlo en


nuestro combineReducer del archivo index.js:

1. import { combineReducers } from 'redux'


2. import loginReducer from './LoginReducer'
3. import loginFormReducer from './LoginFormReducer'
4. import signupFormReducer from './SignupFormReducer'
5. import tweetsReducer from './TweetReducer'
6. import tweetDetailReducer from './TweetDetailReducer'
7. import replyReducer from './ReplyReducer'
8.
9. export default combineReducers({
10. loginReducer,
11. loginFormReducer,
12. signupFormReducer,
13. tweetsReducer,
14. tweetDetailReducer,
15. replyReducer
16. })

Migrando el componente Reply

Aplicaremos los siguientes cambios al archivo Reply.js para migrarlo a Redux.

1. import React from 'react'


2. import update from 'react-addons-update'
3. import config from '../config.js'
4. import PropTypes from 'prop-types'
5. import { connect } from 'react-redux'

Página | 338
6. import { updateReplyForm, resetReplyForm } from './actions/Actions'
7.
8. const uuidV4 = require('uuid/v4');
9.
10. class Reply extends React.Component{
11.
12. constructor(props){
13. super(props)
14. this.state={
15. focus: false,
16. message: '',
17. image: null
18. }
19. }
20.
21. handleChangeMessage(e){
22. this.props.updateReplyForm(e.target.name,e.target.value)
23.
24. this.setState(update(this.state,{
25. message: {$set: e.target.value}
26. }))
27. }
28.
29. handleMessageFocus(e){
30. this.props.updateReplyForm('focus',true)
31.
32. let newState = update(this.state,{
33. focus: {$set: true}
34. })
35. this.setState(newState)
36. }
37.
38. handleMessageFocusLost(e){
39. if(this.props.state.reply.message.length=== 0){
40. this.props.resetReplyForm()
41.
42. this.reset();
43. }
44. }
45.
46. handleKeyDown(e){
47. //Scape key
48. if(e.keyCode === 27){
49. this.reset();
50. }
51. }
52.
53. reset(){
54. this.props.resetReplyForm()
55. this.refs.reply.blur();
56.
57. let newState = update(this.state,{
58. focus: {$set: false},
59. message: {$set: ''},
60. image: {$set:null}
61. })
62. this.setState(newState)
63. }
64.
65. newTweet(e){
66. e.preventDefault();
67.
68. let tweet = {
69. isNew: true,
70. _id: uuidV4(),
71. _creator: {

339 | Página
72. _id: this.props.state.profile._id,
73. name: this.props.state.profile.name,
74. userName: this.props.state.profile.userName,
75. avatar: this.props.state.profile.avatar
76. },
77. date: Date.now,
78. message: this.props.state.reply.message,
79. image: this.props.state.reply.image,
80. liked: false,
81. likeCounter: 0
82. }
83.
84. this.props.operations.addNewTweet(tweet)
85. this.reset()
86. this.props.resetReplyForm()
87. }
88.
89. imageSelect(e){
90. e.preventDefault();
91. let reader = new FileReader();
92. let file = e.target.files[0];
93. if(file.size > 1240000){
94. alert('La imagen supera el máximo de 1MB')
95. return
96. }
97.
98. reader.onloadend = () => {
99. let newState = update(this.state,{
100. image: {$set: reader.result}
101. })
102. this.setState(newState)
103.
104. this.props.updateReplyForm('image', reader.result)
105. }
106. reader.readAsDataURL(file)
107. }
108.
109.
110. render(){
111. let randomID = uuidV4();
112.
113. let reply = this.props.state.reply
114.
115. return (
116. <section className="reply">
117. <img src={this.props.state.profile.avatar}
118. className="reply-avatar"/>
119. <div className="reply-body">
120. <textarea
121. ref="reply"
122. name="message"
123. type="text"
124. maxLength = {config.tweets.maxTweetSize}
125. placeholder="¿Qué está pensando?"
126. className={reply.focus ? 'reply-selected' : ''}
127. value={reply.message}
128. onKeyDown={this.handleKeyDown.bind(this)}
129. onBlur={this.handleMessageFocusLost.bind(this)}
130. onFocus={this.handleMessageFocus.bind(this)}
131. onChange={this.handleChangeMessage.bind(this)}
132. />
133. <If condition={reply.image != null} >
134. <div className="image-box">
135. <img src={reply.image}/>
136. </div>
137. </If>

Página | 340
138. </div>
139. <div className={reply.focus ?
140. 'reply-controls' : 'hidden'}>
141. <label htmlFor={"reply-camara-" + randomID}
142. className={reply.message.length===0 ?
143. 'btn pull-left disabled' : 'btn pull-left'}>
144. <i className="fa fa-camera fa-2x" aria-hidden="true"></i>
145. </label>
146.
147. <input href="#"
148. className={reply.message.length===0 ?
149. 'btn pull-left disabled' : 'btn pull-left'}
150. accept=".gif,.jpg,.jpeg,.png"
151. type="file"
152. onChange={this.imageSelect.bind(this)}
153. id={"reply-camara-" + randomID}>
154. </input>
155.
156. <span ref="charCounter" className="char-counter">
157. {config.tweets.maxTweetSize - reply.message.length }
158. </span>
159.
160. <button className={reply.message.length===0 ?
161. 'btn btn-primary disabled' : 'btn btn-primary '}
162. onClick={this.newTweet.bind(this)}
163. >
164. <i className="fa fa-twitch" aria-hidden="true"></i> Twittear
165. </button>
166. </div>
167. </section>
168. )
169. }
170. }
171.
172. Reply.propTypes = {
173. profile: PropTypes.object,
174. operations: PropTypes.object.isRequired
175. }
176.
177. const mapStateToProps = state => {
178. return {
179. state:{
180. reply: state.replyReducer,
181. profile: state.loginReducer.profile
182. }
183. }
184. }
185.
186. export default connect( mapStateToProps,
187. {updateReplyForm, resetReplyForm} )(Reply);

Como podemos observar, estamos mapeando el perfil del usuario autenticado en


la prop profile y en la prop reply guardamos el objeto del nuevo tweet que
estamos creando. El resto de los cambios es lo mismo que hemos venido
haciendo con el resto de componentes.

Refactorizando el componente Profile

Migrar este componente es muy simple, pues no requiere del store ni de ningún
action para funcionar.

341 | Página
1. import React from 'react'
2. import { Link } from 'react-router'
3. import PropTypes from 'prop-types'
4. import { connect } from 'react-redux'
5.
6. class Profile extends React.Component{
7.
8. constructor(props){
9. super(props)
10. this.state = {}
11. }
12.
13.
14. render(){
15. let bannerStyle = {
16. backgroundImage: (this.props.profile.banner!=null ?
17. 'url('+this.props.profile.banner +')' : 'none')
18. }
19. return(
20. <aside id="profile" className="twitter-panel">
21. <div className="profile-banner">
22. <Link to={"/" + this.props.profile.userName}
23. className="profile-name" style={bannerStyle}/>
24. </div>
25. <div className="profile-body">
26. <img className="profile-avatar" src={this.props.profile.avatar}/>
27. <Link to={"/" + this.props.profile.userName}
28. className="profile-name">
29. {this.props.profile.name}
30. </Link>
31. <Link to={"/" + this.props.profile.userName}
32. className="profile-username">
33. @{this.props.profile.userName}
34. </Link>
35. </div>
36. <div className="profile-resumen">
37. <div className="container-fluid">
38. <div className="row">
39. <div className="col-xs-3">
40. <Link to={"/"+this.props.profile.userName}>
41. <p className="profile-resumen-title">TWEETS</p>
42. <p className="profile-resumen-value">
43. {this.props.profile.tweetCount}</p>
44. </Link>
45. </div>
46. <div className="col-xs-4">
47. <Link to={"/"+this.props.profile.userName + "/following"}>
48. <p className="profile-resumen-title">SIGUIENDO</p>
49. <p className="profile-resumen-value">
50. {this.props.profile.following}</p>
51. </Link>
52. </div>
53. <div className="col-xs-5">
54. <Link to={"/"+this.props.profile.userName + "/followers"}>
55. <p className="profile-resumen-title">SEGUIDORES</p>
56. <p className="profile-resumen-value">
57. {this.props.profile.followers}</p>
58. </Link>
59. </div>
60. </div>
61. </div>
62. </div>
63. </aside>
64. )
65. }
66. }

Página | 342
67.
68. Profile.propTypes = {
69. profile: PropTypes.object.isRequired
70. }
71.
72. const mapStateToProps = (state) => {
73. return {
74. profile: state.loginReducer.profile
75. }
76. }
77.
78. export default connect(mapStateToProps, {}) (Profile);

Hemos eliminado la declaración de la prop profile como propTypes, ya que el


perfil lo tomaremos desde el store.

Refactorizando el componente SuggestedUsers

SugestedUsers es el componente que da recomendaciones de usuarios para


seguir. Este componente también solo muestra datos y no transacciona en el API
REST, pero si consume un servicio para consultar los usuarios sugeridos. Los
cambios necesarios para migrar a Redux son los siguientes. Actualizaremos el
archivo Actions.js para agregar los siguientes cambios:

1. export const getSuggestedUsers = () => (dispatch, getState) => {


2. APIInvoker.invokeGET('/secure/suggestedUsers', response => {
3. dispatch(loadSugestedUserSucess(response.body))
4. },error => {
5. console.log("Error al actualizar el perfil");
6. })
7. }
8.
9. const loadSuggestedUserSuccess = users => ({
10. type: LOAD_SUGESTED_USERS,
11. users: users
12. })

La función getSuggestedUsers consulta al API los usuarios sugeridos, después de


eso, lanza el action loadSuggestedUserSuccess para actualizar el estado.

Constantes

Agregamos las nuevas constantes al archivo const.js.

1. export const LOAD_SUGESTED_USERS = 'LOAD_SUGESTED_USERS'

Reducer

Este componente hace uso de un nuevo reducer, por lo que será necesario
agregar un nuevo archivo llamado SugestedUserReducer.js el cual deberá tener
el siguiente contenido:

343 | Página
1. import {
2. LOAD_SUGESTED_USERS
3. } from '../actions/const'
4.
5. const initialState = {
6. load: false,
7. users: []
8. }
9.
10.
11. export const sugestedUserReducer = (state = initialState,action) => {
12. switch (action.type) {
13. case LOAD_SUGESTED_USERS:
14. return {
15. load: true,
16. users: action.users
17. }
18. default:
19. return state
20. }
21. }
22.
23. export default sugestedUserReducer

Podemos apreciar claramente que este reducer solo actualiza la propiedad users
con los usuarios enviados en el action y actualiza la propiedad load en true.

Combiner Reducers

Agregaremos los siguientes cambios al archivo index.js.

1. import { combineReducers } from 'redux'


2. import loginReducer from './LoginReducer'
3. import loginFormReducer from './LoginFormReducer'
4. import signupFormReducer from './SignupFormReducer'
5. import tweetsReducer from './TweetReducer'
6. import tweetDetailReducer from './TweetDetailReducer'
7. import replyReducer from './ReplyReducer'
8. import sugestedUserReducer from './SugestedUserReducer'
9.
10. export default combineReducers({
11. loginReducer,
12. loginFormReducer,
13. signupFormReducer,
14. tweetsReducer,
15. tweetDetailReducer,
16. replyReducer,
17. sugestedUserReducer
18. })

Migrando el componente SuggestedUser a Redux

Aplicaremos los siguientes cambios para migrar el componente:

1. import React from 'react'


2. import APIInvoker from './utils/APIInvoker'
3. import { Link } from 'react-router'

Página | 344
4.
5. class SuggestedUser extends React.Component{
6.
7. constructor(){
8. super(...arguments)
9. this.state = {
10. load: false
11. }
12. }
13.
14. componentWillMount(){
15. this.props.getSuggestedUsers()
16.
17. APIInvoker.invokeGET('/secure/suggestedUsers', response => {
18. this.setState({
19. load: true,
20. users: response.body
21. })
22. },error => {
23. console.log("Error al actualizar el perfil", error);
24. })
25. }
26.
27. render(){
28. return(
29. <aside id="suggestedUsers" className="twitter-panel">
30. <span className="su-title">A quién seguir</span>
31. <If condition={this.props.load} >
32. <For each="user" of={this.props.users}>
33. <div className="sg-item" key={user._id}>
34. <div className="su-avatar">
35. <img src={user.avatar} alt="Juan manuel"/>
36. </div>
37. <div className="sg-body">
38. <div>
39. <a href={"/" + user.userName}>
40. <span className="sg-name">{user.name}</span>
41. <span className="sg-username">@{user.userName}</span>
42. </a>
43. </div>
44. <a href={"/" + user.userName}
45. className="btn btn-primary btn-sm">
46. <i className="fa fa-user-plus" aria-hidden="true"></i>
47. Ver perfil</a>
48. </div>
49. </div>
50. </For>
51. </If>
52. </aside>
53. )
54. }
55. }
56.
57. const mapStateToProps = (state) => {
58. return {
59. load: state.sugestedUserReducer.load,
60. users: state.sugestedUserReducer.users
61. }
62. }
63.
64. export default connect(mapStateToProps, {getSuggestedUsers})(SuggestedUser);

Solo será necesario remplazar las referencias al estado por las nuevas props
definidas en mapStateToProps.

345 | Página
Refactorizando el componente TwitterDashboard

El componente TwitterDashboard en realidad no necesita de ningún ajuste para


funcionar con Redux, debido a que no hace uso de nada del Store y tampoco
realiza ninguna acción. Sin embargo, sería bueno realizarle algunos ajustes para
limpiar algo de código que no será necesario con Redux. Veamos los cambios:

1. import React from 'react'


2. import Profile from './profile'
3. import TweetsContainer from './TweetsContainer'
4. import SuggestedUser from './SuggestedUser'
5. import PropTypes from 'prop-types'
6.
7. class TwitterDashboard extends React.Component{
8.
9. constructor(props){
10. super(props)
11. }
12.
13. render(){
14. return(
15. <div id="dashboard">
16. <div className="container-fluid">
17. <div className="row">
18. <div className="hidden-xs col-sm-4 col-md-push-1
19. col-md-3 col-lg-push-1 col-lg-3" >
20. <Profile profile={this.props.profile}/>
21. </div>
22. <div className="col-xs-12 col-sm-8 col-md-push-1
23. col-md-7 col-lg-push-1 col-lg-4">
24. <TweetsContainer profile={this.props.profile} />
25. </div>
26. <div className="hidden-xs hidden-sm hidden-md
27. col-lg-push-1 col-lg-3">
28. <SuggestedUser/>
29. </div>
30. </div>
31. </div>
32. </div>
33. )
34. }
35. }
36.
37. TwitterDashboard.propTypes = {
38. profile: PropTypes.object.isRequired
39. }
40.
41. export default TwitterDashboard;

Quiero que observes que no hemos agregado una solo línea de código, al
contrario, hemos quitado todas las referencias a las pros. A medida que
migramos a Redux, va perdiendo el sentido pasar props de un padre a los
componentes hijos, pues estos podrán recuperar la información directamente del
store, sin embargo, siempre habrá escenarios donde si vamos a requerir pasar
props.

Página | 346
Refactorizando el componente Toolbar

El componente Toolbar es la barra que se encuentra en la parte superior de la


aplicación, en la cual, podemos ver nuestro avatar. Este componente solo
requiere del perfil del usuario para mostrar el avatar. Adicional a eso, también
puede cerrar la sesión, por lo que deberemos de tener una operación que borre
el token y actualice el store para eliminar los datos del perfil.

Lo primero será agregar lo siguiente al archivo Actions.js:

1. export const logout = () => (dispatch, getState) => {


2. dispatch(logoutRequest())
3. }
4.
5. const logoutRequest = () => ({
6. type: LOGOUT_REQUEST
7. })

La función logout será la encargada de cerrar la sesión y se apoyará del action


logoutRequest para actualiza el estado.

Constantes

Agregaremos la siguiente constate al archivo const.js:

1. export const LOGOUT_REQUEST = 'LOGOUT_REQUEST'

Reducer

La función logut ya la tenemos en el archivo Actions.js, por lo que no será


necesario realizar ninguna modificación en los reducers.

Migrando el componente Toolbar

Los siguientes cambios deberá aplicarse al archivo Toolbar.js:

1. import React from 'react'


2. import { browserHistory,Link } from 'react-router'
3. import PropTypes from 'prop-types'
4. import {connect} from 'react-redux'
5. import { logout } from './actions/Actions'
6.
7. class Toolbar extends React.Component{
8. constructor(props){
9. super(props)
10. this.state= {}
11. }
12.
13. logout(e){
14. e.preventDefault()
15. window.localStorage.removeItem("token")

347 | Página
16. window.localStorage.removeItem("username")
17. this.props.logout()
18. window.location = '/login';
19. }
20.
21. render(){
22.
23. return(
24. <nav className="navbar navbar-default navbar-fixed-top">
25. <span className="visible-xs bs-test">XS</span>
26. <span className="visible-sm bs-test">SM</span>
27. <span className="visible-md bs-test">MD</span>
28. <span className="visible-lg bs-test">LG</span>
29.
30. <div className="container-fluid">
31. <div className="container-fluid">
32. <div className="navbar-header">
33. <a className="navbar-brand" href="#">
34. <i className="fa fa-twitter" aria-hidden="true"></i>
35. </a>
36. <ul id="menu">
37. <li id="tbHome" className="selected">
38. <Link to="/">
39. <p className="menu-item"><i
40. className="fa fa-home menu-item-icon" aria-hidden="true">
41. </i> <span className="hidden-xs hidden-sm">Inicio</span>
42. </p>
43. </Link>
44. </li>
45. </ul>
46. </div>
47. <If condition={this.props.state.profile != null} >
48. <ul className="nav navbar-nav navbar-right">
49. <li className="dropdown">
50. <a href="#" className="dropdown-toggle"
51. data-toggle="dropdown"
52. role="button" aria-haspopup="true" aria-expanded="false">
53. <img className="navbar-avatar"
54. src={this.props.state.profile.avatar}
55. alt={this.props.state.profile.userName}/>
56. </a>
57. <ul className="dropdown-menu">
58. <li><a href={"/"+this.props.state.profile.userName}>
59. Ver perfil</a></li>
60. <li role="separator" className="divider"></li>
61. <li><a href="#" onClick={this.logout.bind(this)}>
62. Cerrar sesión</a></li>
63. </ul>
64. </li>
65. </ul>
66. </If>
67. </div>
68. </div>
69. </nav>
70. )
71. }
72. }
73.
74. Toolbar.propTypes = {
75. profile: PropTypes.object
76. }
77.
78. const mapStateToProps = (state) => {
79. return {
80. state: {
81. profile: state.loginReducer.profile

Página | 348
82. }
83. }
84. }
85.
86. export default connect(mapStateToProps,{logout})(Toolbar);

Antes de migrar este componente a Redux, tan solo era necesario eliminar del
LocalStorage el token y el usuario. Sin embargo, ahora que todo el estado vive
en el store, eso ya no es suficiente, por lo que hay que ejecutar la función logut
para limpiar los datos del perfil.

Refactorizando el componente Followers & Followings

Debido a que los componentes Followers y Followings son prácticamente


idénticos, vamos a tratarlos juntos en esta sección. Ambos comparten los mismos
actions, dispatchers y reducer por lo que migrarlos será casi igual.

Lo primero será actualizar el archivo Actions.js para agregar los dispatcher y


actions requeridos:

1. export const findFollowersFollowings = (username, type)


2. => (dispatch, getState) => {
3. APIInvoker.invokeGET('/' + type + "/" + username, response => {
4. dispatch(findFollowersFollowingsRequest(response.body))
5. },error => {
6. console.log("Error al obtener los seguidores");
7. })
8. }
9.
10. export const resetFollowersFollowings = () => (dispatch, getState) => {
11. dispatch(resetFollowersFollowingsRequest())
12. }
13.
14. const findFollowersFollowingsRequest = (users) => ({
15. type: FIND_FOLLOWERS_FOLLOWINGS_REQUEST,
16. users: users
17. })
18.
19. const resetFollowersFollowingsRequest = () => ({
20. type: RESET_FOLLOWERS_FOLLOWINGS_REQUEST
21. })

La función findFollowersFollowings servirá para consultar tantos los seguidores


como las personas que siguen a un usuario, para esto, el parámetro type podría
tener los valores (followings o followers) y el username contiene el usuario del
cual queremos conocer la información.

Por otra parte, la función resetFollowersFollowings se encarga de limpiar el store


una vez que el componente es desmontado, esto con la finalidad de liberar los
recursos.

Constantes

349 | Página
Las siguientes constantes deberá ser agregadas al archivo const.js:

1. export const FIND_FOLLOWERS_FOLLOWINGS_REQUEST =


2. 'FIND_FOLLOWERS_FOLLOWINGS_REQUEST'
3. export const RESET_FOLLOWERS_FOLLOWINGS_REQUEST =
4. 'RESET_FOLLOWERS_FOLLOWINGS_REQUEST'

Reducer

Continuaremos con el reducer que nos ayudará con la actualización del estado,
para ello, crearemos un nuevo archivo llamado FollowerReducer.js en el path
/app/reducers, el cual quedará de la siguiente manera:

1. import {
2. FIND_FOLLOWERS_FOLLOWINGS_REQUEST,
3. RESET_FOLLOWERS_FOLLOWINGS_REQUEST
4. } from '../actions/const'
5.
6. const initialState = {
7. users: []
8. }
9.
10. export const followerReducer = (state = initialState, action) => {
11. switch (action.type) {
12. case FIND_FOLLOWERS_FOLLOWINGS_REQUEST:
13. return {
14. users: action.users
15. }
16. case RESET_FOLLOWERS_FOLLOWINGS_REQUEST:
17. return initialState
18. default:
19. return state
20.
21. }
22. }
23.
24. export default followerReducer

Combine Reduces

Dado que hemos creado un nuevo reducer, será necesario registrar el reducer en
nuestro combinedReducers. Para esto, vamos a modificar el archivo index.js para
quedar de la siguiente manera:

1. import { combineReducers } from 'redux'


2. import loginReducer from './LoginReducer'
3. import loginFormReducer from './LoginFormReducer'
4. import signupFormReducer from './SignupFormReducer'
5. import tweetsReducer from './TweetReducer'
6. import tweetDetailReducer from './TweetDetailReducer'
7. import replyReducer from './ReplyReducer'
8. import sugestedUserReducer from './SugestedUserReducer'
9. import followerReducer from './FollowerReducer'
10.
11. export default combineReducers({
12. loginReducer,
13. loginFormReducer,
14. signupFormReducer,

Página | 350
15. tweetsReducer,
16. tweetDetailReducer,
17. replyReducer,
18. sugestedUserReducer,
19. followerReducer
20. })

Migrando el componente Followers

El componente Followers es donde se ven los seguidores de un usuario. Debido


a que es posible ver los seguidores del usuario autenticado como el de cualquier
otro, es necesario que se les envíe el perfil del usuario sobre el que estaremos
trabajando.

1. import React from 'react'


2. import UserCard from './UserCard'
3. import APIInvoker from './utils/APIInvoker'
4. import PropTypes from 'prop-types'
5. import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
6. import {connect} from 'react-redux'
7. import {findFollowersFollowings, resetFollowersFollowings}
8. from './actions/Actions'
9.
10.
11. class Followers extends React.Component{
12.
13. constructor(props){
14. super(props)
15. this.state={
16. users: []
17. }
18. }
19.
20. componentWillMount(){
21. this.props.findFollowersFollowings(this.props.params.user,'followers')
22. this.findUsers(this.props.profile.userName)
23. }
24.
25. componentWillUnmount(){
26. this.props.resetFollowersFollowings()
27. }
28.
29.
30. componentWillReceiveProps(props){
31. this.setState({
32. tab: props.route.tab,
33. users: []
34. })
35. this.findUsers(props.profile.userName)
36. }
37.
38. findUsers(username){
39. APIInvoker.invokeGET('/followers/' + username, response => {
40. this.setState({
41. users: response.body
42. })
43. },error => {
44. console.log("Error en la autenticación");
45. })
46. }
47.
48. render(){

351 | Página
49. return(
50. <section>
51. <div className="container-fluid no-padding">
52. <div className="row no-padding">
53. <CSSTransitionGroup
54. transitionName="card"
55. transitionEnter = {true}
56. transitionEnterTimeout={500}
57. transitionAppear={false}
58. transitionAppearTimeout={0}
59. transitionLeave={false}
60. transitionLeaveTimeout={0}>
61. <For each="user" of={ this.props.state.users }>
62. <div className="col-xs-12 col-sm-6 col-lg-4"
63. key={this.props.route.tab + "-" + user._id}>
64. <UserCard user={user} />
65. </div>
66. </For>
67. </CSSTransitionGroup>
68. </div>
69. </div>
70. </section>
71. )
72. }
73. }
74.
75. Followers.propTypes = {
76. profile: PropTypes.object
77. }
78.
79. const mapStateToProps = (state) => {
80. return {
81. state: state.followerReducer
82. }
83. }
84.
85. export default connect(mapStateToProps,
86. {findFollowersFollowings, resetFollowersFollowings})(Followers);

Lo único nuevo que hemos agregado es el método componentWillUnmount, el cual


limpiara el store una vez que el componente sea desmontado, es importante
hacer esto debido a que tanto Follower como Followings utilizan el mismo
reducer, por lo tanto, es necesario limpiarlo al salir para dejarlo preparado
cuando Followings sea montado. Fuera de esto, solo hemos aplicado los cambios
que hemos venido haciendo en el resto de componentes.

Migrando el componente Followings

La única diferencia que tiene este componente con el anterior, es el parámetro


que le enviamos a la función findFollowersFollowings (línea 21), por lo que nos
ahorraremos las explicaciones:

1. import React from 'react'


2. import UserCard from './UserCard'
3. import APIInvoker from './utils/APIInvoker'
4. import PropTypes from 'prop-types'
5. import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
6. import {connect} from 'react-redux'
7. import {findFollowersFollowings, resetFollowersFollowings}
8. from './actions/Actions'

Página | 352
9.
10. class Followings extends React.Component{
11.
12. constructor(props){
13. super(props)
14. this.state={
15. users: []
16. }
17. }
18.
19. componentWillMount(){
20. // this.findUsers(this.props.profile.userName)
21. this.props.findFollowersFollowings(this.props.params.user,'followings')
22. }
23.
24. componentWillUnmount(){
25. this.props.resetFollowersFollowings()
26. }
27.
28. componentWillReceiveProps(props){
29. this.setState({
30. tab: props.route.tab,
31. users: []
32. })
33. this.findUsers(props.profile.userName)
34. }
35.
36. findUsers(username){
37. APIInvoker.invokeGET('/followings/' + username, response => {
38. this.setState({
39. users: response.body
40. })
41. },error => {
42. console.log("Error en la autenticación");
43. })
44. }
45.
46. render(){
47. return(
48. <section>
49. <div className="container-fluid no-padding">
50. <div className="row no-padding">
51. <CSSTransitionGroup
52. transitionName="card"
53. transitionEnter = {true}
54. transitionEnterTimeout={500}
55. transitionAppear={false}
56. transitionAppearTimeout={0}
57. transitionLeave={false}
58. transitionLeaveTimeout={0}>
59. <For each="user" of={ this.props.state.users }>
60. <div className="col-xs-12 col-sm-6 col-lg-4"
61. key={this.props.route.tab + "-" + user._id}>
62. <UserCard user={user} />
63. </div>
64. </For>
65. </CSSTransitionGroup>
66. </div>
67. </div>
68. </section>
69. )
70. }
71. }
72.
73. Followings.propTypes = {
74. profile: PropTypes.object

353 | Página
75. }
76.
77. const mapStateToProps = (state) => {
78. return {
79. state: state.followerReducer
80. }
81. }
82.
83. export default connect(mapStateToProps,
84. {findFollowersFollowings, resetFollowersFollowings})(Followings);

Refactorizando el componente UserCard

Dado que el componente UserCard solo trabaja con los props de entrada y no
realiza ninguna acción, no es necesario realizar ningún cambio, por lo que así
como esta nos servirá para trabajar con Redux.

Refactorizando el componente MyTweets

El componente MyTweets no requiere de ningún cambio importante. Por el


contrario, lo que haremos será limpiarlo y dejarlo más simple de lo que estaba
en un inicio:

1. import React from 'react'


2. import TweetsContainer from './TweetsContainer'
3. import SuggestedUser from './SuggestedUser'
4. import PropTypes from 'prop-types'
5.
6. class MyTweets extends React.Component{
7.
8. constructor(props){
9. super(props)
10. }
11.
12. render(){
13. return(
14. <div className="row">
15. <div className="col-xs-12 col-sm-12 col-md-12
16. col-lg-8 no-padding-right">
17. <TweetsContainer
18. profile={this.props.profile}
19. onlyUserTweet={true}/>
20. </div>
21. <div className="hidden-xs hidden-sm hidden-md col-lg-4">
22. <SuggestedUser/>
23. </div>
24. </div>
25. )
26. }
27. }

Página | 354
28.
29. MyTweets.propTypes = {
30. profile: PropTypes.object
31. }
32.
33. export default MyTweets;

Dado que el perfil del usuario lo podemos obtener del store, ya no será necesario
enviarlo como prop. Debido a esto, podemos eliminar los propTypes, así como
tampoco es necesario enviar el perfil al componente TweetContainer, pues él
también puede obtener el perfil del store.

Ajustes al archivo TweetsContainer

Debido a que hemos eliminado la propiedad profile que le mandamos al


componente TweetsContainer, tendremos que realizar unos ajustes al
componente TweetsContainer, los cambios a realizar son:

1. componentDidUpdate(prevProps, prevState) {
2. if(prevProps.state.profile.userName !==
3. this.props.state.profile.userName){
4. let username = this.props.state.profile.userName
5. let onlyUserTweet = this.props.onlyUserTweet
6. this.props.getTweet(username, onlyUserTweet)
7. }
8. }
9.
10. componentWillMount(){
11. let username = this.props.state.profile.userName
12. let onlyUserTweet = this.props.onlyUserTweet
13. this.props.getTweet(username, onlyUserTweet)
14. }

Refactorizando el componente UserPage

Como ya sabemos, el componente UserPage es el componente más complejo que


tenemos, debido a la gran cantidad de elementos que contiene y que adicional
tiene el modo lectura y modo edición, por lo que iremos un poco más lentos para
no perdernos.

Lo primero será agregar al archivo Actions.js las siguientes funciones y actions


requeridas para el funcionamiento del componente UserPage.

1. export const getUserProfile = (username) => (dispatch, getState) => {


2. APIInvoker.invokeGET('/profile/' + username, response => {
3. dispatch(getUserProfileResponse(response.body))
4. },error => {
5. console.log("Error al cargar los Tweets", error);
6. })
7. }
8.

355 | Página
9. const getUserProfileResponse = (profile) => ({
10. type: USER_PROFILE_REQUEST,
11. edit: false,
12. profile: profile
13. })
14.
15. export const chageToEditMode = () => (dispatch, getState) => {
16. let currentProfile = getState().userPageReducer.profile
17. dispatch(changeToEditModeRequest(currentProfile))
18. }
19.
20. const changeToEditModeRequest = (currentState) => ({
21. type: CHANGE_TO_EDIT_MODE_REQUEST,
22. edit: true,
23. profile: currentState,
24. currentState: currentState
25. })
26.
27. export const cancelEditMode = () => (dispatch, getState) => {
28. dispatch(cancelEditModeRequest())
29. }
30.
31. const cancelEditModeRequest = () => ({
32. type: CANCEL_EDIT_MODEL_REQUEST
33. })
34.
35. export const updateUserPageForm = (event) => (dispatch, getState) => {
36. dispatch(updateUserPageFormRequest(event.target.id, event.target.value))
37. }
38.
39. const updateUserPageFormRequest = (field, value) => ({
40. type: UPDATE_USER_PAGE_FORM_REQUEST,
41. field: field,
42. value: value
43. })
44.
45. export const userPageImageUpload = (event) => (dispatch, getState) => {
46. let id = event.target.id
47. let reader = new FileReader();
48. let file = event.target.files[0];
49.
50. if(file.size > 1240000){
51. alert('La imagen supera el máximo de 1MB')
52. return
53. }
54.
55. reader.onloadend = () => {
56. if(id == 'bannerInput'){
57. dispatch(userPageBannerUpdateRequest(reader.result))
58. }else{
59. dispatch(userPageAvatarUpdateRequest(reader.result))
60. }
61. }
62. reader.readAsDataURL(file)
63. }
64.
65. const userPageBannerUpdateRequest = (img) => ({
66. type: USER_PAGE_BANNER_UPDATE,
67. img: img
68. })
69.
70. const userPageAvatarUpdateRequest = (img) => ({
71. type: USER_PAGE_AVATAR_UPDATE,
72. img: img
73. })
74.

Página | 356
75. export const userPageSaveChanges = () => (dispatch, getState) => {
76. let state = getState().userPageReducer
77. let request = {
78. username: state.profile.userName,
79. name: state.profile.name,
80. description: state.profile.description,
81. avatar: state.profile.avatar,
82. banner: state.profile.banner
83. }
84.
85. APIInvoker.invokePUT('/secure/profile', request, response => {
86. dispatch(userPageSaveChangesRequest())
87. },error => {
88. console.log("Error al actualizar el perfil");
89. })
90. }
91.
92. const userPageSaveChangesRequest = () => ({
93. type: USER_PAGE_SAVE_CHANGES
94. })
95.
96. export const followUser = (username) => (dispatch,getState) => {
97. let request = {
98. followingUser: username
99. }
100.
101. APIInvoker.invokePOST('/secure/follow', request, response => {
102. dispatch(followUserRequest(!response.unfollow))
103. },error => {
104. console.log("Error al actualizar el perfil");
105. })
106. }
107.
108. const followUserRequest = (follow) => ({
109. type: USER_PAGE_FOLLOW_USER,
110. follow: follow
111. })

La función getUserProfile la utilizamos para recuperar el perfil de un usuario


determinado, para esto, es enviado como parámetro el nombre de usuario a
consultar. Cuando el servicio responde, utilizamos el action
getUserProfileResponse para actualizar el estado con el perfil recuperado y
establecemos el componente como solo lectura.

Las funciones chageToEditMode y cancelEditMode lo único que haces pasar el


componente a modo edición y cancelar el modo de edición, actualizando la
propiedad edit del store. Estos se apoyan de los actions
changeToEditModeRequest y cancelEditModeRequest respectivamente.

Una vez que entramos en modo edición utilizaremos la función


updateUserPageForm para actualizar el estado a medida que capturamos nombre
y descripción del formulario. Cada vez que un cambio sucede en el formulario, el
action updateUserPageFormRequest es disparado con la información para
actualizar el estado.

Los eventos de la carga de imágenes como es el caso del banner o el avatar, se


tratan por medio de la función userPageImageUpload, el cual cargará la imagen y
disparará una acción según la imagen actualizada. Si el banner es actualizado,
entonces el action userPageBannerUpdateRequest es lanzado, por otra parte, si se
actualiza el avatar, entonces el action userPageAvatarUpdateRequest es lanzado.

357 | Página
Cuando hemos terminado de actualizar los datos del usuario, ejecutamos el botón
de guardar, lo que dispara la función userPageSaveChanges el cual se encargará
de ejecutar el API REST para actualizar definitivamente los datos del perfil del
usuario. Cuando los datos son actualizados, es lanzado el action
userPageSaveChangesRequest, el cual cambiara el componente en modo solo
lectura.

La última acción que puede realizar el usuario es seguir a otro usuario, mediante
el botón de seguir, o dejar de seguirlo, para esto, nos apoyamos de la función
followUser, la cual se encarga de la comunicación con el API. Recordemos que,
si no lo estamos siguiendo, entonces lo empezaremos a seguir, por otro lado, si
ya lo seguimos, entonces lo dejaremos de seguir.

Constantes

Tendremos que agregar las siguientes constantes el archivo const.js:

1. export const USER_PAGE_FOLLOW_USER = 'USER_PAGE_FOLLOW_USER'


2. export const USER_PAGE_SAVE_CHANGES = 'USER_PAGE_SAVE_CHANGES'
3. export const USER_PAGE_AVATAR_UPDATE = 'USER_PAGE_AVATAR_UPDATE'
4. export const USER_PAGE_BANNER_UPDATE = 'USER_PAGE_BANNER_UPDATE'
5. export const UPDATE_USER_PAGE_FORM_REQUEST = 'UPDATE_USER_PAGE_FORM_REQUEST'
6. export const CANCEL_EDIT_MODEL_REQUEST = 'CANCEL_EDIT_MODEL_REQUEST'
7. export const CHANGE_TO_EDIT_MODE_REQUEST = 'CHANGE_TO_EDIT_MODE_REQUEST'
8. export const USER_PROFILE_REQUEST = 'USER_PROFILE_REQUEST'

Reducer

Agregaremos un nuevo reducer llamado UserPageReducer.js en el path


/app/reducers, el cual deberá tener el siguiente contenido:

1. import {
2. USER_PROFILE_REQUEST,
3. CHANGE_TO_EDIT_MODE_REQUEST,
4. CANCEL_EDIT_MODEL_REQUEST,
5. UPDATE_USER_PAGE_FORM_REQUEST,
6. USER_PAGE_AVATAR_UPDATE,
7. USER_PAGE_BANNER_UPDATE,
8. USER_PAGE_SAVE_CHANGES,
9. USER_PAGE_FOLLOW_USER
10. } from '../actions/const'
11. import update from 'react-addons-update'
12.
13. const initialState = {
14. edit: false,
15. profile:{
16. name: "",
17. description: "",
18. avatar: null,
19. banner: null,
20. userName: ""
21. }
22. }
23.
24. export const userPageReducer = (state = initialState, action) => {

Página | 358
25. switch (action.type) {
26. case USER_PROFILE_REQUEST:
27. return {
28. edit: false,
29. profile: action.profile
30. }
31. case CHANGE_TO_EDIT_MODE_REQUEST:
32. return {
33. edit: action.edit,
34. profile: action.profile,
35. currentState: action.currentState
36. }
37. case CANCEL_EDIT_MODEL_REQUEST:
38. return {
39. edit: false,
40. profile: state.currentState,
41. currentState: null
42. }
43. case UPDATE_USER_PAGE_FORM_REQUEST:
44. return update(state, {
45. profile: {
46. [action.field]: {$set: action.value}
47. }
48. })
49. case USER_PAGE_BANNER_UPDATE:
50. return update(state,{
51. profile: {
52. banner: {$set: action.img}
53. }
54. })
55. case USER_PAGE_AVATAR_UPDATE:
56. return update(state,{
57. profile: {
58. avatar: {$set: action.img}
59. }
60. })
61. case USER_PAGE_SAVE_CHANGES:
62. return update(state,{
63. edit: {$set: false}
64. })
65. case USER_PAGE_FOLLOW_USER:
66. return update(state, {
67. profile:{
68. follow: {$set: action.follow}
69. }
70. })
71. default:
72. return state
73. }
74. }
75.
76. export default userPageReducer

Este reducer puede parecer imponente por la gran cantidad de actions que puede
procesar, pero si vamos analizando uno por uno, veremos que no hay nada nuevo
que ver, todos los actions son simples actualizaciones al estado.

Combine Reduces

Dado que hemos creado un nuevo reducer, será necesario registrarlo en el


archivo index.js:

359 | Página
1. import { combineReducers } from 'redux'
2. import loginReducer from './LoginReducer'
3. import loginFormReducer from './LoginFormReducer'
4. import signupFormReducer from './SignupFormReducer'
5. import tweetsReducer from './TweetReducer'
6. import tweetDetailReducer from './TweetDetailReducer'
7. import replyReducer from './ReplyReducer'
8. import sugestedUserReducer from './SugestedUserReducer'
9. import followerReducer from './FollowerReducer'
10. import userPageReducer from './UserPageReducer'
11.
12. export default combineReducers({
13. loginReducer,
14. loginFormReducer,
15. signupFormReducer,
16. tweetsReducer,
17. tweetDetailReducer,
18. replyReducer,
19. sugestedUserReducer,
20. followerReducer,
21. userPageReducer
22. })

Migrando el componente UserPage

A continuación, los cambios que hay que realizar para migrar el componente a
Redux

1. import React from 'react'


2. import update from 'react-addons-update'
3. import APIInvoker from './utils/APIInvoker'
4. import { Link } from 'react-router'
5. import { connect } from 'react-redux'
6. import {
7. getUserProfile,
8. chageToEditMode,
9. cancelEditMode,
10. updateUserPageForm,
11. userPageImageUpload,
12. userPageSaveChanges,
13. followUser,
14. relogin}
15. from './actions/Actions'
16.
17. class UserPage extends React.Component{
18.
19. constructor(props){
20. super(props)
21. this.state = {
22. edit: false,
23. profile:{
24. name: "",
25. description: "",
26. avatar: null,
27. banner: null,
28. userName: ""
29. }
30. }
31. }
32.
33. componentWillMount(){
34. let user = this.props.params.user
35. this.props.getUserProfile(user)

Página | 360
36.
37. APIInvoker.invokeGET('/profile/' + user, response => {
38. this.setState({
39. edit:false,
40. profile: response.body
41. });
42. },error => {
43. console.log("Error al cargar los Tweets");
44. window.location = '/'
45. })
46. }
47.
48. imageSelect(e){
49. let id = e.target.id
50. this.props.userPageImageUpload(e)
51.
52. e.preventDefault();
53. let reader = new FileReader();
54. let file = e.target.files[0];
55.
56. if(file.size > 1240000){
57. alert('La imagen supera el máximo de 1MB')
58. return
59. }
60.
61. reader.onloadend = () => {
62. if(id == 'bannerInput'){
63. this.setState(update(this.state,{
64. profile: {
65. banner: {$set: reader.result}
66. }
67. }))
68. }else{
69. this.setState(update(this.state,{
70. profile: {
71. avatar: {$set: reader.result}
72. }
73. }))
74. }
75. }
76. reader.readAsDataURL(file)
77. }
78.
79. handleInput(e){
80. this.props.updateUserPageForm(e)
81.
82. let id = e.target.id
83. this.setState(update(this.state,{
84. profile: {
85. [id]: {$set: e.target.value}
86. }
87. }))
88. }
89.
90. cancelEditMode(e){
91. this.props.cancelEditMode()
92.
93. let currentState = this.state.currentState
94. this.setState(update(this.state,{
95. edit: {$set: false},
96. profile: {$set: currentState}
97. }))
98. }
99.
100. changeToEditMode(e){
101. if(this.props.state.edit){

361 | Página
102. this.props.userPageSaveChanges()
103. this.props.relogin()
104. }else{
105. this.props.chageToEditMode()
106. }
107.
108. if(this.state.edit){
109. let request = {
110. username: this.state.profile.userName,
111. name: this.state.profile.name,
112. description: this.state.profile.description,
113. avatar: this.state.profile.avatar,
114. banner: this.state.profile.banner
115. }
116.
117. APIInvoker.invokePUT('/secure/profile', request, response => {
118. if(response.ok){
119. this.setState(update(this.state,{
120. edit: {$set: false}
121. }))
122. }
123. },error => {
124. console.log("Error al actualizar el perfil");
125. })
126. }else{
127. let currentState = this.state.profile
128. this.setState(update(this.state,{
129. edit: {$set: true},
130. currentState: {$set: currentState}
131. }))
132. }
133. }
134.
135. follow(e){
136. this.props.followUser(this.props.params.user)
137.
138. let request = {
139. followingUser: this.props.params.user
140. }
141. APIInvoker.invokePOST('/secure/follow', request, response => {
142. if(response.ok){
143. this.setState(update(this.state,{
144. profile:{
145. follow: {$set: !response.unfollow}
146. }
147. }))
148. }
149. },error => {
150. console.log("Error al actualizar el perfil");
151. })
152. }
153.
154. render(){
155. let profile = this.props.state.profile
156. let storageUserName = window.localStorage.getItem("username")
157.
158. let bannerStyle = {
159. backgroundImage: 'url(' + (profile.banner) + ')'
160. }
161.
162. let childs = this.props.children
163. && React.cloneElement(this.props.children, { profile: profile })
164.
165. return(
166. <div id="user-page" className="app-container">
167. <header className="user-header">

Página | 362
168. <div className="user-banner" style={bannerStyle}>
169. <If condition={this.props.state.edit}>
170. <div>
171. <label htmlFor="bannerInput" className="btn select-banner">
172. <i className="fa fa-camera fa-2x" aria-hidden="true"></i>
173. <p>Cambia tu foto de encabezado</p>
174. </label>
175. <input href="#" className="btn"
176. accept=".gif,.jpg,.jpeg,.png"
177. type="file" id="bannerInput"
178. onChange={this.imageSelect.bind(this)} />
179. </div>
180. </If>
181. </div>
182. <div className="user-summary">
183. <div className="container-fluid">
184. <div className="row">
185. <div className="hidden-xs col-sm-4 col-md-push-1
186. col-md-3 col-lg-push-1 col-lg-3" >
187. </div>
188. <div className="col-xs-12 col-sm-8 col-md-push-1
189. col-md-7 col-lg-push-1 col-lg-7">
190. <ul className="user-summary-menu">
191. <li className={this.props.route.tab === 'tweets' ?
192. 'selected':''}>
193. <Link to={"/" + profile.userName}>
194. <p className="summary-label">TWEETS</p>
195. <p className="summary-value">
196. {profile.tweetCount}
197. </p>
198. </Link>
199. </li>
200. <li className={this.props.route.tab === 'followings' ?
201. 'selected':''}>
202. <Link to={"/" + profile.userName + "/following" }>
203. <p className="summary-label">SIGUIENDO</p>
204. <p className="summary-value">{profile.following}</p>
205. </Link>
206. </li>
207. <li className={this.props.route.tab === 'followers' ?
208. 'selected':''}>
209. <Link to={"/" + profile.userName + "/followers" }>
210. <p className="summary-label">SEGUIDORES</p>
211. <p className="summary-value">{profile.followers}</p>
212. </Link>
213. </li>
214. </ul>
215.
216. <If condition={profile.userName === storageUserName}>
217. <button className="btn btn-primary edit-button"
218. onClick={this.changeToEditMode.bind(this)} >
219. {this.props.state.edit ? "Guardar" : "Editar perfil"}
220. </button>
221. </If>
222.
223. <If condition={profile.follow != null &&
224. profile.userName !== storageUserName} >
225. <button className="btn edit-button"
226. onClick={this.follow.bind(this)} >
227. {profile.follow
228. ? (<span><i className="fa fa-user-times"
229. aria-hidden="true"></i> Siguiendo</span>)
230. : (<span><i className="fa fa-user-plus"
231. aria-hidden="true"></i> Seguir</span>)
232. }
233. </button>

363 | Página
234. </If>
235.
236. <If condition= {this.props.state.edit}>
237. <button className="btn edit-button" onClick=
238. {this.cancelEditMode.bind(this)} >Cancelar</button>
239. </If>
240. </div>
241. </div>
242. </div>
243. </div>
244. </header>
245. <div className="container-fluid">
246. <div className="row">
247. <div className="hidden-xs col-sm-4 col-md-push-1 col-md-3
248. col-lg-push-1 col-lg-3" >
249. <aside id="user-info">
250. <div className="user-avatar">
251. <Choose>
252. <When condition={this.props.state.edit} >
253. <div className="avatar-box">
254. <img src={profile.avatar} />
255. <label htmlFor="avatarInput"
256. className="btn select-avatar">
257. <i className="fa fa-camera fa-2x"
258. aria-hidden="true"></i>
259. <p>Foto</p>
260. </label>
261. <input href="#" id="avatarInput"
262. className="btn" type="file"
263. accept=".gif,.jpg,.jpeg,.png"
264. onChange={this.imageSelect.bind(this)}
265. />
266. </div>
267. </When>
268. <Otherwise>
269. <div className="avatar-box">
270. <img src={profile.avatar} />
271. </div>
272. </Otherwise>
273. </Choose>
274. </div>
275. <Choose>
276. <When condition={this.props.state.edit} >
277. <div className="user-info-edit">
278. <input maxLength="20" type="text" value={profile.name}
279. onChange={this.handleInput.bind(this)} id="name"/>
280. <p className="user-info-username">
281. @{profile.userName}
282. </p>
283. <textarea maxLength="180" id="description"
284. value={profile.description}
285. onChange={this.handleInput.bind(this)} />
286. </div>
287. </When>
288. <Otherwise>
289. <div>
290. <p className="user-info-name">{profile.name}</p>
291. <p className="user-info-username">
292. @{profile.userName}
293. </p>
294. <p className="user-info-description">
295. {profile.description}</p>
296. </div>
297. </Otherwise>
298. </Choose>
299. </aside>

Página | 364
300. </div>
301. <div className="col-xs-12 col-sm-8 col-md-7
302. col-md-push-1 col-lg-7">
303. {this.props.children}
304. </div>
305. </div>
306. </div>
307. </div>
308. )
309. }
310. }
311.
312. const mapStateToProps = (state) => {
313. return {
314. state: state.userPageReducer
315. }
316. }
317.
318. export default connect(mapStateToProps,
319. {getUserProfile, chageToEditMode, cancelEditMode, updateUserPageForm,
320. userPageImageUpload, userPageSaveChanges, followUser, relogin})(UserPage)

A pesar de lo aparatoso de este archivo, verás que es lo mismo de siempre, pasar


la implementación de las funciones al archivo Actions.js y luego referencias las
funciones por medio de las props. Luego, remplazamos todas las referencias del
estado por las de las props.

Refactorizando el componente TweetReply

Este componente no requieres agregar nada nuevo, sino al contrario, deberemos


limpiar algunas cosas que ya no son necesarios. Veamos los cambios

1. import React from 'react'


2. import Reply from './Reply'
3. import Tweet from './Tweet'
4. import update from 'react-addons-update'
5. import APIInvoker from './utils/APIInvoker'
6. import PropTypes from 'prop-types'
7.
8. class TweetReply extends React.Component{
9.
10. constructor(props){
11. super(props)
12. }
13.
14. handleClose(){
15. $( "html" ).removeClass( "modal-mode");
16. $( "#dialog" ).html( "");
17. }
18.
19. addNewTweet(newTweet){
20. let request = {
21. tweetParent: this.props.tweet._id,
22. message: newTweet.message,
23. image: newTweet.image
24. }
25.
26. APIInvoker.invokePOST('/secure/tweet', request, response => {

365 | Página
27. this.handleClose()
28. },error => {
29. console.log("Error al cargar los Tweets");
30. })
31. }
32.
33. render(){
34.
35. let operations = {
36. addNewTweet: this.addNewTweet.bind(this)
37. }
38.
39. return(
40. <div className="fullscreen">
41. <div className="tweet-detail">
42. <i className="fa fa-times fa-2x tweet-close" aria-hidden="true"
43. onClick={this.handleClose.bind(this)}/>
44. <Tweet tweet={this.props.tweet} detail={true} />
45. <div className="tweet-details-reply">
46. <Reply profile={this.props.profile} operations={operations}/>
47. </div>
48. </div>
49. </div>
50. )
51. }
52. }
53.
54. TweetReply.propTypes = {
55. tweet: PropTypes.object.isRequired,
56. profile: PropTypes.object.isRequired
57. }
58.
59. export default TweetReply;

Dado que el profile lo necesitábamos para mandarlo al componente Reply y este


lo puede recuperar del store, ya no será necesario enviárselo. Por lo mismo, ya
no será necesario definir el propType del profile.

Refactorizando el componente TweetDetails

Lo primero que realizaremos será actualizar el archivo Actions.js para agregar


las nuevas operaciones y actions:

1. export const loadTweetDetail= (tweet) => (dispatch, getState) => {


2. APIInvoker.invokeGET('/tweetDetails/'+tweet, response => {
3. dispatch(loadTweetDetailRequest(response.body))
4. },error => {
5. console.log("Error al cargar los Tweets");
6. })
7. }
8.
9. const loadTweetDetailRequest = (tweetDetails) => ({
10. type: LOAD_TWEET_DETAIL,
11. tweetDetails: tweetDetails
12. })
13.
14. export const addNewTweetReply = (newTweetReply, tweetParentID) =>
15. (dispatch, getState) => {
16. let request = {
17. tweetParent: tweetParentID,

Página | 366
18. message: newTweetReply.message,
19. image: newTweetReply.image
20. }
21.
22. APIInvoker.invokePOST('/secure/tweet', request, response => {
23. newTweetReply._id = response.tweet._id
24. dispatch(addNewTweetReplyRequest(newTweetReply))
25. },error => {
26. console.log("Error al cargar los Tweets");
27. })
28. }
29.
30. const addNewTweetReplyRequest = (newTweetReply) => ({
31. type: ADD_NEW_TWEET_REPLY,
32. newTweetReply: newTweetReply
33. })

La función loadTweetDetails nos permite cargar el detalle del tweet, es decir,


todas las replica o comentarios que ha tenido el Tweet original. Cuando
obtenemos la respuesta del API, actualizamos el estado mediante el action
loadTweetDetailsRequest.

La función addNewTweetReply nos permite guardar la respuesta de un tweet en el


API. La respuesta son tweet como cualquier otro, pero esto tiene como padre al
tweet original. Nos apoyamos la función addNewTweetReplyRequest para actualizar
el estado una vez que el Tweet ha sido creado.

No olvidemos en agregar los imports correspondientes:

1. import {
2. LOGIN_SUCCESS,
3. LOGIN_ERROR,
4. UPDATE_LOGIN_FORM_REQUEST,
5. SIGNUP_RESULT_FAIL,
6. VALIDATE_USER_RESPONSE,
7. UPDATE_SIGNUP_FORM_REQUEST,
8. ADD_NEW_TWEET_SUCCESS,
9. LOAD_TWEETS,
10. LIKE_TWEET_REQUEST,
11. LIKE_TWEET_DETAIL_REQUEST,
12. UPDATE_REPLY_FORM,
13. RESET_REPLY_FORM,
14. LOAD_SUGESTED_USERS,
15. LOGOUT_REQUEST,
16. FIND_FOLLOWERS_FOLLOWINGS_REQUEST,
17. RESET_FOLLOWERS_FOLLOWINGS_REQUEST,
18. USER_PAGE_FOLLOW_USER,
19. USER_PAGE_SAVE_CHANGES,
20. USER_PAGE_AVATAR_UPDATE,
21. USER_PAGE_BANNER_UPDATE,
22. UPDATE_USER_PAGE_FORM_REQUEST,
23. CANCEL_EDIT_MODEL_REQUEST,
24. CHANGE_TO_EDIT_MODE_REQUEST,
25. USER_PROFILE_REQUEST,
26. LOAD_TWEET_DETAIL,
27. ADD_NEW_TWEET_REPLY
28. } from './const'

367 | Página
Constantes

Deberemos agregar las siguientes constantes al archivo const.js:

1. export const ADD_NEW_TWEET_REPLY = 'ADD_NEW_TWEET_REPLY'


2. export const LOAD_TWEET_DETAIL = 'LOAD_TWEET_DETAIL'

Reducer

En esta ocasión no será necesario crear un nuevo store, pues podemos utilizar
el TweetDetailReducer que habíamos creado para el componente Tweet. Solo que
será necesario agregar algunos actions para procesar las solicitudes del
componente TweetDetails.

Los siguientes cambios los deberemos agregar al archivo TweetDetailReducer:

1. import {
2. LOAD_TWEET_DETAIL,
3. ADD_NEW_TWEET_REPLY,
4. LIKE_TWEET_DETAIL_REQUEST
5. } from '../actions/const'
6. import update from 'react-addons-update'
7.
8. let initialState = null
9.
10. export const tweetDetailReducer = (state = initialState, action) => {
11. switch (action.type) {
12. case LOAD_TWEET_DETAIL:
13. return action.tweetDetails
14. case ADD_NEW_TWEET_REPLY:
15. return update(state, {
16. replysTweets: {$splice: [[0, 0, action.newTweetReply]]}
17. })
18. case LIKE_TWEET_DETAIL_REQUEST:
19. if(state._id === action.tweetId){
20. return update(state,{
21. likeCounter : {$set: action.likeCounter},
22. liked: {$apply: (x) => {return !x}}
23. })
24. }else{
25. let targetIndex =
26. state.replysTweets.map( x => {return x._id}).indexOf(action.tweetId)
27. return update(state, {
28. replysTweets: {
29. [targetIndex]: {
30. likeCounter : {$set: action.likeCounter},
31. liked: {$apply: (x) => {return !x}}
32. }
33. }
34. })
35. }
36. default:
37. return state
38. }
39. }
40.
41. export default tweetDetailReducer

Página | 368
El action LOAD_TWEET_DETAIL solo actualizará el estado para agregar los tweets
recuperados por el API, mientras que ADD_NEW_TWEET_REPLY agrega el nuevo
Tweet a la lista de tweets de respuesta del tweet original.

Migrando el component TweetDetails

Los siguientes cambios deberá aplicarse al archivo TweetsDetails.js:

1. import React from 'react'


2. import Reply from './Reply'
3. import Tweet from './Tweet'
4. import APIInvoker from './utils/APIInvoker'
5. import update from 'react-addons-update'
6. import { browserHistory } from 'react-router'
7. import PropTypes from 'prop-types'
8. import { loadTweetDetail, addNewTweetReply } from './actions/Actions'
9. import { connect } from 'react-redux'
10.
11. class TweetDetail extends React.Component{
12.
13. constructor(props){
14. super(props)
15. }
16.
17. componentWillMount(){
18. let tweet = this.props.params.tweet
19. this.props.loadTweetDetail(tweet)
20.
21. APIInvoker.invokeGET('/tweetDetails/'+tweet, response => {
22. this.setState( response.body)
23. },error => {
24. console.log("Error al cargar los Tweets");
25. })
26. }
27.
28.
29.
30. addNewTweet(newTweet){
31. let oldState = this.state;
32. this.props.addNewTweetReply(newTweet, this.props.params.tweet)
33.
34. let newState = update(this.state, {
35. replysTweets: {$splice: [[0, 0, newTweet]]}
36. })
37. this.setState(newState)
38.
39. let request = {
40. tweetParent: this.props.params.tweet,
41. message: newTweet.message,
42. image: newTweet.image
43. }
44.
45. APIInvoker.invokePOST('/secure/tweet', request, response => {
46. },error => {
47. console.log("Error al crear los Tweets");
48. })
49. }
50.

369 | Página
51. componentWillUnmount(){
52. $( "html" ).removeClass( "modal-mode");
53. }
54.
55. handleClose(){
56. $( "html" ).removeClass( "modal-mode");
57. browserHistory.goBack()
58. }
59.
60. render(){
61. $( "html" ).addClass( "modal-mode");
62.
63. let operations = {
64. addNewTweet: this.addNewTweet.bind(this)
65. }
66.
67. return(
68. <div className="fullscreen">
69. <Choose>
70. <When condition={this.props.state == null}>
71. <div className="tweet-detail">
72. <i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
73. </div>
74. </When>
75. <Otherwise>
76. <div className="tweet-detail">
77. <i className="fa fa-times fa-2x tweet-close"
78. aria-hidden="true"
79. onClick={this.handleClose.bind(this)}/>
80. <Tweet tweet={this.props.state} detail={true} />
81. <div className="tweet-details-reply">
82. <Reply profile={this.props.state._creator}
83. operations={operations}
84. key={"detail-" + this.props.state._id} newReply={false}/>
85. </div>
86. <ul className="tweet-detail-responses">
87. <If condition={this.props.state.replysTweets != null} >
88. <For each="reply" of={this.props.state.replysTweets}>
89. <li className="tweet-details-reply" key={reply._id}>
90. <Tweet tweet={reply} detail={true}/>
91. </li>
92. </For>
93. </If>
94. </ul>
95. </div>
96. </Otherwise>
97. </Choose>
98. </div>
99. )
100. }
101. }
102.
103. const mapStateToProps = (state) => {
104. return {
105. state: state.tweetDetailReducer
106. }
107. }
108.
109. export default connect(mapStateToProps,
110. {loadTweetDetail, addNewTweetReply})(TweetDetail);

Lo único nuevo que hemos agregado, es el método componentWillUnmount, con la


finalidad de asegurarnos de que el scroll de la aplicación regrese una vez que

Página | 370
cerramos la pantalla modal. El resto del componente tiene los cambios de
siempre, por lo que no entraremos en los detalles.

Últimas observaciones

Debido a que la migración ha implicado un impacto en todos los componentes de


esta aplicación, es muy seguro que, al momento de realizar la migración de
componente por componente, alguna cosa se nos pudiera haber ido, por esa
razón, si encuentras un problema, no dudes en comparar los componentes con
las versiones que tenemos en los repositorios de GitHub, así podrás estas seguro
de haber migrado todo correctamente. Si después de haber hecho esto, sigues
con problemas, te aconsejo que tomas la versión del repositorio, así podrás estar
100% seguro que todo funciona bien.

371 | Página
Resumen

Redux es sin duda una de las herramientas más potentes a la hora de desarrollar
aplicaciones con React, pues permite tener un control mucho más estricto del
estado y nos evitamos la necesidad de pasar una gran cantidad de propiedades
a los componentes hijos, haciendo mucho que el desarrollo y el mantenimiento
sea mucho menos complejo. De la misma forma, logramos desacoplar a los
componentes con su dependencia de los padres.

Redux puede llegar a ser un reto la primera vez que lo utilizamos, pero a medida
que nos acostumbramos a utilizarlo, resulta cada vez más difícil desarrollar sin
él.

A pesar que Redux es la herramienta por excelencia en el desarrollo web de hoy,


puede ser que nuevas herramientas nazcan y ofrezcan mejores soluciones, si
como en su momento fue Flux, hoy es Redux, y mañana puede ser otra cosa, lo
importante es estar actualizados y está siempre abierto a nuevas opciones.

Quiero felicitarte si has llegado hasta aquí, quiere decir que ya has aprendido
prácticamente todo lo necesario para crear aplicaciones con React. Si bien,
todavía no vemos la parte del backend con NodeJS, quiero recordarte que React
es una librería totalmente diseñada para el FrontEnd, por lo que en este punto,
ya podrías llamarte un FrontEnd developer.

Página | 372
Introducción a NodeJS
Capítulo 13

Hasta este momento, hemos construido una aplicación completa utilizando React
y conectándola a un API REST, sin embargo, poco o nada hemos visto acerca de
NodeJS y como este juega un papel crucial en el desarrollo de aplicaciones web.

Porque es importante aprender NodeJS

NodeJS se ha venido convirtiendo rápidamente en una de las tecnologías más


populares para las grandes empresas, incluso, muchas de las Startups que están
naciendo, están utilizando NodeJS como parte de su Stack tecnológico, pues les
permite desarrolla soluciones rápidamente y con un costo de despliegue muy
económico. Incluso, muchos inventos que utilizan hardware como Arduino o
RaspberryPI están utilizando NodeJS para ejecutar el código que los hace
funcionar, pues una de las principales ventajas que tiene NodeJS es que es
extremadamente ligero y súper eficiente en el uso de los recursos.

Ahora bien, puede que sea una persona que no piensa emprender en este
momento y lo que busques es aprender una nueva tecnología para buscar un
mejor trabajo. En ese caso, déjame decirte que NodeJS es ya hoy en día una de
las tecnologías más buscada por las grandes empresas como Google, Amazon,
Microsoft, etc. Esto quiere decir que aprender NodeJS es sin duda una de las
mejores inversiones que puedes hacer.

373 | Página
Fig. 148 - Posiciones abiertas

La imagen es una gráfica publicada por indeed.com, una famosa página de


trabajos, en la cual se aprecia las posiciones abiertas para las principales
tecnologías web. Lo interesante de esta gráfica, es la forma tan explosiva que ha
crecido la demanda de NodeJS. Esta gráfica nos da una idea bastante clara de lo
que está pasando en el mercado.

El Rol de NodeJS en una aplicación

Como ya lo mencionamos al inicio de este libro, NodeJS es un entorno de


ejecución de JavaScript del lado del servidor, lo que implica que cualquier cosa
que se pueda programar en JavaScript se podrá ejecutar en NodeJS. Eso lo hace
una herramienta muy potente, pero seguramente cuando escuchas eso, se te
venga a la mente que NodeJS crea interfaces gráficas, pues JavaScript es
utilizado para eso habitualmente.

Sin embargo, por muy extraño que parezca, NodeJS no es utilizado para eso,
pues no es un navegador, por lo que no puede renderizar elementos en pantalla,
lo que sí, es que mediante NodeJS podemos servir páginas web al navegador.
Pero no solo queda allí la cosa, puede ser utilizado para aplicaciones no web y
ser montado en sistemas embebidos, o en nuestro caso, nos puede servidor como
base para crear todo un API REST.

En nuestro caso, usaremos NodeJS con dos propósitos, el primero y más claro
hasta el momento, es crear nuestra API REST y el segundo, lo utilizamos para
servir nuestra aplicación Mini Twitter al cliente como ya lo hemos estado haciendo
hasta el momento.

Página | 374
NodeJS es un mundo

NodeJS junto con su todo su mundo de librerías que ofrece NPM puede llegar a
ser abrumador, pues NPM es el repositorio de librerías Open Source más grande
del mundo. Debido a esto, es imposible hablar de todo lo que nos tiene por
ofrecer NodeJS o al menos lo más importante. Por esta razón, nos centraremos
exclusivamente en el desarrollo de API’s con NodeJS y explicaremos alguna que
otra curiosidad que sea necesaria a medida que sea requerido.

Hoy en día hay muchísimos libros que hablan exclusivamente de NodeJS o de


alguna de sus librerías, y a pesar de que son libros exclusivos de NodeJS, dejan
muchas cosas por fuera, ya que NodeJS es un mundo.

Introducción a Express

Como ya lo platicamos, nos centraremos en el desarrollo de APIs con NodeJS, y


Express es sin duda una de las librerías por excelencia para el desarrollo web y
la construcción de API en NodeJS. Express es definido en página oficial
(http://expressjs.com/es/) como:

Infraestructura web rápida, minimalista y flexible para Node.js

Algo que debemos de tomar en cuenta, es que, NodeJS solo el entorno de


ejecución de JavaScript, mientras que Express una librería que complementa a
NodeJS para el desarrollo de aplicaciones web y desarrollo de API’s. Por sí solo,
NodeJS solo es capaz de ejecutar JavaScript, pero no cuenta con las librerías
necesarias para desarrollar aplicaciones web.

En la actualidad existe más librerías para el desarrollo web y API’s con NodeJS,
lo que quiere decir que Express no es la única opción que tenemos. En realidad,
existen tantas opciones que es muy fácil perderse. Solo para nombrar algunas
alternativas están:

 Koa (http://koajs.com/)
 Hapi (https://hapijs.com/)
 Restify (http://mcavage.me/node-restify/)
 Sailsjs (https://sailsjs.com/)
 Strapi (https://strapi.io/)

Estos son tan solo algunos ejemplos rápidos, pero existe una infinidad de librerías
más que nos puede servir para este propósito, por lo que alguien recién llegado
a NodeJS simplemente no sabría cual elegir

375 | Página
Ahora bien, ¿Por qué deberíamos utilizar Express en lugar de cualquier otra?, la
respuesta es simple, Express es la librería más ampliamente utilizada y con la
mayor comunidad de desarrolladores, esto hace que este en constante evolución
y madure a una velocidad más rápida que las demás. Ahora bien, nada está
escrito en esta vida, por lo que siempre hay que estar atento a las cosas que
pasan, pues cualquier día de estos, otra librería pueda superar a Express, pero
por ahora, Express es la más conveniente.

Instalando Express

Una vez que te he convencido de usar Express (o al menos eso creo) pasaremos
a la instalación. Dado que Express es una librería más de NodeJS, esta puede ser
instalada mediante NPM como lo hemos estado haciendo para el resto te librerías
que hemos estado utilizando hasta el momento. Para ello, solo basta con instalar
usando el siguiente comando:

npm install –save [email protected]

Esta instalación ya la habíamos realizado cuando hablamos de Router, por lo que


es probable que al ejecutarlo, no veas ningún cambio en el archivo package.json.

El archivo package.json

Este archivo ya lo hemos explicado en el pasado, sin embargo, como estamos en


la sección de NodeJS, sería bueno dar una repasada, pues este archivo es el más
importante cuando trabajamos con NodeJS.

Muchas personas creen que el archivo package.json, es solo para colocar las
dependencias y configurar algunos scripts para ejecutar el programa, sin
embargo, esto va más allá. Este archivo está pensado para ser un identificador
de nuestro proyecto, pues este, al ser compilado, pasa a ser una librería, y como
toda librería, es posible subirla a los repositorios de NPM. Espera un momento
¿Me estás diciendo que yo puedo crear y subir mi proyecto como una librería a
NPM?, es correcto, nosotros podríamos ahora mismo crear una nueva librería, ya
sea un proyecto completo o una simple utilidad y publicarla para que todo el
mundo la pueda descargar, y por qué no, contribuir en su desarrollo.

Ahora bien, ya que sabemos que el archivo package.json es un descriptor de


nuestro proyecto, debemos de saber que existen ciertas reglas que hay que
seguir. Las más importantes son tener un nombre y una versión, pues estos dos
campos son el identificador de nuestro proyecto.

name

Página | 376
Las reglas para el nombre (name) son las siguientes:

 El nombre debe ser menor o igual a 214 caracteres.


 El nombre no puede comenzar con un punto o un guion bajo.
 Los nuevos paquetes no deben tener letras mayúsculas en el nombre (no
CamelCase).
 El nombre termina siendo parte de una URL, un argumento en la línea de
comando y un nombre de carpeta. Por lo tanto, el nombre no puede
contener ningún carácter que no sea seguro para URL.

Al momento de establecer un nombre para nuestro proyecto, podremos elegir el


nombre que sea, siempre y cuando cumpla con las reglas anteriores, sin
embargo, si nuestro propósito es subir esta librería a NPM, deberemos validar
antes, que el nombre (name) no esté siendo utilizado ya por otra librería. Si este
fuera el caso, tendremos que considerar otro nombre, pues NPM no nos permitirá
subirlo.

version

La versión es el segundo campo más importante, pues junto con el nombre


crearán un identificador único de la librería. La importante de la versión, es que
nos ayuda a identificar la versión del componente, a la vez que nos permite tener
múltiples versiones publicadas.

En teoría, los cambios en el paquete deben venir junto con los cambios en la
versión, esto significa que cada vez que realicemos un cambio en el módulo, por
más simple que parezca, tendremos que aumentar la versión.

La versión es un valor numérico, que puede estar separado por secciones, estas
secciones se marcan con un punto “.”, de tal forma que podemos tener versiones
como las siguientes:

 1
 1.0
 1.0.1
 1.1.0.1
Cualquier combinación es válida, pero es importante entender cómo administrar
las versiones. Por ese motivo, aquí te enseñaremos la nomenclatura más
utilizada.

La más utilizada se divide en 3 bloques “Cambios mayores”.”Cambios


menores”.”bugs”:

377 | Página
 Cambios mayores: Son cambios en la librería que tiene un gran
impacto y que por lo general rompen con la compatibilidad con versiones
anteriores. Estos tipos de cambios incluyen cambios en el
funcionamiento de algunos de sus componentes, cambios en las
interfaces expuestas o incluso, un cambio en la tecnología utilizada.
 Cambios menores: Son cambios o adición de nuevos features que se
agregan a los ya existentes, sin romper la compatibilidad. Dentro de
estos cambios podemos encontrar, la adición de nuevos métodos o
funciones, nuevas capacidades de la librería, optimizaciones o
reimplementacion de funcionalidades encapsuladas que no afectan al
usuario final.
 Bugs: Esta sección la incrementamos cada vez que corregimos uno o
una serie de bugs, pero sin agregar o remplazar funcionalidad,
simplemente son correcciones. La clave en esta sección es que no
debemos incluir nuevos features, si no correcciones a los existentes.

description

Este es solo un campo que nos permite poner una breve descripción de lo hace
nuestro paquete. Es utilizado como una guía para que las personas puedan saber
qué hace tu proyecto y es utilizado por NPM para realizar búsquedas.

Keywords

Es un campo en donde pones palabras claves, las cuales identifican mejor a


nuestra librería. También es utilizada por NPM para realizar búsquedas.

bugs

Aquí es posible definir una URL que nos lleve a la página de seguimiento de bugs
y también es posible definir un email para contactar al equipo de soporte.

1. {
2. "url" : "https://github.com/owner/project/issues",
3. "email" : "[email protected]"
4. }

autor

Campo que nos permite poner el nombre del autor de la librería, puede ser el
nombre del desarrollador o la empresa que lo está desarrollando.

Página | 378
license

Es posible determinar la licencia que tiene nuestra librería, actualmente existe


un catálogo de posibles licencias (https://spdx.org/licenses/).

scripts

Este es un campo muy importante, pues nos permite crear scripts para compilar,
construir, deployar y ejecutar la aplicación, sin embargo, este campo es complejo
y requiere de un entendimiento más avanzado para comprender todas sus
posibilidades. Para ver la documentación completa acerca de cómo construir
script puedes ir a la siguiente URL (https://docs.npmjs.com/misc/scripts).

devDependencies

Aquí se enlistan todos los módulos que son requeridos para ejecutar la aplicación
en modo desarrollo, estos módulos estarán disponibles solo cuando la aplicación
no se ejecute en modo productivo.

Campo dependencies

Aquí definimos las librerías indispensables para correr nuestra aplicación, ya sea
en modo desarrollo o productivo. Esto quiere decir que estas librerías son
indispensables en todo momento.

Documentación de package.json

Dado que este archivo es bastante extenso, te dejo la liga a la documentación


oficial del archivo package.json, por si quieres profundizar en todas las cosas que
tiene por ofrecernos.

Node Mudules

A estas alturas del libro, seguramente ya entiendes a la perfección que son los
módulos de Node, pero solo para dejarlo claro. Los módulos son todos aquellos
paquetes que descargamos con ayuda del comando install de NPM.

Los paquetes que descargamos se guardan en una carpeta llamada node_modules,


la cual se crea automáticamente al momento de instalar cualquier librería, sin
embargo, si descargaste el código del repositorio, es posible que no logres ver la
carpeta directamente sobre Atom, y esto se debe a una configuración de Atom
para ocultar las carpetas que están siendo ignoradas por GIT. Existen dos

379 | Página
opciones para visualizar esta carpeta, la primera y más simple, es verla
directamente sobre el explorar de archivos de tu sistema operativo, es decir,
diriges físicamente a la carpeta del proyecto y la podrás ver:

Fig. 149 - Carpeta node_modules.

La otra opción es realizar un simple configuración en Atom, la cual consiste en ir


a la opción File  Config..,

Esto nos abrirá un archivo de texto, en el cual deberemos cambiar el valor de la


propiedad hideVcsIgnoredFiles de true a false, tal cual se muestra en la siguiente
imagen:

Fig. 150 - Actualización de la propiedad hideVcsIgnoreFiles.

Página | 380
Tras hacer esto, en automático podrás ver la carpeta node_modules en el árbol
del proyecto:

Fig. 151 - Carpeta node_module en Atom

Yo por lo general prefiero tenerlo oculto, pues es raro que necesite ver la carpeta,
además, al hacer eso, también me muestra la carpeta .get, en la cual se guarda
todo el historial de cambios del repositorio de GIT.

Como sea, lo interesante es que en esta carpeta podremos ver todos los módulos
que vamos instalando en nuestro proyecto y que por ende, estarán disponibles
en tiempo de ejecución. Solo por nombrar algunos ejemplos, podrás encontrar
las carpetas React, React-redux, React-router, jsx-control-statements, etc.

Algo importante a notar, es que el nombre de la carpeta corresponde con el


atributo name del archivo package.json de cada módulo.

Creando un servidor de Express

Ya con un entendimiento más claro de lo que es NodeJS y como los paquetes son
administrados, pasaremos a implementar nuestro primero servidor con NodeJS.
En el pasado ya habíamos creado el archivo server.js con la intención de
soportar las reglas de route de React-router, pero no habíamos entrado en los
detalles.

Ahora bien, para aprender desde cero, crearemos un nuevo archivo llamado
express-server.js con la intención de hacer algunas pruebas y no perder el

381 | Página
archivo server.js que ya tenemos funcionando. La intención es usar este nuevo
archivo solo durante este capítulo, después de esto, podremos borrarlo.

Iniciemos creando el archivo express-server.js en la raíz del proyecto, el cual


deberá tener el siguiente contenido:

1. var express = require('express');


2. var app = express();
3.
4. app.get('/*', function (req, res) {
5. res.send("Hello world");
6. });
7.
8. app.listen(8181, function () {
9. console.log('Example app listening on port 8181!');
10. });

Tan solo con estas pocas líneas hemos creado un servidor Express que responde
en el puerto 8181. Hemos cambiado de puerto para evitar que choque con el que
ya tenemos configurado por el proyecto Mini Twitter.

Veamos que está pasando, en la línea 1 estamos importando el módulo de


Express, el cual instalamos mediante el comando npm install –save express.
En la línea 2, estamos creando una nueva instancia de Express que ponemos en
la variable app.

En la línea 4 estamos utilizamos algo llamado Methods (métodos) con el cual es


posible registrar routeadores que procesarán las peticiones entrantes. En este
caso, le estamos indicando a Express que todas las peticiones que lleguen por el
método GET serán atendidas por este Router y como consecuencia regresará la
palabra “Hello world”). Un poco más adelante entraremos en detalles acerca de
cómo funcionan los métodos. Por ahora, es suficiente saber que el “*” indica
cualquier URL y los parámetros req y res, corresponde al request y response
respectivamente.

Finalmente, en la línea 8, creamos un oyente, con la intención de atender todas


las peticiones en el puerto 8181. Este último paso es el que inicia el servidor y lo
hace disponible para ser accedido desde el navegador.

Página | 382
Fig. 152 - Hello world con Express.

En la imagen anterior, ya podemos observar nuestro servidor respondiendo a


nuestras peticiones.

Express Verbs

Algo sumamente importante a la hora de trabajar con Express y el desarrollo de


API’s, es conocer los distintos métodos a los cuales puede responder Express.
HTTP Verbs es como se les conoce a los diferentes métodos que soporta un
servidor HTTP para comunicarse. El termino Verbs suele ser sustituido por
métodos, por lo que utilizaremos el nombre métodos para referirnos a ellos.

Cuando una aplicación se comunica con un servidor HTTP, este le tiene que
indicar que método utilizará para la comunicación, pues cada uno de ellos tiene
una estructura diferente. La principal diferencia que radica entre cada uno de
ellos, es la interpretación que le da el servidor a cada uno de ellos.

El protocolo HTTP así como Express, soportan una gran cantidad de método, sin
embargo, los más utilizados son 4, y el resto es utilizado para cosas más
específicas que no tendría caso comentar ahora. Si quieres ver la lista completa,
puedes verla en la documentación oficial de Express.

Método GET

Este es el método más utilizado por la WEB, pues es el que usa el navegador
para consultar una página a un servidor. Cuando entramos a cualquier página,
ya sea Facebook, Google o Amazon, el navegador lanza una petición GET al
servidor y este le regresa el HTML correspondiente a la página.

383 | Página
Las peticiones no llevan un payload asociado a la petición, si no que la URL es lo
único necesario para que el servidor HTTP sepa qué hacer con ella. En resumidas
cuentas, el método GET es utilizado como un método de consulta.

Método POST

El método POST es el segundo método más utilizado por la WEB, pues permite
enviarle información al servidor, como puede ser el formulario de una página,
una imagen, un JSON, XML, etc. Mediante la barra del navegador es imposible
enviar peticiones POST, pero si es posible mediante programación, formularios o
programas especializados para probar recursos web como es el caso de SoapUI.

Los servidores entiendan la petición POST como un método de creación, es


decir, que cuando se manda un POST, este interpreta que la información deberá
ser utilizada para crear un registro del lado del servidor.

Método PUT

El método PUT no es muy utilizado por los navegadores, al menos no de forma


natural. Este método es utilizado para reemplazar por completo un registro
con los nuevos valores que son enviados en el Payload, lo que indica que este
método si puede tener un payload asociado a la petición.

Método DELETE

Delete tampoco es utilizado por el navegador de forma natural, y es utilizado


para indicar que un registro debe de ser eliminado, este método soporta el
envío de un payload asociado a la petición.

Consideraciones adicionales.

HTTP hace una serie de recomendación de cómo los métodos se deben utilizar,
sin embargo, esto no es garantía que se cumpla, pues perfectamente podrías
mandar una petición DELETE para crear un nuevo registro, o un POST para
actualizar o un GET para borrar. Cuando entremos de lleno a la creación de
nuestros servicios REST retomaremos este tema y veremos las mejores prácticas
para la creación de servicios. Por ahora, basta con que comprendas teóricamente
como deberían de funcionar.

Página | 384
Implementemos algunos métodos.

Para comprender un poco cómo funcionan los métodos, crearemos un router que
procese las solicitudes de los métodos que analizamos hace un momento, para
ello, agregaremos las siguientes líneas a nuestro archivo express-server.js:

1. var express = require('express');


2. var app = express();
3.
4. app.get('*', function (req, res) {
5. res.send("Hello world GET");
6. });
7.
8. app.post('*', function (req, res) {
9. res.send("Hello world POST");
10. });
11.
12. app.put('*', function (req, res) {
13. res.send("Hello world PUT");
14. });
15.
16. app.delete('*', function (req, res) {
17. res.send("Hello world DELETE");
18. });
19.
20. app.listen(8181, function () {
21. console.log('Example app listening on port 8181!');
22. });

Lo que hemos hecho es muy simple, hemos agregado 3 nuevos routers que
procesan las solicitudes para POST (línea 8), PUT (línea 12), DELETE (línea 16),
lo que significa que cuando entre cualquier petición al servidor por cualquiera de
estos métodos, será procesado por el router apropiado.

Observa que tan solo es necesario utilizar app.<method> para crear un router para
cada método, y el * indica que puede procesar cualquier URL entrante, y no solo
la raíz.

Cada método recibe dos parámetros, el path y un Callback, el path, corresponde


a la URL a la que puede responder, por lo que solo se ejecutará si el path se
cumple. El segundo parámetro es una función que será ejecutada si el path se
cumple, esta función recibe dos parámetros, el primero es el request y el segundo
es el response.

Ahora bien. Para probar esto, podemos utilizar SoapUI y ejecutar la URL
http://localhost:8181 en los cuatro métodos disponibles:

385 | Página
Fig. 153 - Probando el método GET

Fig. 154 - Probando el método POST

Página | 386
Fig. 155 - Probando el método PUT.

Fig. 156 - Probando el método DELETE.

387 | Página
Como hemos visto en las imágenes anteriores, ante la misma URL pero con
método diferente, obtenemos un resultado diferente, pues el router que procesa
la solicitud es diferente.

Con esto, nos debe quedar claro que podemos atender la misma URL pero con
distintos comportamientos, porque una misma URL podría hacer cosas diferentes
con tan solo cambiar el método, y es allí donde radica la magia del API REST.

Trabajando con parámetros

Cuando la WEB nació a principio de los 80’s, jamás se imaginó el alcance que
tendría y como esta evolucionaría para crea aplicaciones tan complejas como lo
son hoy en día. En sus inicios, todas las URL a los recursos de internet, eran
meramente un enlace a un documento alojado en otro servidor o directorio del
mismo servidor, y la URL como tal, era irrelevante, por lo que nos encontrábamos
links como los siguientes:

 http://server.com/?page=index
 http://server.com/121202/134%2023.html

Estas URL, si bien, funcionan, la realidad es que no son nada descriptivas, pues
no te da ninguna idea de lo que va hace o donde te van a enviar.

Si lo que buscamos es desarrollar un API fácil de utilizar, tenemos que tener


mucho cuidado al memento de definir las URL, ya que la URL por sí solo, debería
se darnos una idea bastante clara de lo que hace.

Query params

La forma más fácil de enviar parámetros al servidor, es mediante Query params,


los cuales consisten en una serie de key=value (propiedad= valor) que se
agregan al final de una url:

http://api.com/?param1=val1&param2=val2&param3=val3

Cada parámetro debe estar separado con un ampersand (&) y debe de anteponer
un signo de interrogación (?) antes de iniciar con los parámetros.

Para recuperar un query param en Express solo tenemos que ejecutar la siguiente
instrucción req.query.<param-name>, veamos el siguiente ejemplo:

1. app.get('*', function (req, res) {


2. const name = req.query.name

Página | 388
3. const lastname = req.query.lastname
4. res.send("Hello world GET => " + name + ' ' + lastname);
5. });

Hemos actualizado el archivo express-server.js para agregar las siguientes


líneas en el método GET, con la finalidad de recuperar los parámetros name y
lastname, seguido de eso, los concatenamos en la respuesta. Para probar estos
cambios, ejecutaremos la siguiente URL en el navegador:

http://localhost:8181/?name=Oscar&lastname=Blancarte

Fig. 157 - Express query params.

URL params

Los URL params, son parámetros que se pueden pasar por medio de la misma
URL y no estamos hablando de los Query params, en su lugar, las mismas
secciones de una URL se pueden convertir en parámetros, por ejemplo, en el
proyecto Mini Twitter, usamos un servicio para consultar el perfil de un usuario,
para esto, ejecutar una URL como la siguiente: http://api.com/profile/test, en
esta URL, test, es un URL Param, y puede ser recuperado para ser utilizado como
un parámetro.

En Express, es posible definir estos parámetros anteponiendo dos puntos ( : ),


antes de cada sección, por ejemplo, para consultar el perfil, podemos crear un
path con el siguiente formato ( /profile/:username ), adicional, estos parámetros
pueden ser recuperados mediante el request de la siguiente manera: (
req.params.<param-name> )

Actualizaremos nuevamente el archivo express-server.js y agregaremos las


siguientes líneas:

1. app.get('/:name/:lastname', function (req, res) {


2. const name = req.params.name
3. const lastname = req.params.lastname
4. res.send("Hello world GET => " + name + ' ' + lastname);
5. });

389 | Página
Es muy importante que este nuevo router este por arriba del router GET que ya
teníamos, pues como el otro acepta todas las peticiones, no dejará que esta
nueva se procese.

Ahora bien, vemos que hemos cambiado el path para aceptar dos url params, los
cuales son name y lastname, luego estos son recuperados mediante
req.params.name y req.params.lastname.

Ejecutaremos nuevamente los cambios, pero esta vez, utilizaremos la URL:

http://localhost:8181/Oscar/Blancarte

Y el resultado será el siguiente:

Fig. 158 - Express URL params.

Body params

La última forma de enviarle parámetros a Express es mediante el payload y solo


está disponible para los métodos que lo soportan, como es POST, PUTH, DELETE.

En la práctica, el payload no es tan simple de obtener, pues en realidad no es


que llegue junto al mensaje, en su lugar, el payload es enviado por como un
Input Stream, lo que quiere decir que se empieza a recibir por partes hasta
completar todo el mensaje.

Para superar este problema, tendríamos que definir un Middleware, el cual cache
el mensaje primero que los Router y luego procese el payload para el final dejarlo
en el objeto request. Esto quedaría de la siguiente manera:

1. app.use(function(req, res, next){


2. var data = "";
3. req.on('data', function(chunk){ data += chunk})

Página | 390
4. req.on('end', function(){
5. req.body = data;
6. next();
7. })
8. })

Más adelante veremos qué es esto de los Middleware. Ahora bien, eso que
estamos viendo en pantalla no lo vamos a requerir, porque existe una librería
que nos facilita la vida y que adicional, nos convierte el mensaje a JSON o al tipo
que necesitemos.

Body-parse module

La librería body-parse una utilidad que nos ayuda a gestionar el payload,


mediante el cual es posible convertir el payload a el formato que necesitemos y
dejarlo disponible en el objeto request.

Para instalar esta librería bastará con ejecutar el siguiente comando:

npm install --save body-parser

Una vez instalado, actualizaremos el archivo express-server.js para agregar las


siguientes líneas:

1. var express = require('express');


2. var app = express();
3. var bodyParser = require("body-parser")
4.
5. app.use(bodyParser.urlencoded({extended: false}));
6. app.use(bodyParser.json({limit:'10mb'}));
7.
8. app.get('/:name/:lastname', function (req, res) {
9. const name = req.params.name
10. const lastname = req.params.lastname
11. res.send("Hello world GET => " + name + ' ' + lastname);
12. });
13.
14. app.get('*', function (req, res) {
15. const name = req.query.name
16. const lastname = req.query.lastname
17. res.send("Hello world GET => " + name + ' ' + lastname);
18. });
19.
20.
21. app.post('/login', function (req, res) {
22. const body = req.body
23. res.send(body);
24. });
25.
26. app.post('*', function (req, res) {
27. res.send("Hello world POST");
28. });
29.

391 | Página
30. app.put('*', function (req, res) {
31. res.send("Hello world PUT");
32. });
33.
34. app.delete('*', function (req, res) {
35. res.send("Hello world DELETE");
36. });
37.
38. app.listen(8181, function () {
39. console.log('Example app listening on port 8181!');
40. });

En la línea 3 estamos haciendo el import al módulo. En la línea 5 estamos


registrando el body-parse como un middleware y le estamos indicando que no
queremos un formato extendido en la codificación. En la línea 6 le indicamos que
una vez que el payload sea recuperado por el paso anterior, este lo convierta en
JSON, le indicamos que los mensajes no deberán exceder los 10 megas
(mensajes mayores producirán error).

Finalmente, en la línea 21 definimos un nuevo router que atenderá las peticiones


en el path /login, el cual responderá con el payload enviado.

Ejecutemos este ejemplo para ver los resultados:

Fig. 159 - Express parse-body

Observa que en la respuesta nos hemos colocado en la pestaña JSON, pues el


mensaje de respuesta tiene este formato.

Middleware

Página | 392
Los Middleware son funciones comunes y corrientes que tiene la particularidad
de ejecutarse antes que los routings de los métodos tradicionales. Los Middlware
los podemos ver como interceptores que toman la ejecución antes que los demás,
hacen cualquier cosa y luego pasan la ejecución al siguiente middlware, al
termina la cadena de Middleware, la ejecución pasa al routing apropiado para
atender la petición.

Fig. 160 - Ejecución en cadena de Middleware.

Cuando un Middlware toma la petición, tiene acceso total al objeto request y


response, por lo que puede manipular la información antes que este llegue al
routing final, el cual no se dará cuenta de cómo venía el mensaje original.

Las funciones middleware reciben al menos tres parámetros, el request (req),


response (res) y next, esta última es una referencia al siguiente Middleware de
la lista. El middleware tiene la responsabilidad de ejecutar next() al finalizar la
ejecución, pues de lo contrario dejara colgada la petición del cliente.

Otra de las características que tiene el Middleware, es que puede responder


directamente al cliente, sin la necesidad de pasar por el router. Un ejemplo de
esto es la seguridad, si no nos autenticamos, un middleware puede rechazar la
petición sin ni siquiera llevar al router adecuado. Solo en los casos que el
middleware responda al cliente, puede no ejecutar next().

393 | Página
Existe 5 tipos de middleware soportados por Express:

 Middleware de nivel de aplicación


 Middleware de nivel de direccionador
 Middleware de terceros
 Middleware incorporado
 Middleware de manejo de errores

De los 5 listados analizaremos los primeros 4, y en la siguiente sección


analizaremos los de manejo de errores.

Middleware de nivel de aplicación

Los middlewares de aplicación son aquellos que definimos mediante app.use(),


y que adicional definimos nosotros mismo la implementación de la función
Callback

1. var app = express();


2.
3. app.use(function (req, res, next) {
4. console.log('Time:', Date.now());
5. next();
6. });
7.
8. app.use('/user/:id', function (req, res, next) {
9. console.log('Request Type:', req.method);
10. next();
11. });

El primer middleware (línea 3) no tiene un path, por lo que se ejecutará ante


cualquier path sin importar el método.
El segundo middleware es más específico, pues solo se ejecutará cuando la URL
cumpla con el path definido.

Middleware de nivel de direccionador

El middleware de nivel de direccionador funciona de la misma manera que el


middleware de nivel de aplicación, excepto que está enlazado a una instancia
de express.Router(). Este tipo de Middleware es utilizado para agrupar una serie

Página | 394
de reglas de ruteo en un solo objeto, con la finalidad de facilitar su administración
a medida que crece el número de routeos.

Más adelante veremos cómo este tipo de middleware nos ayudará a separar los
routeos que van a la aplicación Mini Twitter de las que van al API.

1. var app = express();


2. var router = express.Router();
3.
4. router.get('/api/profile', function (req, res, next) {
5. //Any action
6. });
7.
8. router.get('/api/user', function (req, res, next) {
9. //Any action
10. });
11.
12. app.use('/api', router);

En el ejemplo anterior vemos ver cómo estamos creando una serie de routeos
pero por medio del objeto router, luego, este objeto es utilizando para crear un
nuevo middleware (línea 12) que atiende en la URL /api.

Middleware de terceros

Los middlewares de terceros son aquellos que están implementados en una


librería externa y que solo hace falta registrarla. Un ejemplo de estos, es body-
parser, recordemos como lo implementamos:

1. var express = require('express');


2. var app = express();
3. var bodyParser = require("body-parser")
4.
5. app.use(bodyParser.urlencoded({extended: false}));
6. app.use(bodyParser.json({limit:'10mb'}));

Observemos como en la línea 5 y 6 estamos haciendo uso de bodyParser para


crear dos middlewares, uno que recupera el payload y el segundo que lo convierte
en json. Este tipo de middleware son también vistos como plugins, pues solo
hace falta registrarlos para que empiecen a funcionar.

Para profundizar más acerca de los middleware, puedes entrar a revisar la


documentación oficial en http://expressjs.com/es/guide/using-middleware.html.

Middleware incorporado

Como su nombre lo indica, estos corresponden a los Middleware que ya viene


incorporados a Express si necesidad de instalar un módulo aparte. Sin embargo,
a partir de la versión 4.x de Express, todos los middlewares incorporados fueron

395 | Página
separados en módulos independientes. Como es el caso de body-parser que
anteriormente venía incorporado, pero hoy en día es un módulo independiente.

Hoy en día, el único middleware incorporado es express.static, el cual sirve


para exponer recursos estáticos. Los recursos estáticos son archivos como el
index.html, styles.css, bundle.js, todos aquellos que queramos que sea
accesibles públicamente sin necesidad de definir un routing específico para cada
archivo y que además su contenido no cambia en el tiempo.
Un ejemplo típico de su utilidad, es para exponer todo el contenido de la carpeta
public.

1. app.use(express.static(path.join(__dirname, 'public')));

En este ejemplo, registramos el middleware static, el cual recibe como parámetro


un path, el cual es la ruta a la carpeta public del mismo proyecto. path.join es
una utilidad que se utiliza para unir dos direcciones, __dirname es la ruta del
proyecto y ‘public’ es el nombre de la carpeta a publicar.

Este ejemplo da como resultado la exposición pública a todos los archivos


contenidos en la carpeta public, por lo que hay que tener cuidado de que
información dejamos aquí.

Error Handler

Un error handler son un tipo especial de Middleware, pues permiten gestionar los
errores producidos en tiempo de ejecución. Un error handlers se definen
exactamente igual que los middlewares a nivel de aplicación, pero con la
diferencia de que estos reciben 4 parámetros, donde el primero es el error (err),
el segundo el request (req), el tercero el response (res) y el cuarto es el next.

1. app.use("/api", function(err, req, res, next){


2. //Any action
3. });
4.
5. app.use(function(err, req, res, next){
6. //Any action
7. });

Es posible definir handlers globales (línea 5) o específicos para una path


determinado (línea 1), la única diferencia, es que el global no tiene ningún path
asociado y el especifico sí.

Ahora bien, es posible tener más de un handler global y más de uno específico
para el mismo path, lo que pasará es que se ejecutarán en el orden en que fueron
definidos, pero siempre y cuando, el handler anterior ejecute next().

Puedes ver la documentación completa de Express para el tratamiento de errores


en la documentación oficial ( http://expressjs.com/en/guide/error-
handling.html).

Página | 396
Resumen

En este capítulo hemos analizado hemos analizado las principales características


que nos ofrece NodeJS + Express para la construcción de aplicaciones WEB y
API´s.

También hemos aprendido acerca de los verbs o métodos disponibles y como


estos deben de ser utilizados.

Hemos aprendido las distintas formas que tiene express para recibir parámetros,
mediante url params, query params y body param o payload.

Hemos aprendido a crear nuestros propios middlewares y a utilizar los


middlewares provistos por terceros.

Sin duda, este capítulo nos deja listos para empezar a construir el API, pero antes
de iniciar con eso, nos introduciremos a MongoDB para conocer cómo funciona y
aprender a conectarnos desde NodeJS.

397 | Página
Introducción a MongoDB
Capítulo 14

En el primer capítulo de este libro nos introdujimos al mundo de MongoDB para


hablar a grandes rasgos de cómo es que esta funciona y las principales
diferencias que tiene contra el modelo SQL tradicional. Después, en el capítulo
5, aprendimos a crear una base de datos Mongo en la nube. Finalmente, en este
capítulo profundizaremos más en el tema, y aprenderemos a utilizar las
operaciones más importantes de MongoDB y veremos cómo utilizarlo en conjunto
con NodeJS.

Porque es importante aprender MongoDB

Como siempre, me gusta explicarle por qué MongoDB es una tecnología que vale
la pena aprender y cómo es que esta está ganando popularidad rápidamente. A
pesar de que MongoDB ha venido subiendo rápidamente en popularidad, la
realidad es que mucha gente todavía se siente desconfiada de usar esta base de
datos, pues rompe completamente con los paradigmas que durante años se nos
ha enseñado.

Temas como transaccionalidad, bloque de registros, relaciones, procedimientos


almacenados, tablas y columnas, etc., es algo que en MongoBD no existe, y no
es porque no este madura o por que falte evolucionar, la cuestión es que
MongoDB y el paradigma NoSQL en general, no se planteó para resolver los
problemas que teníamos con las bases de datos SQL tradicionales. En su lugar,
MongoDB propone un esquema de almacenamiento por documentos, donde toda
la información relacionada al documento, esta toda junta, por lo que no hace
falta hacer joins para unir la información, lo que se refleja en una mejora
considerable en el performance.

Ahora bien, estudiando un poco el mercado y comparando las tecnologías que la


gente está interesada en aprender, me encontré con esta gráfica, la cual da una
clara señal del futuro de las bases de datos NoSQL en general.

Página | 398
Fig. 161 - Grafica de intereses 2017

La gráfica anterior, hace una comparación con los diversos tópicos o temas más
relevantes que la gente ha manifestado con mayor interés de aprender, por lo
que no solo se habla de bases de datos, si no que se compara con cosas como
Realidad Virtual, Internet de las cosas (IoT), Machine Learning, DevOps, Cloud
computing, etc. Lo que a mí me llama la atención, es que las bases de datos
NoSQL (entre las que se incluye MongoDB) representa el segundo lugar de
popularidad, solo sobrepasada por la Arquitectura de Software.

Esta gráfica es muy reveladora, pues no dice que la gran mayoría de personas,
están muy interesadas en aprender Bases de datos NoSQL, lo que sin duda
también disparará la demanda de esta base de datos.

Ahora bien, con estos datos, quiero que veas la siguiente gráfica:

Fig. 162 - Posiciones de trabajo abiertas.

399 | Página
La siguiente gráfica ilustra las posiciones abiertas en enero de 2016, en las cuales
podemos apreciar las principales bases de datos del mercado. Primero que nada,
quiero que observes, como mongo a final de la gráfica, se logra posicionar como
la tercera base de datos más solicitada, solo superada por Oracle y SQL Server.

Sé que la diferencia entre Oracle y SQL Server es abismal. Sin embargo, hay que
recordar que estas dos bases de datos son el estatus quo del momento, es decir,
es donde todas las empresas están actualmente y en muchas de ellas, ya están
empezando a mirar partes de sus aplicaciones a MongoDB, por lo que se espera
que, en los próximos años, las bases de datos NoSQL tomen mucho más fuerza
y empiece a desplazar a las SQL.

Ahora bien. MongoDB no está diseñado para todo tipo de aplicaciones, por lo que
sin duda SQL seguirá teniendo su lugar.

Como conclusión a todo este análisis, cerraría diciendo que MongoDB es sin duda
unas de las tecnologías con mayor potencial en los años que siguen y que la
oferta de trabajo para gente con este perfil va a subir drásticamente, por lo que
es buen momento para empezar a aprender.

El rol de MongoDB en una aplicación

A diferencia de las bases de datos tradiciones o SQL, MongoDB es diseñado para


solucionar problemáticas diferentes, en donde no es requerido realizar grandes
transacciones sobre el mismo registro, dado que MongoDB no cuenta con
transacciones, sin embargo, es posible simular bloqueos para impedir que dos
procesos actualicen el registro al mismo tiempo y rompan la consistencia de la
información.

Otra de las características de Mongo, es su capacidad de adaptarse a los cambios,


pues no requiere de una estructura fija, en su lugar, puede recibir cualquier
elemento JSON. Con la llegada de IoT, Mongo ha demostrado una gran potencia,
pues nos permite guardar las configuraciones de los dispositivos e ir guardado
toda la información que va generando.

Solo por poner un ejemplo, me toco conocer de un proyecto para inducir a los
niños a la tecnología, mediante el lanzamiento e globos orbitales, estos globos
son construidos con dispositivos ultra económicos, como tarjetas Arduino y todo
tipo de sensores compatibles. La idea del proyecto es armar globos que suban
hasta la atmosfera y en su camino vallan registrando temperatura, altura,
posición, humedad y tiene una cámara para tomar fotos cada minuto. Lo
interesante es que este globo, tiene un programa en NodeJS el cual es el
encargado de gestionar la comunicación con los sensores, para finalmente
guardar los registros en una base de datos MongoDB.

Mientras el globo vuela, los alumnos pueden seguir la trayectoria por el GPS para
recuperar la capsula (donde está el hardware). El globo se revienta al entrar a la
atmosfera y empieza su caída en picada. A cierta altura se activa un paracaídas
que hace que la capsula descienda suavemente y va fotografiando su caída. Al

Página | 400
final los alumnos pueden saber dónde cayo exactamente por el GPS y las últimas
fotos que tomo antes de tocar tierra. A y se me olvida, los alumnos se encargan
de programar todo lo necesario para el funcionamiento

No sé ustedes, pero como me hubiera gustado vivir esa experiencia cuando


estudiaba (snif , snif).

Pero, ¿por qué les cuento esta historia de un proyecto universitario?, No sé


ustedes, pero yo veo demasiado potencial en eso, con el hecho de que un
dispositivo alcance la atmosfera y que valla monitoreando cada aspecto, me hace
pensar que las posibilidades son infinitas.

Ahora bien, esto es sin hablar de la robótica, wearables (ropa, relojes, etc.),
dispositivos electrónicos que requiere almacenar información, etc. Lo triste, es
que cuando hablamos de aplicaciones y bases de datos, siempre se nos viene a
la mente los sistemas de información, sin embargo, existen muchísimas más
cosas que requieren de una base de datos.

Con este contexto, parece que queda claro que el contexto de MongoDB en una
aplicación, puede ser en cualquier lugar que se requiera almacenamiento y que
este no requiera de gran cantidad de actualizaciones sobre un mismo objeto.

Como funciona MongoDB

Para comprender como funciona MongoDB, es necesario conocer que son las
Colecciones y los documentos, pues son el equivalente que existe entre Tablas y
Columnas.

Que son las Colecciones

Las colecciones, es la forma que tiene Mongo para agrupar los documentos,
por ejemplo, podemos tener una colección para los usuarios y otra para los
Tweet. Estas colecciones no restringen la estructura que un registro puede tener,
si no que ayuda solo a agruparlos. Por ejemplo, veamos como guardamos la
información del proyecto Mini Twitter:

401 | Página
Fig. 163 - Colecciones en MongoDB.

Por muy impresionante que parezca, en toda la aplicación solo utilizamos dos
colecciones, una para los usuarios (profiles) y otra para los tweets (tweets), lo
cuales podemos ver del lazo izquierdo. Estas dos colecciones las utilizamos para
agrupar los usuarios y los tweets por separado, pero en ningún momento,
definimos la estructura que puede tener una colección.

Por ejemplo, yo podría guardar un usuario (profile) en la colección de los tweets


y Mongo me lo permitiría. Esto es posible debido a que Mongo no define una
estructura de columnas como lo hacen las bases de datos SQL, en su lugar,
Mongo permite guardar Documentos.

Que son los Documentos

Un documento es un archivo en formato JSON, el cual guarda la información


en una estructura “clave:valor”, por ejemplo:

1. {
2. “_id”: “59f9f12317247f48f13367b3”,
3. “_creator”: “59f90ca2de72f70dd9a8d819”,
4. “message”: “Mi primer Tweet”,
5. “image”: null,
6. “replys”: 0,
7. “likeCounter”: 0,
8. “date”: “2017-11-01 10:06:59.036”
9. }

El JSON que vemos a continuación corresponde a un usuario. Cuando creamos


un registro en MongoDB, este agrega el campo _id, el cual corresponde al ID
mediante el cual, puede identificar como único a el registro.

Página | 402
MongoDB también permite la creación de índices, con lo cual, podemos aumentar
el performance en las búsquedas. Los índices son apuntadores, que permite a la
Mongo encontrar de forma más eficiente un documento. Aunque esto es posible
hacerlo desde la terminal o desde un cliente, dejaremos esta responsabilidad a
la librería que utilizaremos para conectarnos a Mongo desde NodeJS.

Operaciones básicas con Compass

En esta sección aprenderemos a realizar las operaciones más básicas que


podemos hacer con Compass, entre las que se encuentras, crear colecciones,
insertar, actualizar, borrar y consultar documentos.

Si bien, Compass no es el cliente más potente en este sentido, si nos permite


realizar todas estas acciones, aunque de una forma muy básica. Al final, esto
será suficiente para empezar.

Creando nuestra primera colección

Lo primero que aprenderemos será a crear colección, para ello, nos dirigiremos
a Compass y nos colocaremos en nuestra base de datos, en nuestro caso sería
“test”, la cual podemos ubicar del lado izquierdo. Una vez allí, nos mostrará el
listado de todas las colecciones existentes hasta el momento, por lo que, si ya
hemos trabajado con el proyecto Mini Twitter, ya deberías de tener creadas las
colecciones profiles y tweets.

Una vez allí, presionamos el botón “CREATE COLLECTION” y nos arrojará una nueva
pantalla:

Fig. 164 - Creando una colección en Compass.

Lo único que nos pedirá, es que le pongamos un nombre a la colección y adicional,


nos pedirá que indiquemos si la colección es limitada (Capped). Las Capped
Collectios sirve para establecer un límite máximo de espacio para la colección,
de tal forma, que, cuando la colección llega a límite, empieza a borrar los
primeros registros para insertar los nuevos.

403 | Página
En nuestro caso no queremos que sea Capped, por lo que solo le podremos el
nombre “animals” para realizar pruebas. Presionamos nuevamente “CREATE
COLLECTION” y listo, habremos creado nuestra primera colección.

Una vez echo este paso, la nueva colección deberá aparecer del lado derecho y
damos click en ella para ver su contenido. Lógicamente, estará vacía, pero más
adelante insertaremos algunos registros.

Insertar un documento

Una vez que ya estamos dentro de la colección, podremos observar el botón que
dice “INSER DOCUMENT” el cual presionaremos. Una nueva pantalla nos
aparecerá y no solicitará los valores para nuestro documento:

Fig. 165 - Insert document.

Como verá, por default nos va a crear un campo llamado “_id”, el cual no
podremos eliminar, pues es el único valor obligatorio. Lo siguiente será empezar
a capturar los valores de nuestro documento.

Cada valor se componente de un nombre y valor, y adicional, es posible definir


un tipo, el cual veremos del lado derecho. Para empezar, crearemos 5 registros
de animales, los cuales tendrán los campos, nombre, color y edad:

Fig. 166 - Perro document.

Página | 404
Y repetiremos lo mismo para otros 4 animales, puedes poner lo que sea para que
practiques. Una vez que termines de capturar los datos, solo presionar el botón
“INSERT” para guardar el documento.

Yo he creado los siguiente 5 animales:

Fig. 167 - 5 animales insertados.

El botón “FIND” actualiza la pantalla para ver todos los registros guardados.
Como vez, el número y nombre de los campos no está limitado, incluso
podríamos crear un nuevo animal que tenga un valor que el resto no, por
ejemplo, voy a crear otro animal que se llame “zapo” y le voy a poner un campo
nuevo llamado “patas:4”:

Fig. 168 - Animal Zapo con el campo patas.

Así de fácil es crear un documento, claro que estos documentos son simples, pero
cualquier valor podrá contener otro objeto dentro.

405 | Página
Actualizar un documento

Actualizar un documento es todavía más fácil que crearlo, pues solo hace falta
ponernos sobre el registro que queremos actualizar y presionar el pequeño lápiz
que sale del lado derecho.

Fig. 169 - Actualizando un documento.

Al presionarlo, el registro se pondrá en modo edición y podremos hacer los


cambios necesarios, como agregar nuevos campos, eliminar o actualizar
existentes. Finalmente, solo hace falta guardar los cambios en el botón
“UPDATE”.

Eliminar un documento

Eliminar un documento es exactamente igual que editarlo, con la diferencia de


que, en lugar de presionar el botón del lápiz, deberemos presionar el botón del
bote de basura:

Fig. 170 - Eliminando un documento.

Finalmente hay que confirmar la eliminación presionando el botón “DELETE”.

Página | 406
Consultar un documento

La consulta es todavía más compleja que el resto de operaciones, pues una


consultar tiene verías secciones que analizar. Las consultas las hacemos
posicionándonos en una colección, una vez allí, podremos ver un panel de
búsqueda, el cual podemos expandir presionando el botón “OPTIONS” para ver
más opciones de búsqueda:

Fig. 171 - Opciones de búsqueda en MongoDB.

Una búsqueda en MongoDB se divide en las siguientes secciones:

407 | Página
 Filter: corresponde a la sección WHERE de SQL, en ella ponemos en
formato {clave: valor} los elementos a filtrar.
 Projection: En esta sección ponemos los campos que esperamos que
nos regrese la búsqueda, es similar a la sección (SELECT columna1,
columna2). La proyección se escribe en formato {clave: valor}.
 Sort: Esta sección se utiliza para ordenar los elementos de la respuesta
y correspondería a la instrucción ORDER BY de SQL. Esta sección se
escribe en formato {clave: valor}, donde la clave es el nombre del
campo a ordenar y el valor solo puede ser 1 o -1 para búsquedas
ascendentes y descendentes.
 Skip: Permite indicar a partir de que registro se regresen los resultados,
es utilizado con frecuencia para la paginación.
 Limit: Se utiliza para establecer el número máximo de registros que
debe de regresar la consulta. Se utiliza en conjunto con Skip para lograr
la paginación. Es similar a la instrucción TOP o LIMIT de SQL.

Aprender a realizar consultas


Sin duda, realizar consultas con MongoDB puede resultas un dolor de cabeza al
inicio, pues de estar a acostumbrados a realizar consultas muy estructuradas con
SQL, deberemos pasar a una sintaxis de JSON, pero una vez que lo
comprendamos, verás que es muy simple.

Filter

Lo primero que aprenderemos será utilizar la sección filter (filtro), en la cual


debemos definir las condiciones con las que un documento será selecciona o no,
muy parecido a la sentencia WHERE de SQL.

La forma más simple de filtrar un elemento, es cuando buscamos un campo que


sea igual a un valor, por ejemplo, queremos buscar todos los animales que tenga
color blanco o lo que es igual {color: “Blanco”}:

Página | 408
Fig. 172 - Busca de animales de color blanco.

Como puedes apreciar, solo hace falta indicar el campo y valor en formato {clave:
valor}.

Operadores lógicos

Los operadores lógicos son todas aquellas expresiones entre dos o más
operadores donde su evaluación da como resultado un booleano, y que por lo
tanto siguen la teoría del algebra de Boole (Tabla de la verdad). Los operadores
disponibles son los siguientes:

Operados Descripción
AND Retorna true si todas las condiciones son verdaderas

1. {$and: [{expresión 1}, {expresión 2}, {expresión N} ] }

OR Retorna true si al menos una de las condiciones es verdadera

1. {$or: [{expresión 1}, {expresión 2}, {expresión N} ] }

NOT Niega cualquier expresión, por lo que un true lo convierte en false


y viceversa.

1. {$nor: [{expresión 1}, {expresión 2}, {expresión N} ] }

NOR Retorna true solo cuando todas las condiciones fallan.

1. { field: { $not: { <operator-expression> } } }

409 | Página
Operador AND

si queremos que adicional al color, la edad sea igual a 5. Solo deberemos agregar
ese nuevo campo en el filtro:

Fig. 173 - Búsqueda de animales blancos y con 5 años de edad.

Solo hace falta separar los valores con una coma, entre cada campo a filtrar. En
este caso, la condición se está evaluando como un AND, por lo que los dos
criterios se deberán cumplir para que el resultado sea retornado.

Esta misma búsqueda se podría lograr mediante la siguiente instrucción:

{ $and: [{nombre: “Perro”} , {edad: 5} ]}

En este formato, el clave deberá ser el operador $and y como valor, se pasa un
array, en donde cada posición corresponde a una condición. El array puede tener
2 o más condiciones.

La sintaxis de este operador es:

{$and: [{expresión 1}, {expresión 2}, {expresión N} ] }

Operador OR

Si lo que buscamos es realizar una búsqueda utilizando el operador OR,


deberemos cambiar un poco la sintaxis, por ejemplo, imagina que queremos
consultar todos los animales que tenga como nombre el valor “Perro” o “Gato”:

Página | 410
Fig. 174 - Búsqueda mediante el operador OR.

Como puedes observar, es necesario iniciar con el operador $or, y como valor,
deberemos enviarle un array, en donde cada posición del array, será una
condición a evaluar mediante el operador OR, en este arreglo puede haber de 2
a N condiciones.

La sintaxis de este operador es:


{$or: [{expresión 1}, {expresión 2}, {expresión N} ] }

Operador NOR (NO OR)

Este operador funciona prácticamente igual que OR, no la diferencia de que este
niega la expresión. Utilizamos el operador NOR para indicar expresiones como
“Selecciona todos los animales donde el nombre NO sea Perro o la edad NO sea
1”:

411 | Página
Fig. 175 - Operador NOR

De los 6 registros que tenemos, solo nos arroja 3, pues dos de ellos tiene edad
= 1 y uno tiene como nombre = Perro. En este operador con una sola condición
que se cumpla será suficiente para que el registro no se muestre.

Observemos que el tercer documento (Zapo) se está mostrando a pesar de tener


la edad=1, sin embargo, notemos que el valor es un String y no un numérico, lo
que lo hace diferente para la comparación.

La sintaxis de este operador es:

{$nor: [{expresión 1}, {expresión 2}, {expresión N} ] }

Operador NOT

El operador NOT se utiliza para negar una expresión, como parámetro recibe un
boolean o una expresión que lo retorne, para finalmente negar el valor.

Página | 412
Fig. 176: operador NOT

Para usar el operador NOT, es necesario indicarle un operador de comparación,


en este caso, usamos el comparador $eq (equals) para decirle no queremos
animales de color igual a blanco.

MongoDB ofrece varios operadores de comparación, pero los analizaremos más


adelante.

La sintaxis de este operador es:

{ field: { $not: { <operator-expression> } } }

Operadores de comparación

Los siguientes operadores ayudan a realizar comparaciones y pueden ser


utilizados en conjunto con los operadores lógicos

Operador Descripción
$eq Valida si un campo es igual a un valor determinado, su
nombre proviene de “equals”.

1. { <field>: { $eq: <value> } }

$ne Valida que un campo no sea igual a un valor determinado.


Su nombre proviene de “not equals”

413 | Página
1. { <field>: { $ne: <value> } }

$gt Valida si un campo es más grande que un valor determinado.


Su nombre proviene de “greater than”.

1. { <field>: {$gt: value} }

$gte Valida si un campo es más grande o igual a un determinado


valor. Su nombre proviene de “greater than or equals”.

1. { <field> : {$gte: value} }

$lt Valida que un campo sea menor que un valor determinado.


Su nombre proviene de “less than”.

1. {field: {$lt: value} }

$lte Valida que un campo sea menor o igual a un valor


determinado. Su nombre proviene de “less than or equals

1. {field: {$lte: value} }

$in Valida que el valor de un campo se encuentra en una lista de


valores, retorna true si existe al menos una coincidencia. Su
nombre proviene de “dentro de”

1. { field: { $in: [<value1>, <value2>, ... <valueN> ] } }

$nin Valida que el valor de un campo no se encuentre en ningún


valor de una lista. Regresa verdadero solo si el campo
evaluado no se encuentra en la lista de valores. Su nombre
proviene de “no en (una lista)”

1. { field: { $nin: [ <value1>, <value2> ... <valueN> ]} }

Hablar de cada operador de comparación puede resultar algo abrumador, pues


nos extenderíamos demasiado explicándolos, por lo que analizaremos solo un par
de estos operadores, ya que al final, todos se utilizan de la misma forma:

Regresando a la colección de animales, vamos a resolver algunas problemáticas


usando los nuevos operadores de comparación que hemos aprendido, el primero
a analizar es:

Caso 1:

Busquemos todos los animales que tenga más de 1 años y que sean de color sea
diferente de café:

Página | 414
Fig. 177 - Operadores de comparación $gt y $ne.

Vemos que, en esta consulta, hemos utilizado los operadores de comparación $gt
(mayor que un año) y $ne (Diferente de Café), adicional, nos hemos apoyado de
operador lógico $and para unir las dos condiciones.

Caso 2:

Encontremos todos los animales que sean “Perro, Conejo y Ratón” o animales
que tenga 4 patas:

Fig. 178 - Operadores de comparación $in y $eq

Veamos que utilizamos el operador $in (animales llamados Perro, Conejo o


Ratón) y el operador $eq (Animales con 4 patas), adicionalmente, nos hemos
apoyado del operador lógico $or para unir las dos condiciones.

415 | Página
Algo interesante en este query, es que solo el Zapo tiene la propiedad patas, lo
que comprueba la versatilidad que tiene MongoDB para crear estructuras
dinámicas.

En estos dos simples, pero prácticos ejemplos, hemos aprendido a utilizar los
operadores de comparación. Yo te invito que adicional a los ejemplos que hemos
planteado, te pongas un rato a jugar con el resto de operadores para que
compruebes por ti mismo como funcionan y aprender incluso a combinarlos con
los operadores lógicos.

Operadores sobre elementos

Los siguientes operadores son utilizados sobre los elementos de un documento,


los cuales tiene que ver con la existencia o no de ciertos elementos o incluso el
tipo de datos de los elementos.

Operador Descripción
$exists Valida si un documento tiene o no un campo determinado, si el
valor del operador es true, entonces buscará todos los documentos
que si cuenten con el campo, por otra parte, si el valor se establece
en false, entonces buscará todos los documentos que no cuente
con el campo.

1. { <field>: { $exists: <boolean> } }

$type Valida el tipo de datos de un determinado campo. Regresa true si


el tipo del campo coincide con el valor enviado.

1. { <field>: { $type: <BSON type number> | <String alias> } }

$all Este operador valida si un array del documento contiene todos los
valores solicitados.

1. { <field>: { $all: [ <value1> , <value2> ... ] } }

Caso 1:

Veamos el siguiente ejemplo. Seleccione todos los animales que tengan el


atributo “patas”:

Página | 416
Fig. 179 - Utilizando el operador de elementos $exists

Zapo es el único animal retornado, pues es el único que cuenta con la propiedad
“patas”. Observa que al operador le hemos puesto el valor true. Esto indica que
buscamos los que SI tengan el atributo, pero también le pudimos haber puesto
false, lo que cambiaría el resultado, pues buscaría solo los documentos que no
tuvieran el atributo. El operador $exists sol valida que el campo exista, sin
importar su valor

Caso 2:

Para probar el operador $type va a ser necesario crear un nuevo registro, el cual
será el siguiente:

Fig. 180 - Nuevo documento para el animal Vaca.

Quiero que prestes atención en el campo edad, pues a diferencia del resto, ha
este le he puesto que la edad es de tipo String, mientras que al resto les puse
Int32:

Fig. 181 - Operador $type.

417 | Página
Caso 3:

Otro ejemplo sería buscar sobre los elementos de un array con ayuda del el
operador $all, para realizar una prueba con este operador deberemos crear dos
nuevos registros, los cuales tenga una lista de “apodos”. Yo he creado los
siguientes dos registros:

Fig. 182 - Dos nuevos registros con el array apodos.

Observa que los dos nuevos registros tienen el array “apodos”, sin embargo, no
tiene los mismos valores, el tercer valor es diferente entre los dos documentos.
Ahora bien, si yo quisiera recuperar los animales que tangan como apodos los
valores “Fido, Cachorro y Huesos” tendría que hacer lo siguiente:

Fig. 183 - Operador $all.

Como resultado, solo nos trae un registro de los dos, pues solo uno tiene los tres
valores indicamos en el operador $all.

Vamos a dejar hasta aquí las operaciones que nos da MongoDB, pues son las
más importantes que necesitaremos para el desarrollo de nuestra API. Si quieres
conocer el listado completo de operadores que soporta MongDB, te invito a que
te des una vuelta por la documentación oficial:

Página | 418
Project

La sección Project se utiliza para determinar los campos que debe de regresar
nuestra consulta, algo muy parecido cuando hacemos un “SELECT campo1,
campo2” a la base de datos. Esta sección es especialmente útil debido a que ayuda
a reducir en gran medida la información que retorna la base de datos, ahorrando
una gran cantidad de transferencia de datos y por lo tanto un aumento en el
performance.

Seleccionando o filtrando campos

Debido a que el viaje por la red es uno de los principales factores de degradación
de performance, es especialmente importante cuidar este aspecto. En MongoDB,
es muy fácil determinar los campos que queremos y no queremos en la
respuesta, pues tan solo falta listas los campos en formato {clave: val, calve:
val, …. } donde la clave es el nombre del campo en el documento y el val solo
puede tener 1 o 0, donde 1 indica que si lo queremos y 0 que no lo queremos.

Veamos un ejemplo para hacer esto más claro, imaginemos que queremos
recuperar el nombre y la edad de todos los animales:

Fig. 184 - Filtrando campos con MongoDB.

En la imagen podemos claramente que de todos los campos que tiene el


documento, solo nos regresó el nombre y la edad, aunque también vemos el _id,
esto se debe a que, por default, Mongo siempre lo regresa.

Ahora bien, si queremos que no nos regrese el _id, hay que decirle explícitamente
de la siguiente manera:

419 | Página
Fig. 185 - Eliminando el _id del resultado.

Observemos que hemos puesto el campo _id con valor a cero (0). El cero
MongoDB lo interpreta como que no lo queremos.

Ahora bien, así como le hemos dicho que campos queremos, también le podemos
indicar simplemente cuales no queremos y el resto si los retornará. Esta sintaxis
es muy cómoda cuando queremos todos los campos con excepción de unos
cuantos, lo que nos ahorra tener que escribir todos los campos que tiene. Veamos
un ejemplo de esto, imaginemos que queremos todos los campos, excepto, el
nombre:

Fig. 186 - Excluyendo solo el nombre del documento.

Trabajando con objetos

Página | 420
El último tipo de selección que veremos será sobre un objeto, para esto
tendremos que hacer algunos cambios. He editado el último registro de la
colección para agregarle una nueva propiedad llamada propiedades la cual es de
tipo Object, y dentro de ella he puesto 3 nuevos campos, alto, largo y peso,
todos estos de tipo Int32.

Fig. 187 - Agregando la propiedad propiedades.

Dado que “propiedades” es un objeto, podríamos determinar que no requerimos


(o requerimos) un valor de este objeto anidado. Imaginemos que requerimos
todos los campos del documento con excepción del campo alto del objeto
propiedades.

Fig. 188 - Obteniendo todo el documento excepto la propiedad alto.

Veamos que en este caso la sintaxis ha cambiado ligeramente, para empezar,


tenemos que poner todo el path hasta llegar a la propiedad mediante puntos “.”,
y este debe de estar entre comillas. En este caso pusimos 0 por que no lo
queríamos, pero igualmente podríamos poner un 1 para que solo nos traiga ese
campo.

Si quieres seguir profundizando en la sección Project, puedes revisar la


documentación oficial de MongoDB

421 | Página
Sort

La sección sort es utilizada para ordenar los resultados, es sin duda de las
secciones más simples, pues solo hace falta indicar los campos a ordenar y el
sentido de la ordenación (ascendentes = 1 o descendente = -1).

Esta sección se define en el formato {key: {1|-1} }, donde key es el nombre del
campo a ordenar.

Veamos unos ejemplos, imaginemos que queremos ordenar los animales por
nombre de forma ascendente:

Fig. 189 - Ordenamiento Ascendente.

Ahora bien, imaginemos que queremos ordenar por nombre ascendente y color
descendente:

Página | 422
Fig. 190 - Ordenamiento ascendente y descendente.

Podemos ver qué cambio el cuarto registro, pues el orden decreciente del color
provoco un reordenamiento.

Paginación con Skip y Limit

Tanto el campo Skip y Limit reciben un valor numérico, es decir que no requieren
un objeto JSON {key:value}. Limit permite determinar cuántos registros
máximos debe de regresar la consulta y Skip indica a partir de que elemento de
empieza a regresar los valores.

Veamos la primera prueba, voy a solicitar que me regrese los 2 primeros


elementos de la lista:

423 | Página
Fig. 191 - Limitando el número de registros con limit.

Ahora bien, si a esto le sumamos la propiedad skipe para que salte el primero
resultado, esto recorrerá la búsqueda en un registro, de tal forma que el segundo
registro se convertirá en el primero y el segundo que veamos, corresponderá al
3 resultado de la búsqueda:

Fig. 192 - Probando la instrucción Skip.

En este momento tenemos 9 registros, por lo que podríamos paginar por bloques
de 3, realizando la siguiente combinación:

 Skip = 0 y Limit = 3
 Skip = 3 y Limit = 3
 Skip = 6 y Limit = 3
 Skip = 9 y Limit = 3 (ya no tendría más resultados)
La técnica es muy simple, primero que nada, ponemos en limit el tamaño de los
bloques que queremos consultar y luego en Skipe debemos ejecutar en múltiplos
del valor colocado en limit, pero siempre empezando en 0.

Página | 424
Fig. 193 - Ejemplo de paginación en bloques de 3.

NodeJS y MongoDB

Tras una larga platica de cómo funciona MongoDB por sí solo, ha llegado el
momento de aprender cómo debemos usarlo en conjunto con NodeJS.

En esta sección estudiaremos la librería Mongoose, la cual es una de las más


populares para trabajar con MongoDB. De la misma forma, aprenderemos a
establecer conexión con la base de datos y aprenderemos que son los modelos.
Adicional a eso, analizaremos algunos ejemplos básicos de inserción,
actualización, eliminación y búsquedas por medio de NodeJS.

Estableciendo conexión desde NodeJS

Antes de poder hacer cualquier cosa, es necesario instalar la librería Mongoose y


establecer conexión con la base de datos, por tal motivo iniciaremos con la
instalación.

npm install --save mongoose

El siguiente paso será crear el archivo AnimalMongoDBTest.js en la raíz del


proyecto (/), en el cual realizaremos nuestras primeras pruebas para aprender a
utilizar Mongoose. El archivo tendrá el siguiente contenido:

1. var mongoose = require('mongoose')


2. var configuration = require('./config')
3. const connectString = configuration.mongodb.development.connectionString
4.

425 | Página
5. var opts = {
6. useNewUrlParser: true,
7. appname: "AnimalMongoDBTest",
8. poolSize: 10,
9. autoIndex: false,
10. bufferMaxEntries: 0,
11. reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect
12. reconnectInterval: 500,
13. autoReconnect: true,
14. loggerLevel: "error", //error / warn / info / debug
15. keepAlive: 120,
16. validateOptions: true
17. }
18.
19. mongoose.connect(connectString, opts, function(err){
20. if (err) throw err;
21. console.log("==> Conexión establecida con MongoDB");
22. })

Analicemos que está pasando. En la línea 1 estamos importando la librería de


mongoose, desde la cual estableceremos la conexión

Seguido, estamos importando el archivo de configuración (línea 2), ya que, si


recordarás, allí colocamos el String de conexión a MongoDB. Si no lo recuerdas,
puedes regresar a la sección “Instalar API REST” en donde analizamos como
obtener el String de conexión.

La línea 3 recuperamos el string de conexión de la configuración y la pasamos a


una constante.

En la línea 5 creamos un objeto de configuración para establecer la conexión a la


base de datos, este objeto tiene puede tener muchísimas opciones de
configuración, por lo que nombraremos las más importantes:

Propiedad Descripción
useMongoClient Es una propiedad propia de Mongoose, la cual habilita una
nueva estrategia de conexión a partir de la versión 4.11 de
Mongoose. Sin dar muchas vueltas, solo hay que ponerla
en true.

appname Propiedad especial para poner el nombre de la aplicación


que se conecta a MongoDB. Es especialmente útil para
buscar un error en los logs de MongoDB.

poolSize Valor número que determina el número de conexiones que


deberá tener abiertas a MongoDB.

autoIndex Mongoose soporta un buffer en memoria para guardar los


documentos si la conexión a MongoDB falla. De tal forma
que al restablecer la conexión podrá guardar los cambios.

bufferMaxEntries Esta propiedad está ligada a autoIndex y solo tiene sentido


definirla si esta otra es true. Esta propiedad recibe un valor
numérico, el cual corresponde al número de operaciones
que amortiguara antes de renunciar y esperar una
conexión.

Página | 426
autoReconnect Propiedad booleana, que le indica a Mongoose si debe de
reconectarse de forma automática en caso de una
desconexión.

reconnectTries Ligada a autoReconnect y corresponde al número de


reintentos que realizará antes de darse por vencido y
marcar error de desconexión.

reconnectInterval Ligada a autoReconnect, determina el tiempo que esperará


entre cada intento de reconexión(reconnectTries)

loggerLevel Determina el nivel de log que debe lanzar Mongoose, los


valores permitidos son (error|warn |info|debug). Se
recomienda tenerlo siempre en error, a menos que se esté
depurando un error, pues la cantidad de log que puede
generar es abrumadora.

keepAlive Habilita una estrategia para garantizar que las conexiones


estén activas en todo momento.

validateOptions Esta es una propiedad especial de Mongoose, la cual ayuda


a validar que todas las propiedades estén correctamente
establecidas, se recomienda tenerla siempre en false,
excepto la primera vez que modifican las propiedades.

En la siguiente liga podrás encontrar el listado completo de propiedades


disponible

Finalmente, en la línea 19, se establece la conexión con el método connect del


objeto mongoose. Como parámetro recibe el String de conexión, las opciones de
configuración y opcionalmente una función que se ejecutará al momento de la
conexión, esta función recibe como parámetro el error (si hubo). Desde esta
función se puede realizar una acción si no se pudo conectar o si se conectó
correctamente.

Si tenemos correctamente configurado el String de conexión y ejecutamos la


aplicación podremos ver que la aplicación se conecta correctamente. Para ello
ejecutamos el comando node AnimalMongoDBTest.js:

Fig. 194 - Conexión exitosa a MongoDB desde Mongoose.

427 | Página
Que son los Schemas de Mongoose

Los Schemas son objetos JavaScript que, en primera instancia, definen la


estructura de un Documento y lo relacionan con una colección de MongoDB.
Mediante los Schemas es posible definir los campos y el tipo de datos esperado
para cada uno de ellos. Además, permite definir valor por default e indicar si un
campo será un Index dentro de la colección.

Una schema es muy sencillo de definir, tan solo es necesario crearlo mediante el
objeto schema proporcionado por mongoose. Veamos cómo quedaría un schema
para la colección animals un ejemplo:

1. var animal = mongoose.Schema({


2. name: {type: String, index: true},
3. color: {type: String, default: "Blanco"},
4. edad: {type: Number, default: 0},
5. apodos: [{type: String, default: []}],
6. propiedades: {
7. alto: Number,
8. largo: Number,
9. peso: Number
10. }
11. },{ strict: false })
12. var Animal = mongoose.model('animals', animal);

De la línea 1 a 11 estamos creando la definición del schema, para ello, es


necesario pasar a mongoose.Schema un objeto que defina la estructura esperada
para un documento. Dentro de este objeto, cada elemento que pongamos, será
un campo en el documento final, por lo que deberemos definir apropiadamente
el nombre y el tipo de cada campo.

Una vez que hemos creado la estructura del schema, solo falta registrarlo en
Mongoose. Esto lo hacemos mediante el método mongoose.model, al cual se le
pasen dos parámetros, el primero corresponde al nombre del modelo, que por
default también corresponde con el nombre de la colección, el segundo
parámetro es la definición del schema. Con solo eso, hemos definido un schema.

Ahora bien, la forma de definir un campo es mediante el formato {campo: type


}, donde el campo, es el nombre que tendrá en el documento, el type es el tipo
de dato. Si un campo no requiere más que definir el tipo de datos, podemos
hacerlo de la siguiente manera:

1. var animal = mongoose.Schema({


2. name: String,
3. edad: Number
4. })

En esta nueva forma, simplemente podemos poner el tipo de datos directamente,


sin crear un nuevo objeto para definirlo. Sin embargo, es muy común que
requiramos poder alguna otra propiedad al campo, como un valor por default o
indicar si se trata de un index. En estos casos, debemos crear un nuevo objeto y

Página | 428
definir todas las propiedades requeridas separadas por coma (,) como en el
primero ejemplo.

Esquemas estrictos y no estrictos

En más de una ocasión, te he mencionado que una de las ventajas de MongoDB


es que permite guardar cualquier documento, sin importar la estructura que este
tenga. Entonces la pregunta que te puedes estar haciendo en este momento es
¿Si MongoDB soporta cualquier estructura, porque debo de crear un Schema?
Esta pregunta es muy importante y la respuesta es simple.

Mongoose utiliza los Schemas para ayudar al programador a tener un mejor


control sobre la creación de los documentos, aunque no es obligados fijar una
estructura. Dado que la mayoría de las aplicaciones manejan una estructura fija,
los Schemas pueden ser una gran ventaja, pues te ayudarán a asegurar que los
documentos tengan la estructura adecuada.

En Mongoose se pueden crear esquemas de dos formas, estrictos y no estrictos,


los estrictos ignorarán cualquier campo que no esté en el schema, y los no
estrictos permitirá agregar nuevos campos si son requeridos.

Dentro de los NO estrictos, podemos crear un schema sin ninguna estructura


para agregar dinámicamente cualquier campo requerido, pero también es posible
definir solo los campos que siempre deben de venir y dejar abierto el esquema
para agregar nuevos campos dinámicamente.

1. var animal = new mongoose.Schema({}, { strict: false });


2. var animal = mongoose.model('animals', animal);

En este nuevo ejemplo, podemos ver que creamos una estructura en blanco y
pasar como segundo argumento la propiedad strict en falso. Con tan solo poner
strinc = false estamos diciendo que el schema puede soportar campos no
definidos en el schema.

Schemas avanzados

Los eschemas no solo sirven para definir la estructura de documento, sino que,
además, permiten crear funciones para validaciones, consultas, propiedades
virtuales, etc, etc. Hablar de toda la capacidad que tiene Mongoose se podría
extender demasiado, por lo que te contare rápidamente algunas de las cosas más
interesantes para que te des una idea de su capacidad.

Statics methods

429 | Página
Las funciones estáticas permiten definir funciones a nivel del schema, que pueden
ser invocadas sin necesidad de tener una instancia del Schema

1. animalSchema.statics.findByName = function(name, cb) {


2. return this.find({ name: new RegExp(name, 'i') }, cb);
3. };
4.
5. var Animal = mongoose.model('Animal', animalSchema);
6. Animal.findByName('fido', function(err, animals) {
7. console.log(animals);
8. });

Para definir una función estática, solo tendremos que crearla sobre el Schema,
el formato es el siguiente: <schema>.statics.<method-name>.

Para ejecutar el método estático, solo tendremos que recuperar el Schema (línea
5) y después ejecutar directamente la función.

Este tipo de métodos se utilizan para realizar búsquedas o realizar algunas


validaciones que no requieren de un documento en concreto.

Instance methods

Los métodos de instancia funcionan exactamente igual que los métodos estáticos,
pero estos se definen por medio de la propiedad methods.

1. var animalSchema = new Schema({ name: String, type: String });


2. animalSchema.methods.findSimilarTypes = function(cb) {
3. return this.model('Animal').find({ type: this.type }, cb);
4. };
5.
6. var Animal = mongoose.model('Animal', animalSchema);
7. var dog = new Animal({ type: 'dog' });
8. dog.findSimilarTypes(function(err, dogs) {
9. console.log(dogs); // woof
10. });

El formato para definir un método es el siguiente: <Instacia>.methods.<method-


name>. Este tipo de métodos se utiliza para realizar operaciones que requieren de
los valores concretos de un documento, como, por ejemplo, validar si no existe
un documento del mismo tipo, o si el nombre de usuario ya está registrado, etc.

Query Helpers

Los query helpers son parecidos a los métodos estáticos, con la diferencia que
están diseñador para implementar funcionalidad custom, como buscar por
nombre, obtener todos los documentos de un tipo, etc.

1. animalSchema.query.byName = function(name) {
2. return this.find({ name: new RegExp(name, 'i') });
3. };
4.

Página | 430
5. var Animal = mongoose.model('Animal', animalSchema);
6. Animal.find().byName('fido').exec(function(err, animals) {
7. console.log(animals);
8. });

Estos se definen mediante de la siguiente manera: <schema>.query.<method-


name>.

Virtuals

Los virtuals permiten agregar propiedades virtuales, las cuales son calculadas
por medio de otras propiedades del mismo modelo, como concatenar el nombre
y el apellido para obtener el nombre completo.

1. personSchema.virtual('fullName').get(function () {
2. return this.name.first + ' ' + this.name.last;
3. });
4.
5. console.log(person.fullName);

Las propiedades virtuales no son persistentes, si no que más bien se calculan al


momento de ser solicitadas.

En la siguiente liga podrás encontrar toda la documentación acerca de la creación


de Schemas: http://mongoosejs.com/docs/guide.html

Plugins

Una de las capacidades más interesantes de los schemas, es que es posible


instalarles plugins, los cuales agregan funcionalidad que originalmente no tienen,
como, por ejemplo, cambiar el ID por una secuencia, agregar validadores de
campos únicos, etc.

Un plugin puede ser aplicado de forma global a todos lo schemas o a un solo


schema, la diferencia radica sobre que objeto lo aplicas:

1. //Global plugin
2. mongoose.plugin(<plugin>, <params>)
3.
4. //Schema plugin
5. Animal.plugin(<plugin>, <params>)

Instalar un plugin se realiza mediante el método plugin el cual recibe uno o dos
parámetros, el primero corresponde al plugin como tal y el segundo es un objeto
con configuraciones para el plugin.

Dado que cada plugin tiene una forma distinta de trabajar, no es posible hablar
acerca de su funcionamiento sin tener que hacer referencia un plugin en concreto.

431 | Página
Por lo voy a dejar una liga a todos los plugin que tenemos disponibles y más
adelante veremos cómo implementar un plugin.

Listado completo de los plugins disponibles: http://plugins.mongoosejs.com/

Schemas del proyecto Mini Twitter

Una vez que hemos aprendido como a crear schemas con Mongoose, vamos a
practicar creando los modelos que utilizaremos en el proyecto Mini Twitter, sin
embargo, esta parte ya no será parte de la aplicación, si no del API.

Tweet Scheme

El schema Tweet lo utilizaremos para guardar los Tweets de todos los usuarios.
En la aplicación Mini Twitter, tratamos las respuesta de los Tweet, como un nuevo
Tweet, con la única diferencia que tiene una referencia al Tweet padre, esta
referencia se lleva a cabo mediante el campo tweetParent. Este schema deberá
ser creado en el archivo Tweet.js en el path /api/models.

1. var mongoose = require('mongoose')


2. var Schema = mongoose.Schema
3.
4. var tweet = Schema({
5. _creator: {type: Schema.Types.ObjectId, ref: 'Profile'},
6. tweetParent: {type: Schema.Types.ObjectId, ref: 'Tweet'},
7. date: {type: Date, default: Date.now},
8. message: String,
9. likeRef: [{type: Schema.Types.ObjectId, ref: 'Profile', default: []}],
10. image: {type: String},
11. replys: {type: Number, default: 0}
12. })
13. //Virtuals fields
14. tweet.virtual('likeCounter').get(function(){
15. return this.likeRef.length
16. })
17. var Tweet = mongoose.model('Tweet', tweet);
18. module.exports= Tweet

Los campos que podemos apreciar son los siguientes:

 _creator: representa el ID del usuario que creo el Tweet, esta propiedad se


guardar como un ObjectId, y el atributo ref le indica a Mongoose, que este
ID debe de corresponder con el schema Profile.
 tweetParent: Cuando el Tweet es una respuesta a otro Tweet, este campo
deberá contener el ObjectId al Tweet padre, el atributo ref le indica a
Mongoose que este campo deberá corresponder con el schema Tweet.
 date: fecha de creación del Tweet. El atributo default le indica a Mongoose,
que en caso de estar null al momento de insertar el documento, este deberá
poner la fecha y hora actual.
 message: corresponde al texto capturado por el usuario.

Página | 432
 likeRef: Es un arreglo con el ID de los usuarios que dieron like al Tweet.
 Image: Imagen asociada al Tweet, siempre y cuando tenga una imagen.
 Replys: Contador de respuestas que tiene el Tweet. Es decir, número de
Tweet hijos.

Para poder tener un contador de los likes que tiene el tweet, creamos un campo
virtual (línea 14), el cual retorna el número de posiciones que tiene el campo
likeRef.

Este schema es ligado a la colección Tweet, como podemos ver en la línea 17.
Finalmente exportamos el schema para poder ser utilizado más adelante en el
API.

Profile Scheme

El schema Profile, representa a un usuario dentro del proyecto Mini Twitter, en


él se guardan los datos básicos de su perfil, y una referencia a las personas que
sigue y lo siguen.

1. var mongoose = require('mongoose');


2. var uniqueValidator = require('mongoose-unique-validator');
3. var Schema = mongoose.Schema;
4. var Tweet = require('./Tweet')
5.
6. var profile = new Schema({
7. name: {type: String},
8. userName: {type: String, unique: true, index: true,
9. uniqueCaseInsensitive: true},
10. password: String,
11. description: {type: String, default: 'Nuevo en Twitter'},
12. avatar: {type: String, default: null},
13. banner: {type: String, default: null},
14. tweetCount: {type: Number, default: 0},
15. followingRef: [{type: Schema.Types.ObjectId, ref: 'Profile', default: [] }],
16. followersRef: [{type: Schema.Types.ObjectId, ref: 'Profile', default: [] }],
17. date: {type: Date, default: Date.now},
18. });
19. //Unique plugin validate
20. profile.plugin(uniqueValidator, { message: 'El {PATH} to be unique.' });
21.
22. //Helpers
23. profile.query.byUsername = function(userName){
24. return this.find({userName: userName})
25. }
26. //Virtuals fields
27. profile.virtual('following').get(function(){
28. return this.followingRef.length
29. })
30. profile.virtual('followers').get(function(){
31. return this.followersRef.length
32. })
33.
34. var Profile = mongoose.model('Profile', profile);
35. module.exports= Profile

433 | Página
Los campos que podemos apreciar son:

 name: Nombre completo del usuario.


 userName: Representa el nombre de usuario, este campo lo marcamos
como índice mediante el atributo index, además, este campo debe de ser
único. Para valida que el campo sea único, estamos utilizando el plugin
mongoose-unique-validator, el cual nos permite agregar los atributos
unique y uniqueCaseInsensitive, lo que hace que el campo sea validado
sin importar mayúsculas y minúsculas.
 Password: Password del usuario. Por seguridad, encriptaremos el
password antes de guardarlo, previniendo que cualquier persona con
acceso a la base de datos pueda conocer el password real del usuario.
 Description: Breve descripción acerca de la persona. Definimos el valor
“Nuevo en Twitter” por default.
 Avatar: Imagen del avatar.
 Banner: Imagen del banner
 tweetCount: Número de Tweets que ha publicado el usuario.
 followingRef: Arreglo con el ID de todos los usuarios que sigue.
 followerRef: Arreglo con el ID de todos los usuarios que lo siguen.
 Date: fecha de registro del usuario. Por default es la fecha y hora actual
del servidor.

En la línea 20 registramos el plugin mongoose-unique-validator, el cual nos


ayudará a validar que no guardemos un userName repetido y nos arroje un error
personalizado.

Debido a que una de las búsquedas más frecuentes, es la búsqueda por nombre
de usuario (userName), hemos creado un método helper (línea 23), el cual nos
retornará un usuario que corresponda con el nombre de usuario solicitado.

Adicional, hemos creados las propiedades virtuales followers (línea 30) y


Followings (línea 27), lo cual retornan el número de personas que sigue y los
que lo siguen.

Finalmente creamos el Schema ligado a la colección profiles (línea 34)

Para que este schema funcione, debemos de instalar el plugin de validación:

npm install –save mongoose-unique-validator

Ejecutar operaciones básicas

Página | 434
Los schemas no solo sirven para definir la estructura de los documentos, sino
que, además, proporcionan una serie de operaciones para insertar, actualizar,
borrar y consultar los documentos.

Save

Mongoose proporciona dos formas de crear un nuevo documento, una es por


medio del método de instancia save y el método statico create. La diferencia
fundamental entre estas dos técnicas, es que en la primera (save) creamos el
documento a partir de un objeto del Schema previamente creado, mientras que
la segunda forma (create) se crea el documento a partir de un objeto simple.
Veamos un ejemplo:

Save

1. var Tweet = mongoose.model('Tweet', yourSchema);


2.
3. var myTweet = new Tweet({
4. _creator: ObjectId('5a012b3986b5c864a4fe6225')
5. message: 'Mi primer tweet'
6. });
7. myTweet.save(function (err) {
8. if (err) return handleError(err);
9. //Tweet saved
10. })

En el fragmento de código podemos apreciar la creación de un tweet por medio


del método save. Apreciemos que en la línea 3 hemos creado el Tweet por medio
del operador new, con lo cual, creamos un objeto de tipo Tweet. Una vez creado
el objeto Tweet, podemos simplemente llamar la función save y si todo sale bien,
habremos creado un nuevo documento en la base de datos.

Observemos que el método save se ejecuta sobre el objeto myTweet, el cual es


una instancia del schema,

Create

1. var Tweet = mongoose.model('Tweet', yourSchema);


2.
3. Tweet.create({
4. _creator: ObjectId('5a012b3986b5c864a4fe6225')
5. message: 'Mi primer tweet'
6. }, function (err, myTweet) {
7. if (err) return handleError(err);
8. // Tweet created
9. })

En este nuevo ejemplo, estamos creando un nuevo documento por medio de


método create. Este método se ejecuta directamente sobre el Schema y recibe
como parámetro un objeto con la estructura del documento. Este objeto puede
tener la estructura que sea.

435 | Página
Cabe mencionar, que los dos ejemplos que mostramos a continuación, dan el
mismo resultado.

Find

En MongoBD se le conoce como find a la operación de búsqueda, lo que sería


equivalente al SELECT de SQL. Podemos buscar documentos por medio de dos
métodos, find y findOne. La primera (find) permite buscar cero o más
documentos, mientras que el segundo método (findOne) permite buscar un solo
documento.

Método find

El método find siempre regresa un array, por lo que, si no encuentra ningún


documento, este regresa un array vacío. El formato de este método es
find(query, projection).

El método find es estático, por lo que lo deberemos de ejecutar sobre el Schema,


veamos un ejemplo:

1. Tweet.find({_id: tweet.id},{message:1,image:1}) //Return array => []

En este ejemplo, estamos buscando un tweet por medio de su ID, le indicamos


que solo queremos el campo message e image mediante la proyección.

Método findOne

El método findOne funciona exactamente igual que find, con la única diferencia
de que este solo regresa un objeto, por lo que, si más de un documento
concuerdan con el query, entonces será retornado el primer documento
encontrado. Por otra parte, si no se encuentra ningún documento, entonces se
regresa null.

1. Tweet.findOne({_id: tweet.id},{message:1,image:1}) //Return one object => {}

NOTA: findOne no soporta las operaciones skip, limit y sort.

Métodos Skip, Limit y Sort

Los operadores skip, limit y sort, solo pueden ser utilizados en conjunto de la
operación find. La forma de utilizarlos es la siguiente:

1. find({_id: tweet.id},{message:1,image:1})
2. .sort( { date: 1 } )
3. .limit( 10 )
4. .skip( 5 )
5. .limit(5)
6. .exec(callback(err, returns){

Página | 436
7. //Any action
8. })

Como podemos ver, la función find se ejecuta normalmente, con la diferencia


que agregamos las instrucciones sort, limit, skip, limit y al final siempre
deberemos ejecutar la función exec para ejecutar la consulta. Exec recibe dos
parámetros, el error (si hay) y la respuesta del query.

NOTA: Para los query podemos utilizar todos los operadores lógicos, de
elementos y comparación que vimos al inicio de este capítulo.

Update

Update es el procedimiento por el cual es posible actualizar un documento


previamente existente en la base de datos. Su nombre es el único que
corresponde con los comandos tradicionales de SQL. Existen 4 formas de
actualizar un documento.

Método update

El método estático update es una instrucción que nos permite actualizar solo el
primer documento que concuerden con un filtro sin retornarlo. Como retorno
obtendremos el número de registros seleccionados y el número de documentos
actualizados.

La función tiene el siguiente formato update(query,update), donde query es el


criterio de búsqueda y update son los campos a actualizar

1. Profile.update(
2. {userName: 'oscar'},
3. {name: 'Oscar Blancarte, description: 'Nuevo en Twitter'}
4. function( err, response){
5. //Any action
6. })

El ejemplo anterior actualiza el campo name y description para el usuario con


userName = oscar. Al terminar la consulta, ejecuta una callback con dos
parámetros, un error (si hay) y la respuesta.

La respuesta es un objeto que contiene información de los registros actualizados,


el cual se ve de la siguiente manera:

1. {
2. n: 1,
3. nModified: 1,
4. opTime:
5. { ts: Timestamp { _bsontype: 'Timestamp', low_: 2, high_: 1510367840 }, t: 1 },

6. electionId: 7fffffff0000000000000001,
7. ok: 1
8. }

De la respuesta solo nos deberá interesar 3 campos:

437 | Página
 n: número de documentos seleccionados por el query.
 nModified: Número de documentos realmente actualizados
 ok: indica si la operación termino correctamente.
Por lo general, siempre nos fiamos en el campo nModified para asegurar que la
operación actualizo al menos un documento.

Método updateMany

El método updateMany también es un método estático, y funciona exactamente


igual que update, con la única diferencia de que este método actualizado todos
los documentos que coincidan con el query, a diferencia de update que solo
actualiza el primero documento.

1. Profile.updateMany(
2. {userName: 'oscar'},
3. {name: 'Oscar Blancarte, description: 'Nuevo en Twitter'}
4. function( err, response){
5. //Any action
6. })

La respuesta es también igual que update.

Método save

El método de instancia save, ya lo habíamos analizado, pues sirve para crear un


documento. Sin embargo, también es posible actualizar un documento existente
mediante este método, sin embargo, para que esto funcione como un update,
deberemos ejecutarlo sobre un objeto que allá sido retornado como parte de un
find. Veamos un ejemplo:

1. Profile.findOne({userName: 'oscar'}, function(err, user){


2. user.description = 'Mi nueva descripción'
3. user.save(function(err, saveUser){
4. //Update completed
5. })
6. })

Cómo podemos ver en este ejemplo, buscamos a un usuario (línea 1), luego
actualizamos la descripción (línea 2) directamente sobre el objeto retornado.
Finalmente, guardamos los cambios mediante el método de instancia save.

Esta forma de actualizar solo se recomienda cuando ya tiene el documento


cargado, pues consultar y luego actualizar es doble trabajo para para la base de
datos.

Remove

Página | 438
Remove es la operación que permite borrar uno más documentos de la base de
datos. Esta operación elimina todos los documentos que coinciden con una
operación. El formato para invocar es el siguiente:

1. Profile.remove({userName: 'oscar'}, function(err){


2. //Any action
3. })

En el ejemplo pasado estamos eliminando todos los usuarios que tenga como
nombre de usuario, oscar.

Population

Population es una de las operaciones más poderosas que tiene Mongoose, pues
permite simular la instrucción JOIN de SQL.

Population es una operación por medio de la cual es remplazado un ObjectId por


el documento real. Para lograr esto, Mongoose requiere de un metadato, el cual
le indique de que schema corresponde el ObjectId, para finalmente consultar el
documento y remplazarlo por el ObjectId.

Para entender cómo funciona population, vamos a repasar el schema de Tweet.

1. var tweet = Schema({


2. _creator: {type: Schema.Types.ObjectId, ref: 'Profile'},
3. tweetParent: {type: Schema.Types.ObjectId, ref: 'Tweet'},
4. date: {type: Date, default: Date.now},
5. message: String,
6. likeRef: [{type: Schema.Types.ObjectId, ref: 'Profile', default: []}],
7. image: {type: String},
8. replys: {type: Number, default: 0}
9. })

Observemos los campos _creator y tweetParent, ambos son de tipo ObjectId y


los tiene un atributo llamado ref, este último, le indica a Mongoose, de que
schema pertenece el ObjectId, de esta forma, cuando se ejecuta la operación
populate, Mongoose buscará el atributo ref para luego buscar el documento en
la colección correspondiente.

Veamos un ejemplo para ver cómo funciona:

1. Tweet.find({})
2. .populate("_creator")
3. .exec(function(err, tweets){
4. //Any action
5. })

En este ejemplo, consultamos todos los tweets y luego realizamos un populate


al creador del tweet. Veamos un ejemplo:

439 | Página
El siguiente documento corresponde a un tweet, tal cual se guarda en la colección
de la base de datos. Ahora bien, observa el campo _creator, el cual tiene como
valor el ObjectId del usuario que creo el Tweet.

1. {
2. "_id": "5a0657ad3ccd98529d83a9b9",
3. "_creator": ObjectId('5a05286db5371dffe40bafae'),
4. "date": "2017-11-11T01:51:41.421Z",
5. "message": "test",
6. "likeCounter": 0,
7. "replys": 0,
8. "image": null
9. }

Tras realizar la operación populate, el ObjectId es remplazado por el objeto que


corresponde al usuario (profile).

1. {
2. "_id": "5a0657ad3ccd98529d83a9b9",
3. "_creator": {
4. "_id": "5a05286db5371dffe40bafae",
5. "name": "Jaime",
6. "userName": "jaime",
7. "avatar": "<img base64>"
8. },
9. "date": "2017-11-11T01:51:41.421Z",
10. "message": "test",
11. "likeCounter": 0,
12. "replys": 0,
13. "image": null
14. }

La operación populate funciona para objetos simples como el que acabamos de


ver, pero también funciona bien para los Array, por lo que remplaza todos
ObjectId por el objeto correspondiente.

Página | 440
Resumen

Como hemos analizado en este capítulo, MongoDB es mucho más potente de lo


que hubiéramos imaginado en un inicio, pues permite realizar básicamente todas
las operaciones que realizamos en una base de datos tradicional y hemos
analizado el rol que juega MongoDB en las aplicaciones modernas y por qué vale
la pena aprender esta tecnología.

También nos hemos adentrado al funcionamiento de MongoDB, aprendiendo los


operadores más importantes como lo son los operadores lógicos, operadores de
elementos y operadores de comparación. También hemos aprendido las
seccionas básica de una consulta en Mongo, como lo son el filter, project, sort,
limit, skip.

Tras conocer cómo funciona MongoDB, aprendimos como conectarnos con


NodeJS y cómo es que a través de los schemas es posible definir la estructura de
nuestros documentos y como es que los schemas son la base para realizar las
operaciones más básicas como lo son, find, findOne, update, updateMany,
findOneAndUpdate, remove, save.

441 | Página
Desarrollo de API REST con
NodeJS
Capítulo 15

Tras un largo camino, ha llegado el momento de entrarle de lleno al desarrollo


del API para nuestro proyecto Mini Twitter. Para muchos, este será uno de los
capítulos más interesantes, pues aprenderemos a crear desde cero un API
utilizando NodeJS + Express + MongoDB.

¿Qué es REST y RESTful?

Una de las grandes preguntas que todo desarrollador se plantea al momento de


iniciar el desarrollo de servicios web, es ¿Cuál es la diferencia entre REST y
RESTful? Pues por lo general, no suelen ser explicados de una forma clara,
sumado con que los conceptos se pueden confundir, incluso, muchos creen que
REST es la abreviación de RESTful.

Para empezar, REST (Representational state transfer) es un tipo de Arquitectura


de software que se ejecuta sobre el protocolo de comunicación HTTP. En este
sentido, podríamos decir que todo internet está bajo esta arquitectura, pues
absolutamente toda la comunicación que hace el navegador es mediante HTTP.
Dicho esto, nos podemos dar cuenta que REST no es para nada nuevo.

La arquitectura REST se rige por una serie de principios que son clave para el
entendimiento entre el cliente y el servidor. Estos principios son los siguientes:

 Todos lo que se mueve a través de HTTP es considerado un Recurso y cada


recurso cuenta con una URL única. Lo que significa que no podrán existir dos
recursos diferentes en una misma URL.
 Todos los recursos tienen un formato particular que describe el tipo de
contenido, el cual es utilizado por el cliente para saber cómo interpretar el
recurso recuperado. El tipo de contenido se establece en las cabeceras de
HTTP mediante el valor Content-Type, el cual varía según el tipo de recurso.
Por ejemplo, image/png, image/gif, text/html, application/json,
applicacipon/xml, etc.

Página | 442
 Toda la comunicación por medio de HTTP deberá utilizar los verbos o métodos
definidos por el protocolo, por ejemplo, GET, POST, PUT, DELETE, etc. Ya
hablamos acerca de los verbos en el pasado.
 Un mismo recurso puede tener múltiples representaciones, lo que quiere
decir que, es posible enviar la misma información en diferente formato, por
ejemplo, es posible enviar un documento en formato JSON o en XML o una
imagen en formato JPEG o PNG.
 Toda la comunicación que se realiza por medio de HTTP es sin estado
(Stateless), lo que significa que cada petición es tratada de forma
independiente y todas las ejecuciones con los mismos parámetros de entrada
deberá arrojar el mismo resultado.

Por otra parte, tenemos RESTful, el cual son los servicios web que se crean
siguiente la arquitectura REST y los principios fundamentales.

A pesar de que REST ya es viejo, los servicios RESTful empezaron a tomar


protagonismo con la llega de la arquitectura SOA, pues se empezó a ver la
ventaja de utilizar los servicio como una forma de integrar a aplicación y
desacoplando los componentes con recursos web accesibles de forma estándar e
interoperables.

En este sentido, podríamos resumir que REST es una arquitectura de software y


RESTful son servicio web implementados utilizados la arquitectura REST.

Fig. 195 - REST y RESTful

REST vs SOA

Es muy probable que el termino SOA (Service-Oriented Architecture) te sea


mucho más conocido que REST o RESTful, pues SOA es un tipo de arquitectura
que ha tenido mucho éxito en los últimos años. Sin embargo, cuando nos
ponemos a pensar detenidamente, es muy probable que ahora te estés
preguntando qué diferencia tiene REST y SOA o más precisamente RESTful y
SOA, pues estas dos últimas promueven la creación de servicios.

443 | Página
Para responder esta pregunta, es necesario entender que es SOA. SOA es un tipo
de arquitectura de software que promueve el desarrollo de servicios como la
unidad más pequeña de un software, y que a partir de los servicios es posible
crear cosas más complejas.

Ahora bien, tanto en SOA como en REST es posible crear servicios, pero existe
una diferencia importante, REST se limita a la comunicación mediante el
protocolo HTTP, mientras que SOA es una arquitectura de más alto nivel, en
donde no se habla de una tecnología específica para el transporte de mensajes,
como lo es HTTP. Esto quiere decir que con SOA es posible tener servicios con
HTTP, pero también es posible utilizar otras tecnologías como Colas de mensajes,
correo electrónico, FTP, TCP, etc.

En este sentido, podríamos decir que RESTful implementa la arquitectura SOA y


REST al mismo tiempo, sin embargo, SOA puede o no implementar REST.

Fig. 196 - La relación entre SOA, REST y RESTful.

Preparar nuestro servidor REST

Finalmente, tras una pequeña introducción a lo que es SOA, REST y RESTful,


podemos iniciar con el desarrollo del API. Para ello, lo primero que necesitaremos
será, crear un servidor capaz de procesar las solicitudes HTTP y atender en los
diferentes métodos.

En el capítulo 9 React Routing, creamos un pequeño servidor (server.js) el cual


lo utilizamos para servir la aplicación Mini Twitter en todas las URL, sin embargo,
apenas hablamos de cómo funcionaba, pues no era el momento para hacerlo.
Ahora que ya hemos explicado un poco de NodeJS, Express y MongoDB, ha
llegado el momento de retomar este archivo, complementarlo.

Configuración inicial del servidor

Página | 444
Lo primero que haremos será analizar el archivo server.js tal cual lo tenemos
hasta el momento. El archivo se debería ver de la siguiente manera:

1. var express = require('express');


2. var app = express();
3. var bodyParser = require("body-parser")
4. var path = require('path')
5. var webpack = require('webpack')
6. var config = require('./webpack.config')
7. var compiler = webpack(config)
8.
9. app.use('/public', express.static(__dirname + '/public'));
10. app.use(bodyParser.urlencoded({extended: false}));
11. app.use(bodyParser.json({limit:'10mb'}));
12.
13. app.use(require('webpack-dev-middleware')(compiler, {
14. noInfo: true,
15. publicPath: config.output.publicPath
16. }));
17.
18.
19. app.get('/*', function (req, res) {
20. res.sendFile(path.join(__dirname, 'index.html'));
21. });
22.
23. app.listen(8080, function () {
24. console.log('Example app listening on port 8080!');
25. });

En la primera sección (líneas 1 a 7) tenemos los import de todos los módulos


requeridos, como Express, body-parser, path y Webpack. De estos ya hemos
hablado en el pasado, con la diferencia de path, el cual es el módulo de utilidades
para trabajar con rutas de archivos y directorios, nada del otro mundo; a medida
que sea requerido lo vamos a ir explicando.

En la línea 9 creamos un Middleware incorporado static para exponer la carpeta


public de forma pública. De esta forma, todo el contenido de la carpeta public,
podrá ser accedida directamente desde el navegador mediante le path
<host>/public/.

En las líneas 10 y 12 configuramos el middleware body-parser para procesar los


mensajes entrantes y convertirlos en JSON.

En las líneas 13 a 16 estamos utilizando el módulo webpack-dev-middleware para


compilar todos los archivos JavaScript y crear el archivo bundle.js. Este
middleware solo lo deberemos utilizar en ambiente de desarrollo, pues crea el
archivo bundle.js en memoria, además que está diseñado para ayudar al
desarrollador.

En las líneas 19 a 21 creamos un router para atender todas las peticiones (/*)
sin importar el path, el cual regresará el archivo index.html. Con este router nos
aseguramos que la aplicación pueda responder en cualquier URL y no solo en la
raíz del dominio.

Finalmente, en las líneas 23 a 25 levantamos el servidor y le indicamos que reciba


peticiones por el puerto 8080. El método listen recibe dos parámetros, el puerto
por que estará escuchando peticiones y un callback que se ejecuta cuando el

445 | Página
servidor se inicia, en el cual se puede hacer cualquier cosa, aunque por lo general
se escribe en el log para informar al usuario que el servicio ya está activo.

Hasta este punto tenemos un servidor que atiende únicamente a la aplicación


React y muestra los archivos estáticos, como lo son styless.css, bundle.js, etc.

Recuerda que para el funcionamiento de Express es necesario instalar los


paquetes:

“npm install --save express” y “npm install --save body-parser”

Establecer conexión con MongoDB

Para conectar nuestro servidor a la base de datos MongoDB realizaremos los


siguientes ajustes al archivo server.js:

1. var express = require('express');


2. var app = express();
3. var bodyParser = require("body-parser")
4. var path = require('path')
5. var webpack = require('webpack')
6. var config = require('./webpack.config')
7. var compiler = webpack(config)
8. var mongoose = require('mongoose')
9. var configuration = require('./config')
10.
11. var opts = {
12. useNewUrlParser: true,
13. appname: "Mini Twitter",
14. poolSize: 10,
15. autoIndex: false,
16. bufferMaxEntries: 0,
17. reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect
18. reconnectInterval: 500,
19. autoReconnect: true,
20. loggerLevel: "error", //error / warn / info / debug
21. keepAlive: 120,
22. validateOptions: true
23. }
24.
25. let connectString = configuration.mongodb.development.connectionString
26. mongoose.connect(connectString, opts, function(err){
27. if (err) throw err;
28. console.log("==> Conexión establecida con MongoDB");
29. })
30.
31. app.use('/public', express.static(__dirname + '/public'))
32. app.use(bodyParser.urlencoded({extended: false}))
33. app.use(bodyParser.json({limit:'10mb'}))
34.
35. app.use(require('webpack-dev-middleware')(compiler, {
36. noInfo: true,
37. publicPath: config.output.publicPath
38. }))
39.
40. app.get('/*', function (req, res) {

Página | 446
41. res.sendFile(path.join(__dirname, 'index.html'))
42. });
43.
44. app.listen(8080, function () {
45. console.log('Example app listening on port 8080!')
46. });

Lo primero que realizaremos será, agregar los import a Mongoose (línea 8) y


nuestro archivo de configuración config.js (línea 9).

El siguiente paso es definir las propiedades para configurar Mongoose (líneas 11


a 23), las cuales explicamos en el capítulo anterior.

Finalmente, obtenemos el String de conexión (línea 25) del archivo de


configuración y establecemos conexión con MongoDB mediante el método
connect de Mongoose (línea 26 a 28). La conexión también la explicamos en la
sección pasada.

En este punto solo faltaría guardar los cambios para que la aplicación se actualice,
de tal forma que, deberemos ver el siguiente resultado en la consola:

Fig. 197 - Conexión exitosa a MongoDB.

Creando un subdominio para nuestra API

Una de las cuestiones más importantes a la hora de publicar un API por internet,
es definir la URL por medio de la cual el API atenderá las solicitudes. Dicha URL
debe de ser significativa, de tal forma que con tan solo ver la URL podremos
identificar que API es y a qué ambiente pertenece (desarrollo, pruebas,
producción).

Por lo general, las API son publicadas sobre el mismo domino de la aplicación
principal, y existen dos formas de hacer esto, la primera y más simple es que
reservemos un path del dominio para atender solicitudes del API, por ejemplo:
http://test.com/api/*. Esto quiere decir que cualquier cosa que llegue con el
path /api, será atendida por el API. Esta estrategia puede resultar atractiva, sin
embargo, no es lo más recomendable, por varias razones:

447 | Página
 la primer y más importante, es que tanto la aplicación como el API
compartirían la misma IP, pues a nivel DNS, es el mismo dominio, lo que
complica la gestión.
 En segundo lugar, tenemos que reservar paths para el API, lo que nos
quita toda la gama de URL para la aplicación.
 En tercer lugar, el ser una subsección del dominio principal, Google o los
demás buscadores podrían indexar los servicios o las páginas de
documentación, como parte de la aplicación, lo cual no siempre es
deseado.

La segunda opción es publicar nuestra API como un subdominio de nuestro


dominio principal, por ejemplo http://api.test.com/*. De esta forma, podemos
configurar en nuestro DNS una dirección IP de destino, de esta forma, separamos
la administración de nuestra aplicación de nuestra API.

Otra de las ventajas, es que ya no es necesario reservar ninguna URL para el API
y como es un subdominio es posible instalarle el mismo certificado de seguridad
del dominio principal.

Exponer el API como un subdominio es una mejor estrategia, y una muestra clara
de esto, son el API de:

 Paypal: https://api.paypal.com/
 Uber: https://api.uber.com
 Facebook: https://graph.facebook.com
 Twitter: https://api.twitter.com

Por los motivos que hemos explicado, en nuestro proyecto Mini Twitter, hemos
decidido crear un subdominio para nuestra API, de tal forma que la URL quedaría
de la siguiente manera http://api.<domain>. Ahora bien, Dado que estamos
desarrollando de forma local, <domain> se remplaza por localhost, de tal forma
que el API lo configuraremos para trabar en http://api.localhost:8080.

Implementando un Virtual Host en NodeJS

Un Virtual Host es la capacidad de un servidor web de tener un alojamiento


compartido, es decir que, por medio del mismo servidor, es posible atender dos
o más aplicaciones diferentes.

Mediante un Virtual Host es posible distinguir entre las solicitudes que entran al
dominio principal (localhost) y de los que entra a un subdominio (api.localhost)
y en NodeJS es extremadamente fácil realizar esta configuración, pero antes de
eso, vamos a necesitar instalar el módulo vhost mediante el siguiente comando:

npm install --save [email protected].

Página | 448
Y vamos a instalar un motor de plantillas de NodeJS llamado Pug, que nos
ayudará a crear páginas web más fácil, del cual hablaremos más adelante,
cuando empezásemos a documentar el API.

npm install --save [email protected]

Una vez que tenemos las dependencias, vamos a necesitar crear el archivo api.js
en el path /api, en el cual vamos procesar todas las solicitudes que lleguen al
API. El archivo se deberá ver de la siguiente manera:

1. var express = require('express')


2. var router = express.Router()
3.
4. const pug = require('pug')
5.
6. router.get('/', function(req, res) {
7. res.send(pug.renderFile(__dirname + '/../public/apidoc/api-index.pug'))
8. })
9.
10. router.get('/*', function(req, res, err){
11. res.status(400).send({message: "Servicio inválido"})
12. })
13.
14. module.exports = router;

En este archivo estamos utilizando un Middleware direccionador (línea 2), luego,


en las líneas 6 y 10, creamos dos router, una para escuchar en la raíz del
subdominio (“/”) y otro para cualquier otra dirección (“/*”), para finalmente,
exportar el Router (línea 14). Es importante expotar el Router, porque más
adelante vamos a necesitar referenciarlo.

El primero router (línea 6) escuchará únicamente en la raíz del subdominio, es


decir http://api.localhost:8080 y tiene la finalidad de mostrar los términos de uso
del API, así como un link a la documentación. Para lograr eso, nos estamos
apoyando del motor de plantillas Pug, el cual permite desarrollar HTML de una
forma mucho más simple. Lo que hacemos es procesar el archivo api-index.pug
para convertirlo en HTML. Crearemos este archivo un poco más adelante.

Por otra parte, tenemos un router que escucha en cualquier URL del subdominio.
Este router es importante, porque se ejecutará cuando ninguna regla de route se
cumpla y de esta forma, es posible enviarle un mensaje de error al usuario.

El siguiente paso será crear el archivo api-index.pug en el path /public/apidoc


el cual se verá de la siguiente manera:

1. html
2. head
3. link(href='https://fonts.googleapis.com/css?family=Roboto', rel='stylesheet')
4. link(
5. rel='stylesheet'
6. href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css'
7. integrity='sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u'
8. crossorigin='anonymous'
9. )
10. link(rel='stylesheet', href='/public/apidoc/api-styles.css')
11. body
12. .api-alert

449 | Página
13. p.title Mini Twitter API REST
14. p.body
15. | Esta API es provista como parte de la aplicación Mini Twitter,
16. | la cual es parte del libro
17. a(href='#')
18. strong "Desarrollo de aplicaciones reactivas con React, NodeJS y MongoDB"
19. | , por lo que su uso es únicamente con fines educativos y por
20. | ningún motivo deberá ser empleada para aplicaciones productivas.
21. .footer
22. button.btn.btn-warning(data-toggle='modal', data-target='#myModal')
23. | Terminos de uso
24. a.btn.btn-primary(href='/catalog') Ver documentación
25.
26. #myModal.modal.fade(
27. tabindex='-1', role='dialog', aria-labelledby='myModalLabel',
28. aria-hidden='true')
29. .modal-dialog
30. .modal-content
31. .modal-header
32. button.close(type='button', data-dismiss='modal',
33. aria-hidden='true') ×
34. h4#myModalLabel.modal-title Terminos de uso
35. .modal-body
36. p El API de Mini Twitter es provista por Oscar Blancarte, autor del libro
37. strong "Desarrollo de aplicaciones reactivas con React, NodeJs y MongoDB"
38. | con fines exclusivamente educativos.
39. p Esta API es provista
40. strong "tal cual"
41. | esta, y el autor se deslizanda de cualquier problema o falla
42. | resultante de su uso. En ningún momento el autor será responsable
43. | por ningún daño directo o indirecto por la pérdida o publicación
44. | de información sensible.
45. strong El usuario es el único responsable por el uso y la información
46. | que este pública.
47. .modal-footer
48. button.btn.btn-primary(type='button', data-dismiss='modal') Cerrar
49.
50.
51. script(src='https://code.jquery.com/jquery.js')
52. script(src='//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js')

No te preocupes por entender su contenido, más adelanta regresaremos para


explicar cómo funciona Pug, por lo que solo limitémonos a copiarlo tal como está.

El siguiente paso será crear el archivo api-styles.css en el path /public/apidoc,


el cual se verá de la siguiente forma:

1. body {
2. background-color: #fafafa;
3. background: #1E5372;
4. background: -webkit-linear-gradient(left, #1C3744 , #1E5372); /
5. background: -o-linear-gradient(right, #1C3744, #1E5372);
6. background: -moz-linear-gradient(right, #1C3744, #1E5372);
7. background: linear-gradient(to right, #1C3744 , #1E5372);
8. }
9.
10. *{
11. font-family: 'Roboto', sans-serif;
12. color: #333;
13. }
14.
15. .hljs {

Página | 450
16. display: block;
17. overflow-x: auto;
18. padding: 0.5em;
19. color: #abb2bf;
20. background: #282c34;
21. }
22.
23. .hljs-comment,
24. .hljs-quote {
25. color: #5c6370;
26. font-style: italic;
27. }
28.
29. .hljs-doctag,
30. .hljs-keyword,
31. .hljs-formula {
32. color: #c678dd;
33. }
34.
35. .hljs-section,
36. .hljs-name,
37. .hljs-selector-tag,
38. .hljs-deletion,
39. .hljs-subst {
40. color: #e06c75;
41. }
42.
43. .hljs-literal {
44. color: #56b6c2;
45. }
46.
47. .hljs-string,
48. .hljs-regexp,
49. .hljs-addition,
50. .hljs-attribute,
51. .hljs-meta-string {
52. color: #98c379;
53. }
54.
55. .hljs-built_in,
56. .hljs-class .hljs-title {
57. color: #e6c07b;
58. }
59.
60. .hljs-attr,
61. .hljs-variable,
62. .hljs-template-variable,
63. .hljs-type,
64. .hljs-selector-class,
65. .hljs-selector-attr,
66. .hljs-selector-pseudo,
67. .hljs-number {
68. color: #d19a66;
69. }
70.
71. .hljs-symbol,
72. .hljs-bullet,
73. .hljs-link,
74. .hljs-meta,
75. .hljs-selector-id,
76. .hljs-title {
77. color: #61aeee;
78. }
79.
80. .hljs-emphasis {
81. font-style: italic;

451 | Página
82. }
83.
84. .hljs-strong {
85. font-weight: bold;
86. }
87.
88. .hljs-link {
89. text-decoration: underline;
90. }
91.
92. .badge{
93. background-color: #E74C3C;
94. }
95.
96. api-alert p{
97. margin: 0px;
98. }
99.
100. .api-alert{
101. margin: auto;
102. width: 40%;
103. margin-top: 50px;
104. background-color: #fafafa;
105. padding: 20px;
106. border-top: 5px solid #FEB506;
107. border-radius: 5px;
108. }
109.
110. .method-templete{
111. margin: auto;
112. width: 80%;
113. margin-top: 50px;
114. margin-bottom: 50px;
115. background-color: #fafafa;
116. padding: 20px;
117. border-top: 5px solid #FEB506;
118. border-radius: 5px;
119. }
120.
121. .secure-icon{
122. font-size: 50px;
123. float: right;
124. }
125.
126. .api-alert .title{
127. font-size: 28px;
128. text-align: center;
129. margin-bottom: 20px;
130. }
131.
132. .api-alert .body{
133. font-size: 20px;
134. text-align: justify;
135. }
136.
137. .api-alert .body strong{
138. font-style: italic;
139. color: #3498DB;
140. }
141.
142. .api-alert .footer{
143. margin-top: 20px;
144. text-align: center;
145. }
146.
147. .api-alert .footer a{

Página | 452
148. margin-left: 20px;
149. }
150.
151. .label{
152. margin-left: 20px;
153. font-size: 12px;
154. }

Este archivo de estilos es solo para el API, y no lo compartiremos con el proyecto


Mini Twitter.

Una vez hecho esto, tendremos que regresar al archivo server.js y agregar solo
las líneas marcadas:

1. var express = require('express');


2. var app = express();
3. var bodyParser = require("body-parser")
4. var path = require('path')
5. var webpack = require('webpack')
6. var config = require('./webpack.config')
7. var compiler = webpack(config)
8. var mongoose = require('mongoose')
9. var configuration = require('./config')
10. var vhost = require('vhost')
11. var api = require('./api/api')
12.
13. var opts = {/**props**/}
14.
15. let connectString = configuration.mongodb.development.connectionString
16. mongoose.connect(connectString, opts, function(err){
17. if (err) throw err;
18. console.log("==> Conexión establecida con MongoDB");
19. })
20.
21. app.use('/public', express.static(__dirname + '/public'))
22. app.use(bodyParser.urlencoded({extended: false}))
23. app.use(bodyParser.json({limit:'10mb'}))
24.
25. app.use(require('webpack-dev-middleware')(compiler, {
26. noInfo: true,
27. publicPath: config.output.publicPath
28. }))
29.
30. app.use(vhost('api.*', api));
31.
32. app.get('/*', function (req, res) {
33. res.sendFile(path.join(__dirname, 'index.html'))
34. });
35.
36. app.listen(8080, function () {
37. console.log('Example app listening on port 8080!')
38. });

Lo primero que haremos será importar el módulo vhost (línea 10) y el archivo
api.js (línea 11) que acabos de crear.

El segundo paso será crear el Virtual Host mediante la línea 30. Lo que estamos
haciendo en esta línea es crear un Middleware con vhost, el cual tomará todas
peticiones que lleguen al path api.* y las envíe al archivo api.js, más
precisamente, al Router definido dentro.

453 | Página
Con estos simples pasos, hemos terminado de crear nuestro Virtual Host, y no
solo eso, sino que, además, se ve muy bien. Para probarlo, entra a la URL
http://api.localhost:8080 en tu navegador para que veas los resultados:

Fig. 198 - Virtual Host con NodeJS.

Configurar subdominio
Recuerda configurar tu sistema operativo para que
redireccione las peticiones del subdominio
api.localhost a la ip 127.0.0.1, de lo contrario no
podrá ser accedido.

Cross-origin resource sharing (CORS)

En este punto, podrías pensar que nuestra API ya está funcionando


correctamente y ya la podemos integrar con nuestra aplicación, sin embargo,
esto es una verdad a medias, pues al momento de ser consumida desde nuestro
proyecto Mini Twitter, el navegador nos lanzará error. Esto debido a que por
seguridad, el navegador bloquea cualquier petición AJAX a un recurso fuera del
dominio en el que se encuentra la página.

Ante el navegador, un subdominio, es un dominio diferente, por lo que lanzará


un error como el siguiente:

Fig. 199 - CORS error.

Página | 454
Para solucionar este problema, es necesario agregar un header a nuestra
respuesta del servidor, permitiendo que el navegador aprueba la ejecución de
recursos externos, este header es el Access-Control-Allow-Origin. Por suerte,
hay un módulo que nos puede ayudar con eso, de tal forma que agregue por
nosotros el header en todas las respuestas. El módulo es cors y lo podemos
instalar mediante el comando:

npm install --save [email protected].

1. let connectString = configuration.mongodb.development.connectionString


2. mongoose.connect(connectString, opts, function(err){
3. if (err) throw err;
4. console.log("==> Conexión establecida con MongoDB");
5. })
6.
7. app.use('*', require('cors')());
8.
9. app.use('/public', express.static(__dirname + '/public'))

En la línea 7 agregamos el Middleware Cors que se ejecuta ante cualquier path


(*), el cual agregará el header “Access-Control-Allow-Origin: *”.

Con este simple paso, el API ya podrá ser invocado desde la aplicación.

Desarrollo del API REST

Estamos a punto de empezar a desarrollar los servicios de nuestra API REST,


pero antes de eso, quisiera abordar dos temas importantes de los cuales no
hemos hablado, el primero son las mejores prácticas para la creación de URL y
los códigos de respuesta REST.

Mejores prácticas para crear URI’s

Diseñar un API REST no se trata solo de crear servicios que cumplan su función,
sino que, además, las URL de los servicios deben de darla al usuario una idea
bastante clara de lo que hace un servicio. Un servicio con una URL bien diseña,
puede decirle al usuario que hace sin necesidad de leer la documentación.

Uso correcto de los métodos (Verbs)

Utilizar correctamente los métodos es sin duda, una de las principales cosas que
debemos de respetar, pues una mala implementación puede llegar a ser
sumamente confuso. Recordemos que los métodos más utilizados son:

455 | Página
 GET: Solo lectura, y se utiliza para consultar información
 POST: Se utiliza para creación de un nuevo registro.
 PUT: Remplazar actualizar/remplazar un registro
 DELETE: Se utiliza para eliminar un registro.

Una URL por sí sola, no nos dice que operación va a realizar sobre un registro,
por ejemplo, veamos la siguiente URL:

/user/juan

Si vemos la URL, podemos comprender rápidamente, que es un servicio de


usuarios, y que va hacer algo con el usuario “juan”, sin embargo, no está claro
hasta que tenemos el método, que todo cobra sentido. Si lo ejecutamos con el
método GET, entonces podemos asumir que el servicio está consultando al
usuario juan, pero si ejecutamos esa misma URL con el método DELETE, entonces
podemos entender que vamos a eliminar al usuario juan.

En este sentido, si empezamos a revolver los métodos para realizar operaciones


para las que no fueron diseñadas, volveremos loco al usuario.

URL simples, compactas y concretas

Al momento de definir las URL, debemos evitar utilizar cosas como


/findUserByName/juan o /deleteUser/juan. En el primer ejemplo, estamos
complicando las cosas, pues una URL como /users/juan en el método GET, dice
exactamente lo mismo. En el segundo ejemplo, poner la frase delete está de
más, pues con el método DELETE es suficiente para dar a entender.

Utilizar lo más posible las URL params

Uno de los grandes errores al definir las URL de nuestros servicios, es no definir
URL params, y en su lugar, esperar que los parámetros requeridos vengan como
parte del payload. En la práctica, recibirlo en el payload o como URL param, dará
el mismo resultado, sin embargo, para el usuario siempre será más claro definir
los parámetros como parte de la URL.

Camel Case

Existen ocasiones donde una sola palabra no es suficiente para representar el


objetivo de un servicio, en estos casos es posible unir palabras utilizando la
notación Camel Case, iniciando en minúscula y agregando Mayúsculas al inicio
de la siguiente palabra.

Página | 456
Por ejemplo, imagina que tienes un servicio de búsqueda de órdenes de ventas
y compras, un servicio con el path /ordes/:id sería un poco confuso, pues no
sabrías que tipo de orden estas buscando, en este caso podrías agregar
/salesOrders y /purchaseOrders.

Códigos de respuesta

Todos los servicios REST por el simple hecho de trabajar sobre HTTP, retornan
un código de respuesta, este código de respuesta es un indicativo para el cliente
sobre lo que paso durante la ejecución. Los códigos de respuesta más utilizados
son:

 200: Respuesta de OK, es utilizada para responder una respuesta exitosa


 400: Bad request, indica que los parámetros enviados son inválidos.
 401: Unauthorized, es retornado cuando no tenemos privilegios para
consumir un recurso.
 404: Not found, indica que el recurso solicitado no existe.
 500: Internal Server Error, indica que se produjo un error inesperado en el
servidor.
Consulta la lista completa de códigos de respuesta.

Migrando a nuestra API REST

Debido a que ya vamos a empezar a trabajar con nuestra propia API REST, será
necesario redirigir las llamadas del proyecto Mini Twitter a nuestra nueva API.
Para lograr eso, tendremos que entrar al archivo config.js que se encuentra en
la raíz del proyecto y cambiar el host y el puerto para apuntar a nuestra API.

1. module.exports = {
2. debugMode: true,
3. server: {
4. port: 8080,
5. host: "http://api.localhost"
6. },
7. tweets: {
8. maxTweetSize: 140
9. },
10. mongodb: {
11. development: {
12. connectionString: "<dev conneción string>"
13. },
14. production: {
15. connectionString: "<prod connection string>"
16. }
17. }
18. }

Deberemos apuntar al localhost y cambiar el puerto al 8080, pues es el puerto


en el que responde nuestra API.

457 | Página
En el momento en que realicemos este cambio, la aplicación Mini Twitter dejará
de funcionar por completo, pues no estará disponible ninguno de los servicios
necesarios para funcionar. Por ese motivo, vamos a ir desarrollando los servicios
más importantes para el correcto funcionamiento y nos iremos adentrando a los
menos necesario al final.

Implementar los servicios REST

En esta sección pasaremos de lleno a implementar los servicios REST que le dan
vida a la aplicación Mini Twitter.

Servicio - usernameValidate

El primero servicio que vamos a desarrollar, es el de validación de nombre de


usuario, el cual nos permite validar si un nombre de usuario ya está siendo
utilizado o está disponible.

Antes de empezar con el desarrollo, analicemos la siguiente ficha, la cual describe


la forma en que deberemos desarrollar el servicio:

Nombre Validación de disponibilidad de nombre de usuario


URL /usernameValidate/:username
URL Username: nombre del usuario a validar
params
Método GET
Request N/A
Response
 OK: Valor boolean que indica si el usuario está
disponible o no
 Message: Leyenda para mostrar al usuario en
caso de estar o no disponible.

1. {
2. "ok": true,
3. "message": "Usuario disponible"
4. }

Una vez que hemos analizado la ficha, ya sabemos los detalles básicos para su
implementación, como la URL, el método y el formato del request/response.

Lo primero que haremos será crear la carpeta controllers, la cual deberá estar
dentro del path /api, en esta nueva carpeta, vamos a crear el archivo
UserController.js. Dentro de este archivo vamos a crear todos los servicios que
involucran a los usuarios.

Página | 458
Nuevo concepto: Controller
Los controllers son clases diseñadas para encapsular
todas las operaciones que afectan al modelo de datos,
recibiendo las órdenes del usuario, procesando la
solicitud y regresando un resultado.

La intención de usar clases controladores, es separar la lógica de routeo que


definimos en el archivo api.js de la lógica de negocios. De esta forma, el api
tomará los parámetros de entrar y delegará la responsabilidad al controller.

El archivo UserController.js deberá tener el siguiente contenido:

1. var Profile = require('../models/Profile')


2.
3. function usernameValidate(req, res, err){
4.
5. Profile.find().byUsername(req.params.username.toLowerCase()).exec()
6. .then(function(profiles){
7. if (profiles.length > 0) throw {message: "Usuario existente"}
8. res.status(200).send({
9. ok: true,
10. message: "Usuario disponible"
11. })
12. }).catch(function(err){
13. res.status(200).send({
14. ok: false,
15. message: err.message || "Error al validar el nombre de usuario"
16. })
17. })
18. }
19.
20. module.exports = {
21. usernameValidate
22. }

Dado que vamos a trabajar con los modelos de Mongoose, tendremos que
importar el modelo Profile en la línea 1.

La función usernameValidate (línea 3) será ejecutada por express, es por ese


motivo que debemos recibir los parámetros req, res y err.

En la línea 5 realizamos una búsqueda por medio de la función helper que


definimos en el modelo Profile (byUsername), el cual recibe como parámetro el
nombre de usuario a buscar. El nombre de usuario lo recuperamos del URL param
(req.params.username) y ejecutamos la búsqueda.

En la línea 7 validamos si la consulta retorno algún registro. Si la búsqueda


retorna al menos un registro, quiere decir que el usuario ya existe y un mensaje
de error es retornado, por otra parte, si no hay registros, quiere decir que el
usuario está disponible.

Finalmente, exportamos la función (línea 21) para que pueda ser referencia
desde fuera.

459 | Página
El paso final, será agregar la regla de routeo /usernameValidate a nuestra API,
de tal forma que, cuando llegue una solicitud, este la delegue a nuestro
controller. Para lograr esto, tendremos que editar nuestro archivo api.js para
agregar las líneas marcadas:

1. var express = require('express')


2. var router = express.Router()
3. var userController = require('../api/controllers/UserController')
4. const pug = require('pug')
5.
6. router.get('/', function(req, res) {
7. res.send(pug.renderFile(__dirname + '/../public/apidoc/api-index.pug'))
8. })
9.
10. //Public access services
11. router.get('/usernameValidate/:username', userController.usernameValidate)
12.
13.
14. router.get('/*', function(req, res, err){
15. res.status(400).send({message: "Servicio inválido"})
16. })
17.
18. module.exports = router;

El primer paso es importar nuestro controller (línea 3) y el segundo será agregar


el route a nuestro objeto Router (línea 11). Veamos que este route, escucha en
el path /usernameValidate/:username y envía la solicitud al método la función de
nuestro controller.

Para validar el funcionamiento del servicio, es necesario dirigirnos directamente


a http://localhost:8080/signup, y capturar un nombre de usuario:

Fig. 200 - Probando el servicio usernameValidate.

Página | 460
Servicio - Signup

El siguiente servicio que vamos a desarrollar, es el de creación de cuentas


(Signup). El cual nos va permitir crear nuevos usuarios para la aplicación.

Antes de empezar con el desarrollo analicemos su ficha:

Nombre Alta de usuarios


URL /Signup
Método POST
Request
 name: nombre de la persona que crea la
cuenta.
 username: nombre de usuario.
 password: contraseña de acceso.
Ejemplo:

1. {
2. "name": "Juan Perez ",
3. "username": "juan",
4. "password": "1234"
5. }

Response
 Ok: booleana que indica si la operación fue
exitosa o no.
 Profile: contiene los datos del usuario creado,
entre los que destacan
o _id: ID único del usuario
o date: fecha de creación

1. {
2. "ok": true,
3. "body": {
4. "profile": {
5. "__v": 0,
6. "name": "Juan Perez",
7. "userName": "juan",
8. "password": "",
9. "_id": "5a1f40142aab1e2318d65e98",
10. "date": "2017-11-29T23:17:40.508Z",
11. "followersRef": [],
12. "followingRef": [],
13. "tweetCount": 0,
14. "banner": null,
15. "avatar": null,
16. "description": "Nuevo en Twitter"
17. }
18. }
19. }

461 | Página
Una vez analizada la ficha, iniciaremos agregando la función signup dentro del
archivo UserController.js el cual procesará las solicitudes para el alta de
usuarios, la función se verá de la siguiente manera:

1. function signup(req, res, err){


2.
3. const newProfile = new Profile({
4. name: req.body.name,
5. userName: req.body.username.toLowerCase(),
6. password: bcrypt.hashSync(req.body.password, 10)
7. })
8.
9. newProfile.save()
10. .then(()=> {
11. res.send({
12. ok: true,
13. body: {
14. profile: newProfile
15. }
16. })
17. }).catch(err => {
18. let errorMessage = null
19. if(err.errors!=null && err.errors.userName != null){
20. errorMessage = "Nombre de usuario existente"
21. }else{
22. errorMessage = 'Error al guardar el usuario'
23. }
24. res.send({
25. ok:false,
26. message:"Error al crear el usuario",
27. error: errorMessage
28. })
29. })
30. }

El primer paso será crear un objeto Profile mediante el schema de Mongoose


(línea 3 a 7) utilizado los parámetros enviados en el payload de la petición. Para
crear el profile es necesario el nombre (name), nombre de usuario (username) y el
password, sin embargo, no podemos guardar el password tal cual llega a nuestra
API, pues eso es un issue de seguridad muy grave, que podría ser aprovechado
por cualquier usuario con acceso a la base de datos, pues podría conocer los
password de los usuarios. Por tal motivo, es necesario encriptarlos mediante la
librería bcrypt, la cual instalaremos de la siguiente manera:

npm install --save [email protected] [email protected]

Y tendríamos que agregar el import correspondiente al inicio del archivo:

1. var bcrypt = require('bcrypt')

Mediante la función hashSync estamos creando un hash del password, el cual


luego puede ser comparado sin necesidad de conocer el password original que
creo dicho hash.

Una vez creado el objeto Profile, solo resta guardarlo en la base de datos
mediante la función save (línea 9) que proporciona el Schema. Si todo sale bien,
el perfil retornado por Mongoose es retornado con el ID generado. Por otra parte,
si hay algún error, lo regresamos al cliente.

Página | 462
Cabe mencionar que en la línea 19 validamos si hay un error de userName
duplicado, esto con ayuda del plugin mongoose-unique-validator. Los errores los
encontramos en err.errors.{field}, donde field es el campo que tiene algún
error.

Para concluir con este archivo, solo faltaría exportar la nueva función al final del
archivo.

El último paso será agregar el Router al archivo api.js, el cual agregaremos


debajo del router de usernameValidate:

1. //Public access services


2. router.get('/usernameValidate/:username', userController.usernameValidate)
3. router.post('/signup', userController.signup)

Para validar que todo funciona correctamente, podemos intentar crear un nuevo
usuario desde la página de signup y comprobar el registro está en MongoDB.

Autenticación con JSON Web Token (JWT)

Una de las grandes características de un API, es que debe de ser seguro, por eso,
implementar un sistema de autenticación para el API es indiscutible. La
importancia de implementar seguridad del lado del servidor, es impedir que
usuarios no autenticados puedan acceder a recursos restringidos.

En la actualidad existen muchas formas de implementar seguridad en un API,


como es el clásico user/password, OAuth, certificados, autorización por IP, HTTP
basic authentication, tokens, etc.

De todas las opciones que existen, el uso de token es una de las más populares,
pues permite la autenticación sin necesidad de tener que enviar nuestras
credenciales cada vez que necesitamos consumir el API. Por otra parte, el Token
permite guardar datos adicionales, los cuales pueden ser descifrado y
aprovechados del lado del API, como es el caso de la fecha de vigencia, la cual
permite invalidar un Token que tiene cierto tiempo de haber sido creado.

En el caso del proyecto Mini Twitter, utilizaremos la autenticación por medio de


Tokens, pero nos apoyaremos en el cásico user/password para la generación del
token. De esta forma, el usuario tendrá que autenticarse enviando su usuario y
password para que el API le regrese un Token. Una vez con el token, el cliente
solo tendrá que enviar el token generado por el API en todas las invocaciones.
De esta forma, el API sabrá cuál es el usuario que intenta acceder.

En el artículo Autenticación con JSON Web Token explico con mucho más detalle
acerca de este tema, por si quiere profundizar en el aprendizaje de esta fantástica
herramienta.

463 | Página
Antes de iniciar con la implementación de JWT tenemos que entender que existen
servicios que no requieren autenticación y otros que sí, por eso motivo, tenemos
que tener una lógica para identificar cuáles serán protegidos y cuáles no. Para
facilitar las cosas, nos vamos a apoyarnos en la URL para realizar esa
diferenciación, de tal forma que todos los servicios que inicien en /secure/* será
sujetos a autenticación, mientras que el resto no.

A pesar de que solo las URL /secure/* son protegidas, siempre es bueno solicitar
el token (si lo tiene) para saber quién nos está invocando, por ese motivo,
empezaremos desarrollando un Middleware que recupere el header
“authorization” que corresponde al token, lo descifre y lo agregue a nuestro
objeto request, con la intención que esté disponible para el resto de los servicios.

Para ello agregaremos el siguiente Middleware al inicio del archivo api.js:

1. var configuration = require('../config')


2. var jwt = require('jsonwebtoken')
3.
4. router.use('/',function(req, res, next) {
5. var token = req.headers['authorization']
6. if(!token){
7. next()
8. req.user = null
9. return
10. }
11.
12. token = token.replace('Bearer ', '')
13. jwt.verify(token, configuration.jwt.secret, function(err, user) {
14. if (err) {
15. req.user = null
16. next()
17. } else {
18. req.user = user
19. next()
20. }
21. })
22. })

El middleware escucha en cualquier path, y recupera el token del header (línea


5), si el token no está presenta, simplemente invoca next y finaliza con un return.

Si el token está presente, se procese con la validación, para ello, es necesario


retirar la palabra “Bearer “ (línea 12), la cual viene incluida por default, pero no
es necesaria para la validación. El siguiente paso es validar el token, mediante el
método verify que nos proporciona JWT, el cual requiere de una contraseña que
obtenemos del archivo de configuración (configuration.jwt.secret).

Si el proceso de validación termina correctamente, un objeto JSON es creado con


los datos asociados al token y es agregado al objeto request en la propiedad user
(línea 18).

Si el proceso de autenticación del token falla, entonces, simplemente igualamos


la propiedad user a null.

Página | 464
En este punto, será necesario instalar el módulo jsonwebtoken mediante el
siguiente comando:

npm install --save [email protected]

Adicional a eso, tendremos que agregar una contraseña en el archivo config.js,


dicha contraseña será utilizada para cifrar y descifrar los Tokens, por lo que por
ningún motivo deberá hacerse pública. La contraseña puede ser cualquier valor:

1. module.exports = {
2. debugMode: true,
3. server: {
4. port: 8080,
5. host: "http://api.localhost"
6. },
7. tweets: {
8. maxTweetSize: 140
9. },
10. mongodb: {
11. development: {
12. connectionString: "<Connection string>"
13. },
14. production: {
15. connectionString: "<Connection string>"
16. }
17. },
18. jwt: {
19. secret: "#$%EGt2eT##$EG%Y$Y&U&/IETRH45W$%whth$Y$%YGRT"
20. }
21. }

El siguiente paso será denegar el acceso a los servicios restringidos, por lo cual,
agregaremos el siguiente middleware al archivo api.js justo debajo del
middleware que acabamos de agregar:

1. router.use('/secure',function(req, res, next) {


2. if(req.user === null){
3. res.status(401).send({
4. ok: false,
5. message: 'Token inválido'
6. })
7. return
8. }
9. next()
10. })

Este nuevo middleware solo escucha las llamadas en el path /secure, lo que nos
permite realizar acciones solo para los servicios protegidos. Este middleware
valida si la propiedad req.user es null, si es null significa que no se presentó un
token o el que se presento es inválido. En tal caso, un código 401 es retornado
junto con la leyenda “Token inválido”. Por otra parte, si el token es correcto,
entonces, simplemente permitimos que continúe la ejecución con la llamada a
next.

En este momento, solo nos falta agregar la lógica para crear los Tokens, pero
esto lo veremos en el servicio de login.

465 | Página
Servicio - Login

El servicio login nos permite autenticarnos ante el API, para lo cual es necesario
mandar las credenciales user/password, y como respuesta, nos retornará el
Token generado.

Veamos la ficha del servicio:

Nombre Autenticación
URL /login
Método POST
Request
 name: nombre de la persona que crea la
cuenta.
 username: nombre de usuario.
Ejemplo:

6. {
7. "username": "juan",
8. "password": "1234"
9. }

Response
 Ok: valor booleano que indica si la operación
fue exitosa o no.
 Profile: contiene los datos del usuario creado.
 Token: Token generado para autenticarse en el
API.

20. {
21. "ok": true,
22. "body": {
23. "profile": {
24. "__v": 0,
25. "name": "Juan Perez",
26. "userName": "juan",
27. "password": "",
28. "_id": "5a1f40142aab1e2318d65e98",
29. "date": "2017-11-29T23:17:40.508Z",
30. "followersRef": [],
31. "followingRef": [],
32. "tweetCount": 0,
33. "banner": null,
34. "avatar": null,
35. "description": "Nuevo en Twitter"
36. }
37. },
38. "token": "<Token>"
39. }

Antes de desarrollar el servicio, vamos a crear una dependencia, la cual es una


función para crear los Tokens, para ello, vamos a crear el archivo AuthService.js
que crearemos dentro de una nueva carpeta /api/services/, el archivo se verá
de la siguiente manera:

Página | 466
1. var jwt = require('jsonwebtoken')
2. var configuration = require('../../config')
3.
4. function generateToken(user) {
5. var u = {
6. username: user.username,
7. id: user.id
8. }
9. return token = jwt.sign(u, configuration.jwt.secret, {
10. expiresIn: 60 * 60 * 24 // expires in 24 hours
11. })
12. }
13.
14. module.exports = {
15. generateToken
16. }

La función generateToken se utiliza para generar un token basado en un objeto


JSON, este obtento tendrá que tener el ID y username del usuario, con esto se
crea un objeto que representará el token (línea 5).

En la línea 9 creamos el token mediante el método sign de JWT. Esta función


requiere el objeto JSON y la contraseña con la que se firmará. Adicionalmente,
agregamos una fecha de expiración de 24h (línea 10). Finalmente exportamos la
función (línea 15).

Ya con esta dependencia resulta, vamos a agregar la función login al archivo


UserController.js:

1. function login(req, res, err){


2. Profile.findOne({userName: req.body.username.toLowerCase()})
3. .then(profile => {
4. if(profile==null){
5. res.send({
6. ok:false,
7. message: "Usuario y contraseña inválida"
8. })
9. return
10. }
11.
12. bcrypt.compare(req.body.password, profile.password, function(err,valid){
13. if (!valid) {
14. return res.send({
15. ok: false,
16. message: 'Usuario y password inválidos'
17. });
18. }
19.
20. let user = {
21. username: req.body.username,
22. id: profile._id
23. }
24.
25. let token = authService.generateToken(user)
26.
27. res.send({
28. ok:true,
29. profile: {
30. id: profile.id,
31. name: profile.name,
32. userName: profile.userName,

467 | Página
33. avatar: profile.avatar || '/public/resources/avatars/0.png',
34. banner: profile.banner || './public/resources/banners/4.png',
35. tweetCount: profile.tweetCount,
36. following: profile.following,
37. followers: profile.followers
38. },
39. token: token
40. })
41. });
42. }).catch(err => {
43. res.send({
44. ok: false,
45. message: "Error al validar el usuario"
46. })
47. })
48. }

Lo primero que hacemos es buscar al usuario en la base de datos mediante el


username (línea 2), Si el usuario no se encuentra, retornamos el error. Si el
usuario se encuentra, el siguiente paso será comparar el password enviado, con
el password almacenado en MongoDB. Dado que el password está encriptado, es
necesario realizar la comparación mediante el módulo bcrypt (línea 12).

Si el password concuerda, entonces estamos ante el usuario legítimo de la


cuenta, por lo que procederemos a crearle un token mediante la función
generateToken que acabamos de crear (línea 25). Finalmente retornamos el perfil
del usuario junto con el token generado (línea 41).

Cuando un usuario es creado, no tiene una foto de avatar, ni un banner, por lo


que regresamos uno por default, para lograr esto, hemos guardado dos imágenes
dentro de la carpeta public, con la finalidad de retornarlas por default. Las
imágenes son las siguiente:

Imagen por default para el Imagen por default para el banner, la cual
avatar, la cual deberemos deberá estar en la carpeta
guardar en el path /public/resources/banners/ con el nombre
/public/resources/avatars 4.png
/ con el nombre 0.png

Para comprobar que las hemos creado correctamente, deberemos poder ver las
imágenes en las siguientes URL, de lo contrario algo hemos hecho mal:

 http://localhost:8080/public/resources/avatars/0.png
 http://localhost:8080/public/resources/banners/4.png

Finalmente, No olvidemos importar la dependencia a AuthService.js y exportar


el nuevo servicio.

Página | 468
El último paso es agregar el router al archivo api.js, el cual se verá de la siguiente
manera:

1. //Public access services


2. router.get('/usernameValidate/:username', userController.usernameValidate)
3. router.post('/signup', userController.signup)
4. router.post('/login', userController.login)

En este momento ya estará listo nuestro servicio, pero no lo podremos ver


reflejado en la aplicación hasta crear el siguiente servicio (relogin), pues la
aplicación al momento de autenticarnos nos manda directo a la raíz de la
aplicación, donde el servicio relogin es ejecutado y al fallar porque no existe,
nos borra el token y nos manda de nuevo a login.

Servicio - Relogin

El servicio relogin es muy parecido al servicio de login, pues también sirve para
autenticar al usuario, sin embargo, tiene una pequeña diferencia, este servicio,
se utiliza para autenticar a los usuarios que ya tiene un token.

Mediante este servicio, es posible autenticar de forma automática a un cliente


que tiene un token válido, sin tener que capturar nuevamente su usuario y
contraseña. Mientras el token sea vigente, el usuario ya no requerirá introducir
su usuario y contraseña.

Veamos la ficha de este servicio:

Nombre Actualización de credenciales (relogin)


URL /secure/relogin
Método GET
Headers authorization: token del usuario
Request N/A
Response
 Ok: valor booleano que indica si la operación
fue exitosa o no.
 Profile: contiene los datos del usuario creado,
entre los que destacan
 Token: Token generado para autenticarse en el
API.

40. {
41. "ok": true,
42. "body": {
43. "profile": {
44. "__v": 0,
45. "name": "Juan Perez",
46. "userName": "juan",
47. "password": "",
48. "_id": "5a1f40142aab1e2318d65e98",

469 | Página
49. "date": "2017-11-29T23:17:40.508Z",
50. "followersRef": [],
51. "followingRef": [],
52. "tweetCount": 0,
53. "banner": null,
54. "avatar": null,
55. "description": "Nuevo en Twitter"
56. }
57. },
58. "token": "<Token>"
59. }

Una de las cosas que llama la atención de este servicio, es que se ejecuta por el
método GET y que no tiene request, esto se debe a que solo requiere del token
en el header “authorization” para validar al usuario. Como respuesta regresa lo
mismo del servicio login pero nos retorna un token actualizado con nueva fecha
de vencimiento.

Lo primero será crear la función relogin en el archivo UserController.js, el cual


se verá de la siguiente manera:

1. function relogin(req,res, err){


2. let userToken = {
3. id: req.user.id,
4. username: req.user.username
5. }
6. let newToken = authService.generateToken(userToken)
7.
8. Profile.findOne({_id: req.user.id})
9. .then(profile => {
10. if(profile === null){
11. res.send({
12. ok: false,
13. message: "No existe el usuario"
14. })
15. }else{
16. res.send({
17. ok:true,
18. profile: {
19. id: profile._id,
20. name: profile.name,
21. userName: profile.userName,
22. avatar: profile.avatar || '/public/resources/avatars/0.png',
23. banner: profile.banner || '/public/resources/banners/4.png',
24. tweetCount: profile.tweetCount,
25. following: profile.following,
26. followers: profile.followers
27. },
28. token: newToken
29. });
30. }
31. })
32. .catch(err => {
33. res.send({
34. ok: false,
35. message: "Error al consultar el usuario",
36. error: err
37. })
38. })
39. }

Página | 470
Dado que este servicio está expuesto en /secure/relogin, quiere decir que antes
de llegar a este servicio, deberá pasar por los middlewares de autenticación que
definimos en api.js, lo que nos garantiza que, si llega hasta aquí, es porque es
un usuario autenticado.

El siguiente paso es generarle un nuevo token con los datos del token viejo
(líneas 2 a 6). Seguido, buscamos al usuario por medio del ID y retornar los datos
del perfil junto con el nuevo token.

Restaría exportar la función al final del archivo para poder accederlo de forma
externa.

Finalmente, solo nos falta agregar el router en el archivo api.js, el cual quedaría
de la siguiente manera:

1. //Private access services (security)


2. router.get('/secure/relogin',userController.relogin)
3.
4. //Public access services
5. router.get('/usernameValidate/:username', userController.usernameValidate)
6. router.post('/signup', userController.signup)
7. router.post('/login', userController.login)

Ya con este servicio terminado, ya es posible probar la autenticación desde la


pantalla de login, sin embargo, una vez autenticado, no podremos ver más que
nuestros datos del perfil.

Fig. 201 - Probando los servicios login y relogin.

Servicio - Consultar los últimos Tweets

Este servicio es el que nos permite recuperar los últimos 10 tweets publicados
por todos los usuarios. Normalmente una red social utiliza IA para determinar los

471 | Página
tweets que deberás ver en tu home, sin embargo, nosotros no tenemos esas
capacidades, por lo que optamos por los 10 últimos tweets.

Veamos la ficha del servicio:

Nombre Consultar los últimos Tweets


URL /tweets
Método GET
Headers N/A
Request N/A
Response
 Ok: valor booleano que indica si la operación
fue exitosa o no.
 Body: contiene los datos del tweet, como lo
son:
o Id: ID único del tweet
o _creator: Perfil del usuario que creo el
Tweet.
o Date: fecha de creación
o Message: Texto capturado en el tweet.
o likeCounter: número de likes
o replys: número de likes
o image: Imagen asociada al Tweet (si
existe).

1. {
2. "ok": true,
3. "body": [
4. {
5. "_id": "5a0657ad3ccd98529d83a9b9",
6. "_creator": {
7. "_id": "5a05286db5371dffe40bafae",
8. "name": "Juan",
9. "userName": "Juan",
10. "avatar": "<Base64 img>"
11. },
12. "date": "2017-11-11T01:51:41.421Z",
13. "message": "test",
14. "likeCounter": 0,
15. "replys": 0,
16. "image": null
17. },
18. ...
19. ]
20. }

Como podemos observar, este servicio regresa un array dentro del body, donde
cada posición corresponde a un Tweet.

Página | 472
Todos los servicios relacionados con Tweets, los vamos a crear en otro controller,
por lo cual, crearemos el archivo TweetController.js en el path
/api/controllers, el cual se verá de la siguiente manera:

1. var Profile = require('../models/Profile')


2. var Tweet = require('../models/Tweet')
3. var mongoose = require('mongoose');
4.
5. function getNewTweets(req, res, err){
6. let user = req.user || {}
7.
8. Tweet.find({tweetParent : null})
9. .populate("_creator",{banner: 0})
10. .sort('-date')
11. .limit(10)
12. .exec(function(err, tweets){
13. if(err){
14. res.send({
15. ok: false,
16. message: "Error al cargar los Tweets",
17. error: err
18. })
19. return
20. }
21.
22. let response = tweets.map( x => {
23. return{
24. _id: x._id,
25. _creator: {
26. _id: x._creator._id,
27. name: x._creator.name,
28. userName: x._creator.userName,
29. avatar: x._creator.avatar || './public/resources/avatars/0.png'
30. },
31. date: x.date,
32. message: x.message,
33. liked: x.likeRef.find(
34. likeUser => likeUser.toString() === user.id || null),
35. likeCounter: x.likeCounter,
36. replys: x.replys,
37. image: x.image
38. }
39. })
40.
41. res.send({
42. ok: true,
43. body: response
44. })
45. })
46. }
47.
48. module.exports={
49. getNewTweets
50. }

La función getNewTweets es la que utilizaremos para recuperar los últimos tweets.


Esta función no requiere que estemos autenticados para recuperar los Tweets,
pero si utiliza al usuario autenticado para determinar si ya le disté like al Tweet,
por eso motivo, en la línea 6 recuperamos el usuario autenticado o definimos un
objeto en vacío en su lugar.

473 | Página
Lo siguiente es buscar todos los Tweet donde el campo tweetParent sea null
(línea 8), para asegurar de no recuperar Tweet que correspondan a respuestas.
Lo siguiente es hacer un populate (join) con los Perfiles (línea 9), con la intención
de remplazar el ID del usuario por el objeto en sí, con todos sus datos, además,
le mandamos banner:0 para indicarle que NO queremos el banner. Lo siguiente
es ordenar (línea 10) los Tweets de forma descendiente y solicitamos los primeros
10 tweet (línea 11). Terminamos ejecutando la consulta con la instrucción exec
(línea 12).

En la línea 22 iteramos todos los Tweets para construir el array que


retornaremos. Solo cabe detenernos la propiedad avatar y liked. En el campo
avatar retornamos la imagen en base64. Si la imagen no existirá, retornamos la
imagen de avatar por default (línea 29). Y finalmente, para el campo liked
buscamos dentro del array likeRef si algún ID corresponde con el usuario
autenticado, si esto es así, quiere decir que el usuario le dio like al tweet y
retornamos true. Finalmente, los tweets son retornados en la línea 41.

Para concluir este servicio, faltaría realizar dos acciones en el archivo api.js, la
primera es realizar el import a TweetController.js y agregar el routeo siguiente:

1. //Private access services (security)


2. router.get('/secure/relogin',userController.relogin)
3.
4. //Public access services
5. router.get('/tweets',tweetController.getNewTweets)
6. router.get('/usernameValidate/:username', userController.usernameValidate)
7. router.post('/signup', userController.signup)
8. router.post('/login', userController.login)

Con estos últimos cambios, ya podremos ver los tweets en nuestro proyecto:

Fig. 202 - probando el servicio de obtención de los últimos tweets.

Servicio - Consultar se usuarios sugeridos

Página | 474
El siguiente servicio tiene como propósito mostrar al usuario una lista de 3
usuarios sugeridos para que siga. Ahora bien, Las redes sociales actuales utilizan
algoritmos con Inteligencia Artificial para determinar que usuarios son buenos
prospectos para ser sugeridos, capacidad que desde luego no tenemos en este
mini proyecto, por lo que nos limitaremos a mostrar los últimos 3 usuario
registros en el proyecto.

Veamos la ficha del servicio:

Nombre Consulta de usuario sugeridos


URL /secure/suggestedUsers
Método GET
Headers authorization: token del usuario
Request N/A
Response
 Ok: valor booleano que indica si la operación
fue exitosa o no.
 Body: contiene un array de perfiles de
usuarios.

60. {
61. "ok": true,
62. "body": [
63. {
64. "_id": "5a204cdca0738b612c9c9f5f",
65. "name": "marco",
66. "description": "Nuevo en Twitter",
67. "userName": "marco",
68. "avatar": "<base64 img>",
69. "banner": "<base64 img>",
70. "tweetCount": 0,
71. "following": 0,
72. "followers": 0
73. },
74. ...
75. ]
76. }
77.

Agregaremos la función getSuggestedUser en el archivo UserController.js, el


cual se verá de la siguiente forma:

1. function getSuggestedUser(req, res, err){


2. let user = req.user
3.
4. Profile.find({userName: {$ne: user.username}})
5. .sort({"date": -1})
6. .limit(3)
7. .exec()
8. .then(users => {
9. let response = users.map(x => {
10. return {
11. _id: x._id,
12. name: x.name,
13. description: x.description,
14. userName: x.userName,

475 | Página
15. avatar: x.avatar || '/public/resources/avatars/0.png',
16. banner: x.banner || '/public/resources/banners/4.png',
17. tweetCount: x.tweetCount,
18. following: x.following,
19. followers: x.followers
20. }
21. })
22. res.send({
23. ok: true,
24. body: response
25. })
26. })
27. .catch(err => {
28. res.send({
29. ok: false,
30. message: err.message,
31. error: err
32. })
33. })
34. }

Este servicio es bastante simple, pues solo realiza la búsqueda de Perfiles (línea
4), los ordena por fecha de creación en forma descendente (línea 5) y luego
recupera los 3 primeros registros (línea 6).

Los resultados son iterados en la línea 9 para crear el array de respuesta.


Finalmente, los resultados son retornados en la línea 22.

Para finalizar, solo tendremos que agregar router correspondiente en el archivo


api.js:

1. //Private access services (security)


2. router.get('/secure/relogin',userController.relogin)
3. router.get('/secure/suggestedUsers',userController.getSuggestedUser)
4.
5. //Public access services
6. router.get('/tweets',tweetController.getNewTweets)
7. router.get('/usernameValidate/:username', userController.usernameValidate)
8. router.post('/signup', userController.signup)
9. router.post('/login', userController.login)

Tras agregar estos cambios, ya podremos ver los perfiles sugeridos en la página
de inicio:

Página | 476
Fig. 203 - Probando los usuarios sugeridos.

Servicio – Consulta de perfiles de usuario

Este servicio es el que se utiliza para consultar el perfil de un usuario. En la


aplicación Mini Twitter es utilizada para mostrar el perfil de otro usuario o incluso
el nuestro.

Veamos la ficha del servicio:

Nombre Consulta de perfiles de usuario


URL /profile/:username
URL username: nombre de usuario del perfil a consultar.
params
Método GET
Headers authorization: token del usuario
Request N/A

477 | Página
Response
 Ok: valor booleano que indica si la operación
fue exitosa o no.
 Body: contiene todos los datos del perfil
1. _id: identificador único del usuario.
2. Name: Nombre completo del usuario
3. Description: descripción acerca del
usuario.
4. userName: nombre de usuario.
5. Avatar: Foto de perfil
6. Banner: Imagen del banner
7. tweetCount: Conteo de tweet
publicados
8. Followings: número de personas que
sigue
9. Followers: número de seguidores.
10. Follow: indica si el consumir esta
siguiente al usuario.

78. {
79. "ok": true,
80. "body": {
81. "_id": "5a012b1486b5c864a4fe6223",
82. "name": "Oscar Blancarte",
83. "description": "Nuevo en Twitter",
84. "userName": "oscar",
85. "avatar": "<base64 img>",
86. "banner": "<base64 img>",
87. "tweetCount": 76,
88. "following": 1,
89. "followers": 2,
90. "follow": false
91. }
92. }
93.

Agregaremos la función getProfileByUsername en el archivo UserController.js


el cual se verá de la siguiente manera:

1. function getProfileByUsername(req, res, err){


2. let user = req.params.user
3. if(user === null){
4. res.send({
5. ok:false,
6. message: "parametro 'user' requerido"
7. })
8. }
9.
10. Profile.findOne({userName: user})
11. .then(user => {
12. if(user === null){

Página | 478
13. res.send({
14. ok: false,
15. message: "Usuario no existe"
16. })
17. return
18. }
19.
20. var token = req.headers['authorization'] || ''
21. token = token.replace('Bearer ', '')
22. jwt.verify(token, configuration.jwt.secret, function(err, userToken) {
23. let follow = false
24.
25. if (!err) {
26. follow = user.followersRef.find(
27. x => x.toString() === userToken.id.toString()) != null
28. }
29. res.send({
30. ok:true,
31. body: {
32. _id: user._id,
33. name: user.name,
34. description: user.description,
35. userName: user.userName,
36. avatar: user.avatar || '/public/resources/avatars/0.png',
37. banner: user.banner || '/public/resources/banners/4.png',
38. tweetCount: user.tweetCount,
39. following: user.following,
40. followers: user.followers,
41. follow: follow
42. }
43. })
44. })
45. }).catch(err => {
46. res.send({
47. ok: false,
48. message: err.message || "Error al obtener los datos del usuario",
49. error: err
50. })
51. })
52. }

Esta función requiere que se le envíe como URL param el nombre del usuario al
cual se va a consultar, de lo contrario, un error será retornado (línea 4).

El siguiente paso será, consultar el perfil mediante el username (línea 10), si el


perfil es encontrado, se procederá con enviar como respuesta el perfil encontrado
(línea 29), pero antes de eso, es necesario determinar si el usuario que consumió
el API está siguiendo al usuario consultado, por lo que recuperamos el token
(línea 20) y luego lo desciframos para recuperar el ID del usuario, si el token
existe y es válido (línea 22) se procede a determinar si el ID del usuario está en
el array followersRef (línea 26). Finalmente, respondemos con el perfil
consultado y en el campo follow (línea 41) envíanos el booleano que indica si
seguimos o no a este usuario.

Adicional a esto, tendremos que agregar el método getProfileByUsername a los


exports y agregar los siguientes imports al inicio del archivo:

1. var configuration = require('../../config')


2. var jwt = require('jsonwebtoken')

479 | Página
El último paso será agregar el router correspondiente al archivo api.js:

1. //Private access services (security)


2. router.get('/secure/relogin',userController.relogin)
3. router.get('/secure/suggestedUsers',userController.getSuggestedUser)
4.
5. //Public access services
6. router.get('/tweets',tweetController.getNewTweets)
7. router.get('/usernameValidate/:username', userController.usernameValidate)
8. router.get('/profile/:user',userController.getProfileByUsername)
9. router.post('/signup', userController.signup)
10. router.post('/login', userController.login)

Una vez que terminamos los cambios, es posible dirigirse a la sección del perfil
de nuestro usuario o el de cualquier otro:

Fig. 204 - Probando la consulta de perfil de usuario.

Servicio – Consulta de Tweets por usuario

El servicio de consulta de Tweets por usuario, permite consultar únicamente los


Tweets de un determinado usuario, el cual se utiliza en la sección del perfil de un
usuario.

Si nos dirigimos en este momento a la sección del perfil de usuario, verá que se
están mostrando los tweets de todos los usuarios, esto es posible debido a que
este servicio y el de los 10 últimos tweets (/tweets) son compatibles en la URL.
Pero una vez que implementemos este, Express podrá determinar que este es el
correcto para el path /tweets/:username.

Página | 480
Veamos la ficha del servicio:

Nombre Consulta de Tweets por usuario


URL /tweets/:username
URL username: nombre de usuario del perfil a consultar.
params
Método GET
Headers authorization: token del usuario
Request N/A
Response
 Ok: valor booleano que indica si la operación
fue exitosa o no.
 Body: contiene los datos del tweet, como lo
son:
o Id: ID único del tweet
o _creator: Perfil del usuario que creo el
Tweet.
o Date: fecha de creación
o Message: Texto capturado en el tweet.
o likeCounter: número de likes
o replys: número de likes
o image: Imagen asociada al Tweet (si
existe).

21. {
22. "ok": true,
23. "body": [
24. {
25. "_id": "5a0657ad3ccd98529d83a9b9",
26. "_creator": {
27. "_id": "5a05286db5371dffe40bafae",
28. "name": "Juan",
29. "userName": "Juan",
30. "avatar": "<Base64 img>"
31. },
32. "date": "2017-11-11T01:51:41.421Z",
33. "message": "test",
34. "likeCounter": 0,
35. "replys": 0,
36. "image": null
37. },
38. ...
39. ]

94. }

Crearemos la función getUserTweets dentro del archivo TweetController.js, el


cual se verá de la siguiente manera:

1. function getUserTweets(req, res, err){


2. let username = req.params.user

481 | Página
3.
4. Profile.findOne({userName: username}, function(err, user){
5. if(err){
6. res.send({
7. ok: false,
8. message: "Error al consultar los tweets",
9. error: err
10. })
11. return
12. }
13.
14. if(user == null){
15. res.send({
16. ok: false,
17. message: "No existe el usuarios"
18. })
19. return
20. }
21.
22. Tweet.find({_creator: user._id,tweetParent : null})
23. .populate("_creator")
24. .sort('-date')
25. .exec(function(err, tweets){
26. if(err){
27. res.send({
28. ok: false,
29. message: "Error al cargar los Tweets",
30. error: err
31. })
32. return
33. }
34.
35. let response = tweets.map( x => {
36. return{
37. _id: x._id,
38. _creator: {
39. _id: x._creator._id,
40. name: x._creator.name,
41. userName: x._creator.userName,
42. avatar: x._creator.avatar || '/public/resources/avatars/0.png'
43. },
44. date: x.date,
45. message: x.message,
46. liked: x.likeRef.find(
47. likeUser => likeUser.toString() === user.id || null),
48. likeCounter: x.likeCounter,
49. replys: x.replys,
50. image: x.image
51. }
52. })
53.
54. res.send({
55. ok: true,
56. body: response
57. })
58. })
59. })
60. }

Este servicio es prácticamente igual al de búsqueda de los últimos tweets, pero


tiene dos pequeñas diferencias. Debido a que la búsqueda es por usuario, es
necesario validar que exista el usuario solicitado (línea 4), si el usuario existe,
entonces podemos proceder con la búsqueda de los tweets, agregamos el filtro

Página | 482
por usuario (línea 22). El resto del servicio es exactamente igual al anterior.
Terminamos aquí exportando la función al final del archivo.

Por otra parte, es necesario agregar el router correspondiente al archivo api.js:

1. //Private access services (security)


2. router.get('/secure/relogin',userController.relogin)
3. router.get('/secure/suggestedUsers',userController.getSuggestedUser)
4.
5. //Public access services
6. router.get('/tweets/:user', tweetController.getUserTweets)
7. router.get('/tweets',tweetController.getNewTweets)
8. router.get('/usernameValidate/:username', userController.usernameValidate)
9. router.get('/profile/:user',userController.getProfileByUsername)
10. router.post('/signup', userController.signup)
11. router.post('/login', userController.login)

Si actualizamos la pantalla de perfil, podremos ver que solo salen Tweet del
usuario en cuestión.

Servicio – Actualización del perfil de usuario

Mediante este servicio es posible actualizar el nombre, la descripción, el avatar y


el banner de nuestro perfil. En el proyecto Mini Twitter es utilizado para en la
pantalla del perfil de usuario, al memento de guardar los cambios.

Veamos la ficha del servicio:

Nombre Consulta de Tweets por usuario


URL /tweets/:username
URL username: nombre de usuario del perfil a consultar.
params
Método PUT
Headers authorization: token del usuario
Request
 username: usuario a actualizar
 name: Nuevo nombre de usuario
 description: nueva descripción
 vatar: nueva foto de perfil
 banner: nueva imagen para el banner.

1. {
2. "username":"oscar",
3. "name":"Oscar Blancarte.",
4. "description":"User description",
5. "avatar":"<Base 64 Image>",
6. "banner":"<Base 64 Image>"
7. }

483 | Página
Response
 Ok: booleana que indica si la operación fue
exitosa o no.
 Body: contiene todos los datos actualizados del
perfil.

1. "ok": true,
2. "body": {
3. "_id": "5a012b1486b5c864a4fe6223",
4. "name": "Oscar Blancarte",
5. "description": "Nuevo en Twitter",
6. "userName": "oscar",
7. "avatar": "<base64 image>",
8. "banner": "<base64 image>",
9. "tweetCount": 76,
10. "following": 1,
11. "followers": 2,
12. "follow": false
13. }
14. }

Agregaremos la función updateProfile al archivo UserController.js, la cual se


verá de la siguiente manera:

1. function updateProfile(req, res, err){


2. let username = req.user.username
3. const updates = {
4. name: req.body.name,
5. description: req.body.description,
6. avatar: req.body.avatar,
7. banner: req.body.banner
8. }
9.
10. Profile.update({ userName: username }, updates)
11. .then(updates => {
12. res.send({
13. ok: true
14. })
15. })
16. .catch(err => {
17. res.send({
18. ok: false,
19. message: "Error al actualizar el perfil",
20. error: err
21. })
22. })
23. }

Este es un método bastante simple, pues solo se crea un objeto con los cambios
(línea 3) y luego se procese con la actualización del perfil mediante el método
update del schema Profile. Se define el nombre de usuario como filtro para solo
actualizar el registro correcto, solo que el nombre de usuario no lo tomamos del
request, si no del token. De esta forma nos aseguramos que no puedan actualizar
más que su propio perfil.

Finalmente agregamos el método a los exports y agregamos el router


correspondiente al archivo api.js:

Página | 484
1. //Private access services (security)
2. router.get('/secure/relogin',userController.relogin)
3. router.get('/secure/suggestedUsers',userController.getSuggestedUser)
4. router.put('/secure/profile', userController.updateProfile)
5.
6. //Public access services
7. router.get('/tweets/:user', tweetController.getUserTweets)
8. router.get('/tweets',tweetController.getNewTweets)
9. router.get('/usernameValidate/:username', userController.usernameValidate)
10. router.get('/profile/:user',userController.getProfileByUsername)
11. router.post('/signup', userController.signup)
12. router.post('/login', userController.login)

Para comprobar los resultados, solo restaría editar tu perfil de usuario, cambiar
tu nombre, descripción, avatar y banner, guardar los cambios y actualizar la vista
para asegurarnos de que los cambios se guardaron correctamente.

Servicio – Consulta de personas que sigo

Este servicio se utiliza para recuperar el perfil de todas las personas a las que
seguimos. En el proyecto mini Twitter se utiliza en la sección del perfil del usuario.

Veamos la ficha:

Nombre Consulta de personas que sigo


URL /followings/:user
URL user: nombre de usuario del cual se requieren las
params personas que sigue
Método GET
Headers N/A
Request N/A
Response
 Ok: valor booleano que indica si la operación
fue exitosa o no.
 Body: array con un listado de perfiles de
usuario

1. {
2. "ok":true,
3. "body":[
4. {
5. "_id":"5938bdd8a4df2379ccabc1aa",
6. "userName":"emmanuel",
7. "name":"Emmauel Lopez",
8. "description":"Nuevo en Twitter",
9. "avatar":"<Base 64 Image>",
10. "banner":"<Base 64 Image>"
11. },
12. ...
13. ]
14. }

485 | Página
Crearemos la función getFollowing dentro del archivo UserController.js el cual
se verá de la siguiente manera:

1. function getFollowing(req, res, err){


2. let username = req.params.user
3. Profile.findOne({userName : username})
4. .populate("followingRef")
5. .exec()
6. .then(followings => {
7. if(followings === null) throw { message: "No existe el usuario"}
8.
9. let response = followings.followingRef.map( x => {
10. return {
11. _id: x._id,
12. userName: x.userName,
13. name: x.name,
14. description: x.description,
15. avatar: x.avatar || '/public/resources/avatars/0.png',
16. banner: x.banner || '/public/resources/banners/4.png'
17. }
18. })
19.
20. res.send({
21. ok: true,
22. body: response
23. })
24. })
25. .catch(err => {
26. res.send({
27. ok:false,
28. message: err.message || "Error al consultara los seguidores",
29. error: err.error || err
30. })
31. })
32. }

Para obtener las personas que sigue un determinado usuario, es tan simple como,
consultar el perfil deseado y luego realizar un populate (join) mediante el campo
followingRef (línea 4), el cual es un array de ID de las personas que sigue. Ya
con eso, solo falta iterar los resultados para generar la respuesta (línea 9).

Finalmente agregamos el método al export y agregamos el router


correspondiente:

1. //Public access services


2. router.get('/tweets/:user', tweetController.getUserTweets)
3. router.get('/tweets',tweetController.getNewTweets)
4. router.get('/usernameValidate/:username', userController.usernameValidate)
5. router.get('/profile/:user',userController.getProfileByUsername)
6. router.post('/signup', userController.signup)
7. router.post('/login', userController.login)
8. router.get('/followings/:user',userController.getFollowing)

Para comprobar los resultados, solo basta con ir a la sección de “Siguiendo” del
perfil del usuario:

Página | 486
Fig. 205 - Probando la sección de "siguiendo".

Servicio – Consulta de seguidores

Este servicio se utiliza para recuperar el perfil de todas las personas que siguen
a un determinado usuario. En el proyecto mini Twitter se utiliza en la sección del
perfil del usuario.

Veamos la ficha:

Nombre Consulta de seguidores


URL /followers/:user
URL user: nombre de usuario del cual se requieren las
params personas que sigue
Método GET
Headers N/A
Request N/A
Response
 Ok: valor booleano que indica si la operación
fue exitosa o no.
 Body: array con un listado de perfiles de
usuario

15. {
16. "ok":true,
17. "body":[
18. {
19. "_id":"5938bdd8a4df2379ccabc1aa",
20. "userName":"emmanuel",
21. "name":"Emmauel Lopez",
22. "description":"Nuevo en Twitter",
23. "avatar":"<Base 64 Image>",
24. "banner":"<Base 64 Image>"
25. },

487 | Página
26. ...
27. ]
28. }

Crearemos la función getFollower dentro del archivo UserController.js el cual


se verá de la siguiente manera:

1. function getFollower(req, res, err){


2. let username = req.params.user
3. Profile.findOne({userName : username})
4. .populate("followersRef")
5. .exec()
6. .then(followers => {
7. if(followers === null) throw {message: "No existe el usuario"}
8. let response = followers.followersRef.map( x => {
9. return {
10. _id: x._id,
11. userName: x.userName,
12. name: x.name,
13. description: x.description,
14. avatar: x.avatar || '/public/resources/avatars/0.png',
15. banner: x.banner || '/public/resources/banners/4.png'
16. }
17. })
18. res.send({
19. ok: true,
20. body: response
21. })
22. })
23. .catch(err => {
24. res.send({
25. ok:false,
26. message: err.message || "Error al consultara los seguidores",
27. error: err.error || err
28. })
29. })
30. }

Para obtener las personas que siguen al usuario, es tan simple como, consultar
el perfil deseado y luego realizar un populate (join) mediante el campo
followersRef (línea 4), el cual es un array de ID de las personas que lo siguen.
Ya con eso, solo falta iterar los resultados para generar la respuesta (línea 9).

Finalmente agregamos el método al export y agregamos el router


correspondiente:

1. //Public access services


2. router.get('/tweets/:user', tweetController.getUserTweets)
3. router.get('/tweets',tweetController.getNewTweets)
4. router.get('/usernameValidate/:username', userController.usernameValidate)
5. router.get('/profile/:user',userController.getProfileByUsername)
6. router.post('/signup', userController.signup)
7. router.post('/login', userController.login)
8. router.get('/followings/:user',userController.getFollowing)
9. router.get('/followers/:user',userController.getFollower)

Página | 488
Para comprobar los resultados, solo basta con ir a la sección de “Seguidores” del
perfil del usuario:

Fig. 206 - Probando la sección de "Seguidores".

Servicio – Seguir

Este servicio se utiliza para seguir o dejar de seguir a un usuario. Es utilizado


desde la pantalla del perfil del usuario.

Este servicio es un poco más complicado que el resto, pues implica transaccionar
el documento de los dos perfiles involucrados. A un documento hay que agregarle
un seguidor (followersRef) y al otro hay que agregarle que lo seguimos
(followingsRef). Pero si lo dejamos de seguir, hay que hacer exactamente lo
contrario.

Veamos la fecha del servicio:

Nombre Seguir
URL /secure/follow
URL N/A
params
Método GET
Headers authorization: token del usuario
Request
 followingUser: nombre de usuario del perfil
que deseamos seguir/dejar de seguir

1. {
2. "followingUser":"jperez"
3. }

489 | Página
Response
 Ok: booleana que indica si la operación fue
exitosa o no.
 Unfollow: booleano que indica si seguimos o
dejamos de seguir, false indica que lo seguimos
y true que lo dejamos de seguir

1. {
2. "ok": true,
3. "unfollow": false
4. }

Agregaremos la función follow al archivo UserController.js, el cual se verá de


la siguiente manera:

1. function follow(req, res, err){


2. let username = req.user.username
3. let followingUser = req.body.followingUser
4.
5. Profile.find({userName: {$in:[username,followingUser]}})
6. .then(users => {
7. if(users.length != 2) throw {message: "El usuario no existe"}
8. let my = users.find( x => x.userName == username)
9. let other = users.find( x => x.userName == followingUser)
10. let following = my.followingRef.find(
11. x => x.toString() === other._id.toString()) != null
12. let myUpdate = null
13. let otherUpdate = null
14. if(following){
15. myUpdate = {$pull: {followingRef: other._id}}
16. otherUpdate = {$pull: {followersRef: my._id}}
17. }else{
18. myUpdate = {$push: {followingRef: other._id}}
19. otherUpdate = {$push: {followersRef: my._id}}
20. }
21.
22. Profile.update({userName: my.userName}, myUpdate)
23. .then(myUp => {
24. Profile.update({userName: other.userName}, otherUpdate)
25. .then(otherUp => {
26. res.send({
27. ok: true,
28. unfollow: following,
29. err: err.error || err
30. })
31. })
32. .catch( err => {
33. res.send({
34. ok: false,
35. message: err.message || "Error al ejecutar la operación",
36. err: err.error || err
37. })
38. })
39. })
40. .catch(err => {
41. res.send({
42. ok: false,
43. message: err.message || "Error al ejecutar la operación",
44. err: err.error || err
45. })
46. })

Página | 490
47. })
48. .catch(err => {
49. res.send({
50. ok: false,
51. message: err.message || "Error al ejecutar la operación",
52. err: err.error || err
53. })
54. })
55. }

Lo primero que haremos será identificar el usuario que solicita la acción de seguir
y el usuario que vamos a seguir, para ello, guardamos en la variable username
(línea 2) el usuario que solicita la acción, el cual recuperamos del token. Por otra
parte, guardamos el usuario al que vamos a seguir en la variable followingUser
(línea 3).

Lo siguiente es consultar los dos perfiles (el que solicita y al que seguiremos)
(línea 5), y utilizamos el operador $in, para consultar los dos perfiles. Si el
resultado regresa menos de dos documentos, quiere decir que uno de los dos
perfiles no existe y regresamos un error.

Una vez que tenemos los dos perfiles, tenemos que identificar en qué posición
del arreglo se encuentra el solicitante y al que seguiremos, para ello, guardamos
el index de su posición en las variables my y other (líneas 8 y 9).

En la línea 10 hacemos una búsqueda en los ID de seguidores para determinar


si ya estamos siguiendo al usuario. Ya que, dependiendo de eso, serán las
operaciones de update que realizaremos.

En las líneas 14 a 20 prepararemos las instrucciones de actualización. Si no


estamos siguiendo al usuario, utilizaremos operaciones push para agregar al
arreglo la referencia al usuario que seguimos y del otro perfil, agregaremos como
seguidor (líneas 18 a 19). Por otra parte, si ya lo estamos siguiente, tendremos
que eliminar las referencia mediante el operador pull (líneas 15 y 16).

Los siguientes pasos son más fáciles, pues solo tendremos que actualizar los dos
perfiles mediante los comandos ya preparados, las actualizaciones las
realizaremos en las líneas 22 y 24.

Finalmente, solo quedaría exportar la nueva función y agregar el router


correspondiente en el archivo api.js:

1. //Private access services (security)


2. router.get('/secure/relogin',userController.relogin)
3. router.get('/secure/suggestedUsers',userController.getSuggestedUser)
4. router.put('/secure/profile', userController.updateProfile)
5. router.post('/secure/follow', userController.follow)

Para probar los cambios, solo tenemos que presionar el botón se “seguir” o
“siguiendo" del perfil de cualquier usuario.

Servicio – Crear un nuevo Tweet

491 | Página
Este servicio nos permitirá crear un nuevo Tweet o crear una respuesta a un
Tweet existente. En el proyecto Mini Twitter es utilizado por el componente
Reply.js, el cual se utiliza desde la página principal o como parte del detalle de
un Tweet para realizar una respuesta.

Veamos la ficha del servicio:

Nombre Crear un nuevo Tweet


URL /secure/tweets
URL N/A
params
Método POST
Headers authorization: token del usuario
Request
 message: Texto asociado al Tweet.
 image : Imagen asociada al Tweet.

1. {
2. "message": "¡hola mundo! este es mi primer Tweet",
3. "image": "<base64 img>"
4. }

Response
 Ok: valor booleano que indica si la operación fue
exitosa o no.
 tweet: Objecto con todos los datos del Tweet,
entre los que están:
o _id: ID asociado al Tweet.
o date: fecha de creación
o message: mensaje asociado al tweet
o Image: Imagen asociada al tweet.

1. {
2. "ok": true,
3. "tweet": {
4. "__v": 0,
5. "_creator": "593616dc3f66bd6ac4596328",
6. "message": "hola mundo",
7. "image": "<base64 img>",
8. "_id": "59f66ea3ceb9f6a00c7b3143",
9. "replys": 0,
10. "likeCounter": 0,
11. "date": "2017-10-30T00:13:23.293Z"
12. }
13. }

Lo primero que haremos será crear la función addTweet dentro del archivo
TweetController.js:

1. function addTweet(req, res, err){


2. if(req.body.tweetParent){ // Reply Tweet
3. createReplyTweet(req, res, err)
4. }else{ // New Tweet
5. createNewTweet(req, res, err)

Página | 492
6. }
7. }

Dado que la lógica para crear un Tweet y una respuesta es distinta, hemos
separado la funcionalidad en dos funciones. La función createNewTweet (línea 5)
la utilizaremos para crear un nuevo Tweet, mientras que la función
createReplyTweet (línea 3) es para crear las respuestas.

La función createNewTweet quedará de la siguiente manera:

1. function createNewTweet(req, res, err){


2. let user = req.user
3. const newTweet = new Tweet({
4. _creator: user.id,
5. tweetParent: req.body.tweetParent,
6. message: req.body.message,
7. image: req.body.image
8. })
9.
10. Profile.update({_id: user.id},{$inc: {tweetCount: 1}})
11. .then(update => {
12. if((!update.ok) || update.nModified == 0 )
13. throw {message: "No existe el usuario"}
14. newTweet.save()
15. .then(saveTweet => {
16. res.send({
17. ok: true,
18. tweet: saveTweet
19. })
20. })
21. .catch(err => {
22. res.send({
23. ok: false,
24. message: err.message || "Error al guardar el Tweet",
25. error: err.error || err
26. })
27. })
28. })
29. .catch(err => {
30. res.send({
31. ok: false,
32. message: err.message || "Error al guardar guardar el Tweet",
33. error: err.error || err
34. })
35. })
36. }

Crear un nuevo Tweet se realiza en tres partes. La primera es crear el Objecto


del Tweet mediante el schema Tweet (línea 3). El segundo paso es incrementar
el contador de Tweet del usuario (línea 10), para lo cual utilizamos el operador
$in, pues permite una actualización segura, ya que, sin importar el valor actual,
solamente le incrementará en 1. Si la actualización termino correctamente,
entonces podemos proceder con guardar el Tweet (línea 14).

Si el último paso termino correctamente, entonces simplemente retornamos el


tweet creado.

Por otra parte, tenemos la función createReplyTweet la cual se verá de la


siguiente manera:

493 | Página
1. function createReplyTweet(req, res, err){
2. let user = req.user
3. const newTweet = new Tweet({
4. _creator: user.id,
5. tweetParent: req.body.tweetParent,
6. message: req.body.message,
7. image: req.body.image
8. })
9.
10. Tweet.update({_id: req.body.tweetParent},{$inc:{replys:1}})
11. .then(update => {
12. if((!update.ok) || update.nModified == 0 )
13. throw {message: "No existe el Tweet padre"}
14. newTweet.save()
15. .then(saveTweet => {
16. res.send({
17. ok: true,
18. tweet: saveTweet
19. })
20. })
21. .catch(err => {
22. res.send({
23. ok: false,
24. message: "Error al guardar el Tweet",
25. error: err
26. })
27. })
28. })
29. .catch(err => {
30. res.send({
31. ok: false,
32. message: "Error al actualizar al usuario",
33. error: err
34. })
35. })
36. }

Para crear una respuesta, la lógica cambia un poco, lo primero será crear el
objeto del Tweet correspondiente la de respuesta (línea 3). El segundo paso será
actualizar el Tweet padre, al cual hay que incrementarle en 1 el contador de
respuestas (línea 10). Si todo sale bien y hemos actualizado al menos un registro
(línea 12), entonces procedemos con guardar el Tweet de respuesta mediante el
método save (línea 14). Finalmente retornamos el nuevo Tweet creado en la línea
16.

Ahora solo nos restaría agregar únicamente la función addTweet a los exports y
agregar el router correspondiente en el archivo api.js:

1. //Private access services (security)


2. router.get('/secure/relogin',userController.relogin)
3. router.get('/secure/suggestedUsers',userController.getSuggestedUser)
4. router.put('/secure/profile', userController.updateProfile)
5. router.post('/secure/follow', userController.follow)
6. router.post('/secure/tweet', tweetController.addTweet)

Para probar los cambios, solo basta con crear un nuevo tweet desde la pantalla
principal, un que las respuestas será difícil probar, pues de momento no
podremos ver el detalle del Tweet para comprobar el resultado:

Página | 494
Fig. 207 –Probando la creación de un nuevo Tweet.

Servicio – Like

El servicio like nos permite indicar que un Tweet es de nuestro agrado, con lo
cual, el Tweet va incrementando un contador de likes. Aunque también le
podemos indicar que algo ya no nos gusta. Este servicio se utiliza desde el
componente Tweet.js cuando presionamos el botón del corazón.

Veamos la ficha del servicio:

Nombre Like
URL /secure/like
URL N/A
params
Método POST
Headers authorization: token del usuario
Request
 tweetID: ID del Tweet al que deseamos dar like
 like : Valor booleano que indica si queremos darle
like (true) o dislike (false).

1. {
2. "tweetID": "59ed5728022307a950b3c756",
3. "like": true
4. }

495 | Página
Response
 Ok: booleano que indica si la operación fue exitosa
o no.
 body: Objeto que contiene los datos actualizados
del Tweet.

1. {
2. "ok": true,
3. "body": {
4. "_id": "59ed5728022307a950b3c756",
5. "_creator": "593616dc3f66bd6ac4596328",
6. "message": "Mi libro de \"Patrones de diseño\"",
7. "image": "<Base 64 Image>",
8. "__v": 0,
9. "replys": 4,
10. "likeCounter": 1,
11. "date": "2017-10-23T02:42:48.679Z"
12. }
13. }

Iniciaremos creado la función like dentro del archivo TweetController.js, el cual


se vera de la siguiente manera:

1. function like(req, res, err){


2. let user = req.user
3.
4. let updateStatement = null
5. if(req.body.like){
6. updateStatement = {$push: {likeRef: mongoose.Types.ObjectId(user.id)} }
7. }else{
8. updateStatement = {$pull: {likeRef:mongoose.Types.ObjectId(user.id)} }
9. }
10.
11. Tweet.findByIdAndUpdate(req.body.tweetID, updateStatement)
12. .then(tweet => {
13. if(tweet == null) throw {message: "No existe el Tweet solicitado"}
14. res.send({
15. ok: true,
16. body: {
17. _creator: tweet._creator,
18. tweetParent: tweet.tweetParent,
19. date: tweet.date,
20. message: tweet.message,
21. likeRef: tweet.likeRef,
22. image: tweet.image,
23. replys: tweet.replys,
24. likeCounter: tweet.likeCounter += req.body.like ? 1 : -1
25. }
26. })
27. })
28. .catch(err => {
29. res.send({
30. ok: false,
31. message: err.message || "Error al actualizar el Tweet",
32. error: err.error || err
33. })
34. })
35. }

Página | 496
Darle like a un Tweet es una tarea muy simple, pues solo es necesario agregar
el ID del usuario dentro del array likeRef del objeto Tweet. Por otra parte, si lo
que buscamos es quitar el like, solo tenemos que eliminar el ID del array.

El primer paso es determina la operación que vamos a realizar, es decir, si es un


like o un dislike y crear el objeto con las instrucciones para la actualización. Esto
lo hacemos en las líneas 5 a 9. Si el request tiene la propiedad like en true, indica
un like, por lo que hacemos un push al array likeRef (línea 6). Por otro lado, si es
false, realizaremos un pull para eliminar el ID del array likeRef (línea 8).

El siguiente paso es realizar la actualización mediante el método


findByIdAndUpdate (línea 11) y retornar el tweet actualizado (línea 14). Solo hay
algo importante a resaltar. El método findByIdAndUpdate primero buscar el
documento y luego lo actualiza, por lo que el virtual likeCounter no estar
actualizado con el nuevo incremento, por lo que sumamos o restamos en 1 según
la operación (línea 24).

Finalmente, solo restaría agregar esta nueva función a los exports y agregar el
router correspondiente al archivo api.js:

1. //Private access services (security)


2. router.get('/secure/relogin',userController.relogin)
3. router.get('/secure/suggestedUsers',userController.getSuggestedUser)
4. router.put('/secure/profile', userController.updateProfile)
5. router.post('/secure/follow', userController.follow)
6. router.post('/secure/tweet', tweetController.addTweet)
7. router.post('/secure/like', tweetController.like)

Para probar los cambios solo presionemos el botón del corazón en cualquier
Tweet:

Fig. 208 - Probando el servicio de like.

Servicio – Consultar el detalle de un Tweet

El último servicio que nos resta para terminar el API es de la consulta del detalle
de un Tweet, el cual nos permite recuperar todas las respuestas asociadas a un
Tweet.

Este servicio es utilizado al momento de dar click sobre cualquier Tweet, donde
de forma modal, podemos ver todo el Tweet con su detalle.

497 | Página
Veamos la ficha del servicio:

Nombre Like
URL /tweetDetails/:tweetID
URL tweetID: ID del Tweet a consular el detalle.
params
Método GET
Headers N/A
Request N/A
Response
 Ok: booleano que indica si la operación fue exitosa
o no.
 body: Objeto que contiene un Tweet con todo su
detalle
o _id: identificador único del Tweet.
o _creator: Perfil del usuario que creo el
Tweet.
o _date: fecha de creación del Tweet.
o Message: Texto del Tweet.
o Liked: indica si le dimos like al Tweet.
o likeCounter: contador de likes
o image: Imagen asociada al Tweet.
o Replys: números de respuestas
o reploysTweets: Arreglo de Tweets
correspondientes a las respuestas

1. {
2. "ok": true,
3. "body": {
4. "_id": "59ed5728022307a950b3c756",
5. "_creator": {
6. "_id": "593616dc3f66bd6ac4596328",
7. "name": "Oscar Blancarte.",
8. "userName": "oscar",
9. "avatar": "<Base 64 Image>"
10. },
11. "date": "2017-10-23T02:42:48.679Z",
12. "message": "Mi libro de \"Patrones de diseño\"",
13. "liked": false,
14. "likeCounter": 0,
15. "image": "<Base 64 Image>",
16. "replys": 1,
17. "replysTweets": [
18. {
19. "_id": "59f51e49830f6ac1c4c841a2",
20. "_creator": {
21. "_id": "593616dc3f66bd6ac4596328",
22. "name": "Oscar Blancarte.",
23. "userName": "oscar",
24. "avatar": "<Base 64 Image>"
25. },
26. "date": "2017-10-29T00:18:17.071Z",
27. "message": "dgnfdgh",
28. "liked": false,
29. "likeCounter": 0,

Página | 498
30. "replys": 0,
31. "image": null
32. },
33. ...
34. ]
35. }
36. }

Agregaremos la función getTweetDetails dentro del archivo TweetController.js,


el cual deberá verse de la siguiente manera:

1. function getTweetDetails(req, res, err){


2. let user = req.user || {}
3. let tweetId = req.params.tweet
4. if(!mongoose.Types.ObjectId.isValid(tweetId)){
5. res.send({
6. ok: false,
7. message: "ID del Tweet Inválido"
8. })
9. return
10. }
11.
12. Tweet.findOne({_id: tweetId})
13. .populate("_creator")
14. .exec()
15. .then(tweet => {
16. if(tweet == null)throw {message: "No existe el Tweet"}
17.
18. Tweet.find({tweetParent: mongoose.Types.ObjectId(tweetId)})
19. .populate("_creator").sort('-date').exec()
20. .then(tweets => {
21. let replys = []
22. if(tweets != null && tweets.length > 0){
23. replys = tweets.map(x => {
24. return {
25. _id: x._id,
26. _creator: {
27. _id: x._creator._id,
28. name: x._creator.name,
29. userName:x._creator.userName,
30. avatar: x._creator.avatar || '/public/resources/avatars/0.png'
31. },
32. date: x.date,
33. message: x.message,
34. liked: x.likeRef.find(
35. likeUser => likeUser.toString() === user.id || null),
36. likeCounter: x.likeCounter,
37. replys: x.replys,
38. image: x.image,
39.
40. }
41. })
42. }
43. res.send({
44. ok: true,
45. body: {
46. _id: tweet._id,
47. _creator: {
48. _id: tweet._creator._id,
49. name: tweet._creator.name,
50. userName:tweet._creator.userName,
51. avatar: tweet._creator.avatar || '/public/resources/avatars/0.png'

499 | Página
52. },
53. date: tweet.date,
54. message: tweet.message,
55. liked: tweet.likeRef.find(
56. likeUser => likeUser.toString() === user.id || null),
57. likeCounter: tweet.likeCounter,
58. image: tweet.image,
59. replys: tweet.replys,
60. replysTweets: replys
61. }
62. })
63. }).catch(err => {
64. res.send({
65. ok:false,
66. message: err.message || "Error al consultar el detalle",
67. error: err.error || err
68. })
69. })
70. }).catch(err => {
71. res.send({
72. ok:false,
73. message: err.message || "Error al cargar el Tweet",
74. error: err.error || err
75. })
76. })
77. }

La consulta del detalle del Tweet puede aparentar complicada, pero en realidad
es muy simples y solo se requiere de dos pasos para obtener la información
necesaria. El primero es consular al Tweet del cual se requiere el detalle, para
eso, recuperamos el ID desde los URL params (línea 3) y luego realizar la
búsqueda del Tweet por medio del ID (línea 12), aprovechamos para realizar un
populate (join) con el perfil del usuario (línea 13).

Ya con el Tweet de la búsqueda anterior, podemos realizar una nueva búsqueda


en los Tweet para traernos todos los Tweet donde si padre sea el Tweet anterior
(línea 18) y realizamos nuevamente un populate sobre el creador de las
respuestas. Finalmente, solo creamos la respuesta con el Tweet padre y los Tweet
hijos (línea 43).

En este punto, solo tenemos que exportar la nueva función y agregar el router
correspondiente:

1. //Public access services


2. router.get('/tweets/:user', tweetController.getUserTweets)
3. router.get('/tweets',tweetController.getNewTweets)
4. router.get('/usernameValidate/:username', userController.usernameValidate)
5. router.get('/profile/:user',userController.getProfileByUsername)
6. router.post('/signup', userController.signup)
7. router.post('/login', userController.login)
8. router.get('/followings/:user',userController.getFollowing)
9. router.get('/followers/:user',userController.getFollower)
10. router.get('/tweetDetails/:tweet', tweetController.getTweetDetails )

Para probar los cambios, solo tendremos que dar click sobre cualquier Tweet y
un popup debería emerger con todo el detalle del Tweet:

Página | 500
Fig. 209 - Probando el detalle del Tweet.

Documentando el API REST

Documentar un API correctamente es clave para su usabilidad, pues permite a


terceros entender cómo se utiliza, que servicios están disponibles, como
ejecutarlos y que información se espera como entrada y salida. Por desgracia,
gran parte de las API’s que se desarrollan, no tiene una documentación
aceptable.

Por esa razón, vamos aprender a crear una documentación simple para nuestra
API, la cual podrás utilizar más adelante para documentar cualquier otra.

Introducción al motor de plantillas Pug

Para publicar la documentación por la web, utilizaremos el motor de plantillas


Pug, el cual permite generar documentos HTML de una forma más simple y
menos verbosa. Pug que anteriormente se llamaba Jade, está basado en Haml,
aun que mejora la sintaxis de una forma sorprendente. Te invito a que entres a

501 | Página
la página oficial de Pug, en donde podrás encontrar toda la información y
documentación actualizada.

Esta sección busca explicar los conceptos más básicos de Pug, ya que lo
utilizaremos para desarrollar la documentación del API, sin embargo, si se quiere
profundizar en el aprendizaje de este motor de plantillas, es recomendable
dirigirse a la documentación oficial o buscar una lectura especifica del tema.

Sintaxis básica de Pug

Básicamente, Pug nos permite crear una plantilla escrita en su propio lenguaje y
este compila la plantilla para entregarnos un HTML puro y compatible con el
navegador.

Veamos el siguiente documento HTML:

1. <!DOCTYPE html>
2. <html lang="es">
3. <head>
4. <title>Pug</title>
5. <script type="text/javascript">
6. foo = true;
7. bar = function () {};
8. if (foo) {
9. bar(1 + 5)
10. }
11. </script>
12. </head>
13. <body>
14. <h1>Pug - node template engine</h1>
15. <div id="container" class="col">
16. <p>You are amazing</p>
17. <p>Jade is a terse and simple.</p>
18. </div>
19. </body>
20. </html>

Este mismo documento que acabamos de ver se puede simplificar con Pug, de
tal forma que el siguiente documento da como resultado el mismo documento
HTML que acabo de ver:

1. doctype html
2. html(lang='es')
3. head
4. title Pug
5. script(type='text/javascript').
6. foo = true;
7. bar = function () {};
8. if (foo) {
9. bar(1 + 5)
10. }
11. body
12. h1 Pug - node template engine
13. #container.col
14. p You are amazing
15. p Jade is a terse and simple.

Página | 502
Solo a simple vista, podemos ver una reducción considerable de líneas (20 vs
15), es decir, nos hemos ahorrado una cuarta parte y este rango incrementa con
documentos más grandes.

La otra gran diferencia que podemos apreciar, es que no tenemos etiquetas de


apertura y cierre, es su lugar solo definimos el nombre de la etiqueta inicial, pero
sin los símbolos <>.

Pug utiliza los “tabs” o espacios para identificar que elemento va dentro de otro,
ya que no cuenta con una etiqueta de apertura y cierre, es por ello, que es
sumamente importante respetar los tabuladores. Por ejemplo, Pug sabe que la
etiqueta head y body van dentro de html, debido a que estas dos tiene un tab más
que html. De la misma forma, Pug sabe que title va dentro de head por que
title tiene un tab más que titile.

Clases de estilo

Otra de las ventajas que ofrece Pug, es la forma en que no permite definir las
clases de estilo, pues solo tenemos que agregar un punto (.) antes de cada clase
de estilo. Veamos el siguiente ejemplo:

1. div.myclass.myclass2

Este ejemplo es equivalente a:

1. <div class="myclass myclass2">


2. </div>

Solo es necesario definir el nombre de la etiqueta cuando requerimos una en


especial, de lo contrario, podemos hacerlo de la siguiente manera:

1. .myclass.myclass2

Cuando un elemento empieza con punto (.). Pug asume que es un div, por lo que
el resultado anterior es el mismo que si iniciáramos con div.

Establecer un ID a un elemento

También podemos agregarle un ID a nuestros elementos:

1. .myclass.myclass2#myID

Solo es necesario poner el símbolo # y luego el ID que le queremos poner al


elemento. El resultado es el siguiente:

1. <div id="myID" class="myclass myclass2">


2. </div>

503 | Página
Por otra parte, si lo que buscamos es agregarle texto a un elemento, solo
tenemos que agregar un espacio en blanco y agregar el texto:

1. p.myclass.myclass2#myID Hello World!

El resultado:

1. <p id="myID" class="myclass myclass2">Hello World!</p>

Definir atributos a un elemento

Pug permite definir los atributos de varias formas, pero nos centraremos en las
dos principales formas. La primera y más utilizada es definir todas las
propiedades en línea, en donde cada atributo es colocado uno enseguida del otro,
pero separados con una coma y todos dentro de un par de paréntesis.

1. link(rel='stylesheet', href='/styles.css')

La otra forma, es definir un atributo por línea:

1. link(
2. rel='stylesheet'
3. href='/styles.css'
4. )

El resultado en los dos casos es el mismo:

1. <link rel="stylesheet" href="/styles.css">

Tipo de código

Pug no solo permite agregar fácilmente etiquetas HTML, si no que permite hacer
paginas dinámicas mediante la inclusión de fragmentos de código Javascript, con
los cuales es posible agregar variables, ciclos, condiciones, etc.

Existen 3 formas de agregar Javascript, las cuales son: Unbuffered, Buffered, y


Unescaped Buffered, las cuales analizaremos a continuación.

Unbuffered Code

Son fragmentos de código que no se escriben directamente en la salida, en


su lugar, son procesados como bloque JavaScript. Estos bloques inician con un
guion alto (-). Son utilizados para agregar estructuras de control o definir
variables. Ejemplo:

Página | 504
Pug HTML Output
1. - for (var x = 0; x < 3; x++) 1. <li>item</li>
2. li item 2. <li>item</li>
3. <li>item</li>

Como resultado tenemos un ciclo for que se ejecuta en 3 ocasiones, y en cada


iteración genera un elemento <li> con el texto ítem.

Buffered code

Son fragmentos de código que son evaluadas y el resultado es enviado al


documento HTML de salida. Estos fragmentos inician con el símbolo igual (=).

Pug HTML Output


1. p = 'Hello ' + 'world' 1. <p>Hello world</p>

Unescaped Buffered

Funciona exactamente igual que el anterior, con la diferencia de que el resultado


de la evaluación puede contener elementos HTML. El bloque inicia con el
símbolo (!=).

Pug HTML Output


p!= 'This code is' + ' <strong>not</stron <p>This code is <strong>not</strong> es
g> escaped!' caped!</p>

Iteraciones

Pug soporta dos tipos de iteraciones, each y while, las cuales funcionan
exactamente igual que en cualquier lenguaje de programación.

Each

El iterador each no requiere definirse dentro de un bloque unbuffered.

Pug HTML Output


1. ul 4. <ul>
2. each val in [1, 2, 3] 5. <li>1</li>
3. li= val 6. <li>2</li>
7. <li>3</li>
8. </ul>

505 | Página
Como resultado tenemos la iteración del arreglo definido en el mismo ciclo, el
cual consta te de 3 elementos. El valor de cada iteración se guarda en la variable
val, que luego es mostrada en pantalla utilizando un bloque buffered (=).

While

El ciclo while también funciona como en cualquier otro lenguaje de programación

Pug HTML Output


1. - var n = 0; 5. <ul>
2. ul 6. <li>0</li>
3. while n < 4 7. <li>1</li>
4. li= n++ 8. <li>2</li>
9. <li>3</li>
10. </ul>

Para generar el contador, hemos definido una variable en un bloque unbuffered,


luego realizamos el while con 4 iteraciones, mostrando el valor de la variable.

API home

El API Home o página de bienviva, debe de ser una página sencilla alojada en el
home de la URL del API, en este caso sería api.localhost:8080. En esta página se
aconsejable brindar un mensaje al usuario para que sepa que está en el API.
También se aconseja mostrar los términos de uso y una liga a la documentación
de los servicios disponibles.

Esta página ya la hemos creado en esta misma sección, pero no habíamos


analizado cómo funcionaba, la cual se ve de la siguiente manera:

Fig. 210 - API Home

Página | 506
Si regresamos al archivo api.js, podremos ver que hemos definido un router
para escuchar en la raíz del subdominio:

1. router.get('/', function(req, res) {


2. res.send(pug.renderFile(__dirname + '/../public/apidoc/api-index.pug'))
3. })

Pug nos proporciona el método renderFile, el cual sirve para compilar la plantilla
y darnos un documento HTML como respuesta. Puede recibir básicamente dos
parámetros, uno de la URL a la plantilla y el segundo es un objeto que sirve como
parámetros para la plantilla, aunque en este caso, solo utilizamos un parámetro.

Dicho esto, podríamos resumir que cuando el home (/) del api sea ejecutado,
Pug abrirá la plantilla api-index.pug y nos retornará el HTML de la página. Ahora
bien, seguramente te estarás preguntando como desarrollamos la plantilla.

1. doctype html
2. html
3. head
4. link(href='https://fonts.googleapis.com/css?family=Roboto', rel='stylesheet')
5. link(
6. rel='stylesheet'
7. href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css'
8. integrity='sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u'
9. crossorigin='anonymous'
10. )
11. link(rel='stylesheet', href='/public/apidoc/api-styles.css')
12. body
13. .api-alert
14. p.title Mini Twitter API REST
15. p.body
16. | Esta API es provista como parte de la aplicación Mini Twitter,
17. | la cual es parte del libro
18. a(href='#')
19. strong "Desarrollo de aplicaciones reactivas con React, NodeJS y MongoDB"
20. | , por lo que su uso es únicamente con fines educativos y por
21. | ningún motivo deberá ser empleada para aplicaciones productivas.
22. .footer
23. button.btn.btn-warning(data-toggle='modal', data-target='#myModal')
24. | Terminos de uso
25. a.btn.btn-primary(href='/catalog') Ver documentación
26.
27. #myModal.modal.fade(
28. tabindex='-1', role='dialog', aria-labelledby='myModalLabel',
29. aria-hidden='true')
30. .modal-dialog
31. .modal-content
32. .modal-header
33. button.close(type='button', data-dismiss='modal',
34. aria-hidden='true') ×
35. h4#myModalLabel.modal-title Terminos de uso
36. .modal-body
37. p El API de Mini Twitter es provista por Oscar Blancarte, autor del libro
38. strong "Desarrollo de aplicaciones reactivas con React, NodeJs y MongoDB"
39. | con fines exclusivamente educativos.
40. p Esta API es provista
41. strong "tal cual"
42. | esta, y el autor se deslizanda de cualquier problema o falla
43. | resultante de su uso. En ningún momento el autor será responsable
44. | por ningún daño directo o indirecto por la pérdida o publicación
45. | de información sensible.
46. strong El usuario es el único responsable por el uso y la información

507 | Página
47. | que este pública.
48. .modal-footer
49. button.btn.btn-primary(type='button', data-dismiss='modal') Cerrar
50.
51.
52. script(src='https://code.jquery.com/jquery.js')
53. script(src='//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js')

Analicemos primero que nada la estructura básica del documento. En la línea 1,


definimos el meta doctype, para que el navegador entienda que estamos
utilizando HTML5. En las líneas 2, 3 y 12, definimos las etiquetas <html>, <head>
y <body>.

Dentro del head importamos la fuente Robo, el framework Bootstrap e


importamos nuestras propias clases de estilo (api-styles.css).

El body está compuesto básicamente por dos secciones, el panel que podemos
ver en pantalla y un diálogo que muestra los términos de uso. La primera sección
(líneas 13 a 25) corresponde a lo que ve el usuario en pantalla, el cual contiene
un título (línea 14), el cuerpo o mensaje (líneas 15 a 21) y el footer, que es
donde ponemos los botones (líneas 22 a 25). Observemos que el botón para los
términos de uso (línea 23), contiene los atributos data-toggle y data-target, los
cuales son provistos por Bootstrap para crear paneles modales, el primer atributo
es el tipo de pantalla que queremos, en este caso modal y el segundo es el ID
del elemento que vamos a mostrar cuando se presione el botón. El otro botón
nos manda a /catalog, en donde estarán listados todos los servicios disponibles.

La segunda sección del archivo es el panel modal, el cual contiene el mismo ID


(myModal) que pusimos en el botón. Este panel contiene básicamente lo mismo,
un header, un body y un footer, con la diferencia, de que el botón del footer cierra
el modal.

Service catalog

Como parte del API, siempre deberemos tener una sección donde listemos los
servicios disponibles. Los servicios pueden ser mostrados por categorías si son
muchos o listar todos en una misma sección, si el número de servicios es
reducido, como es nuestro caso, una solo pantalla servirá para mostrarlos.

Antes de empezar a desarrollar esta sección, veamos el resultado final:

Página | 508
Fig. 211 - API Catalog.

Como podemos observar, el catalogo es simplemente una lista con los servicios
disponibles con la información básica:

Fig. 212 - Información de cada servicio.

Cada servicio contará con la siguiente información:

 Nombre: Nombre descriptivo del servicio


 Descripción: Una breve nota acerca de lo que hace el servicio
 URL: Dirección en la que responde el servicio
 Método: Método o verb en el cual responde el servicio
 Autenticación: Indicador que le dice al usuario si el servicio requiere
autenticación.

509 | Página
Lo primero que crearemos será, el archivo api-catalog.pug en el path
/public/apidoc, el cual se ve de la siguiente manera:

1. doctype html
2. html(lang="es")
3. head
4. title Mini Twitter API REST
5. link(href='https://fonts.googleapis.com/css?family=Roboto', rel='stylesheet')
6. link(rel='stylesheet'
7. href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css'
8. integrity='sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u'
9. crossorigin='anonymous')
10. link(rel='stylesheet', href='/public/apidoc/api-styles.css')
11. body
12. .container
13. .row
14. .col-xs-12
15. .method-templete
16. .list-group
17. each item in services
18. a.list-group-item(href=item.apiURLPage)
19. if item.secure
20. span.badge secure
21. h4.list-group-item-heading #{item.title}
22. span.label.label-success #{item.method}
23. span.label.label-primary #{item.url}
24. p.list-group-item-text #{item.desc}
25. script(src='https://code.jquery.com/jquery.js')
26. script(src='//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js')

Esta página recibe como parámetro un objeto llamado “services”, que no es más
que un array con los datos de todos los servicios disponibles, el cual vamos a
iterar en la línea 17 para representar cada servicio. Vamos a utilizar la variable
“ítem” para guardar los datos de cada servicio. Los atributos disponibles para
cada servicio son:

 apiURLPage: URL en la que podemos encontrar la documentación


del servicio
 title: nombre del servicio
 method: método en el que responde
 url: URL en la que responde
 desc: descripción del servicio
 secure: booleano que indica si el servicio requiere un token de
autenticación
El segundo será crear un router que escuche en el path /catalog en el archivo
api.js:

1. router.get('/', function(req, res) {


2. res.send(pug.renderFile(__dirname + '/../public/apidoc/api-index.pug'))
3. })
4.
5. router.get('/catalog', function(req, res){
6. const meta = require('../public/apidoc/meta/catalog.js')
7. res.send(
8. pug.renderFile(__dirname + '/../public/apidoc/api-catalog.pug', meta))
9. })

Página | 510
Como podrás ver, este router muestra el archivo api-catalog.pug y le manda
como parámetro el objeto meta, el cual es un archivo que contiene la lista de
servicios, el cual vamos a explicar.

Ahora bien, esta página requiere del archivo catalog.js, el cual deberemos de
crear en el path /public/apidoc/meta y se verá de la siguiente manera:

1. const loginPost = require('./login-post.js')


2. const reloginGet = require('./relogin-get.js')
3. const signupPost = require('./signup-post.js')
4. const profileGet = require('./profile-get.js')
5. const profilePut = require('./profile-put.js')
6. const followersGet = require('./followers-get.js')
7. const followingsGet = require('./followings-get.js')
8. const suggestedUsersGet = require('./suggestedUsers-get.js')
9. const followPost = require('./follow-post.js')
10. const usernameValidateGet = require('./usernamevalidate-get.js')
11. const tweetsGet = require('./tweets-get.js')
12. const tweetsusernameGet = require('./tweetsusername-get.js')
13. const addtweetsPost = require('./addtweets-post.js')
14. const tweetsdetailsGet = require('./tweetsdetails-get.js')
15. const tweetlikePost = require('./tweetlike-post.js')
16.
17. module.exports = {
18. services: [
19. loginPost,
20. reloginGet,
21. signupPost,
22. profileGet,
23. profilePut,
24. followersGet,
25. followingsGet,
26. suggestedUsersGet,
27. followPost,
28. usernameValidateGet,
29. tweetsGet,
30. tweetsusernameGet,
31. addtweetsPost,
32. tweetsdetailsGet,
33. tweetlikePost
34. ]
35. }

Este archivo solo exporta un objeto llamado services, el cual es creado a partir
de una serie de objetos, es decir un archivo por servicio.

La idea es la siguiente, vamos a crear un archivo independiente que contenga los


metadatos de un servicio, de esta forma, vamos a tener un archivo por servicio.

Vamos a explicar la estructura que deberá tener cada archivo, la cual es la misma
para todos, pero la información cambia:

1. module.exports = {
2. apiURLPage: "/catalog/addtweets-post",
3. title:"Creación de nuevo Tweet",
4. desc:"Servico utilizado para la creación de un nuevo Tweet",
5. secure: true,
6. url: "/secure/tweets",
7. method:"POST",
8. urlParams: [],

511 | Página
9. requestFormat:"{}",
10. dataParams: "{}",
11. successResponse: "{}",
12. errorResponse:"{}"
13. }

Analicemos los campos que tiene:

 apiURLPage: URL en la que encontramos la documentación del servicio


 title: nombre del servicio
 desc: descripción del servicio
 secure: booleano que indica si el servicio tiene seguridad
 url: URL donde responde el servicio.
 method: método en el que responde el servicio
 urlParams: URL en la que responde el servicio
 requestFormat: formato del request
 dataParams: Un JSON con un ejemplo del request
 successResponse: Ejemplo de una respuesta
 errorResponse: ejemplo de una respuesta con error.

Debido a que son un total de 15 archivos y que son bastante repetitivos, vamos
a limitarnos a mostrar solo uno, con la única finalidad darnos una idea de cómo
quedaría un archivo terminado. El resto de archivos lo podemos encontrar en el
repositorio de Github.

El siguiente archivo corresponde al servicio “Consulta de seguidores”:

1. module.exports = {
2. apiURLPage: "/catalog/followers-get",
3. title:"Consulta de seguidores de un usuario determinado",
4. desc:"Mediante este servico es posible recuperar los seguidores de un usuario d
eterminado por el url param 'username'",
5. secure: false,
6. url: "/followers/:username",
7. method:"GET",
8. urlParams: [
9. {
10. name: "username",
11. desc: "Nombre de usuario",
12. require: true
13. }
14. ],
15. requestFormat:"",
16. dataParams: "",
17. successResponse: "{\r\n \"ok\":true,\r\n \"body\":[\r\n {\r\n
\"_id\":\"5938bdd8a4df2379ccabc1aa\",\r\n \"userName\":\"emmanuel\",\r\n
\"name\":\"Emmauel Lopez\",\r\n \"description\":\"Nuevo en Twitte
r\",\r\n \"avatar\":\"<Base 64 Image>\",\r\n \"banner\":\"<Base 6
4 Image>\"\r\n },\r\n {\r\n \"_id\":\"5938bdd8a4df2379ccabc1aa\
",\r\n \"userName\":\"carlos\",\r\n \"name\":\"Carlos Hernandez\"
,\r\n \"description\":\"Nuevo en Twitter\",\r\n \"avatar\":\"<Bas

Página | 512
e 64 Image>\",\r\n \"banner\":\"<Base 64 Image>\"\r\n }\r\n ]\r\n}
",
18. errorResponse:"{\r\n \"ok\": false,\r\n \"message\": \"No existe el usuario
\"\r\n}"
19. }

Podemos ver que la documentación de este servicio lo vamos a encontrar en


“/catalog/followers-post”, vemos el nombre, la descripción, un request y
response de ejemplo, los cuales deberá estar correctamente especificado para
ser representados como un String. También tenemos una lista de URL params,
en el cual podemos indicar el nombre, la descripción y si es requerido o no.

Al final, deberemos tener una estructura igual que la siguiente:

Fig. 213 - Listado de archivos

Service documentation

La última página que nos faltaría, es donde podemos ver toda la documentación
del servicio, la cual se verá de la siguiente manera:

513 | Página
Fig. 214 - Documentación de un servicio.

Para crear esta nueva sección, deberemos crear el archivo api-method.pug, el


cual deberá estar en el siguiente path /public/apidoc.

1. doctype html
2. html(lang="es")
3. head
4. title Mini Twitter API REST
5. link(rel='stylesheet',
6. href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css')
7. script(
8. src='//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js')
9. script.
10. hljs.initHighlightingOnLoad();
11. link(href='https://fonts.googleapis.com/css?family=Roboto', rel='stylesheet')
12. link(rel='stylesheet',
13. href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css',
14. integrity='sha384-
BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u',
15. crossorigin='anonymous')

Página | 514
16. link(rel='stylesheet', href='/public/apidoc/api-styles.css')
17. body
18. .container
19. .row
20. .col-xs-12
21. .method-templete
22. if secure
23. span.secure-icon &#128274;
24. .form-group
25. label(for='name') Nombre
26. output#name #{title}
27. .form-group
28. label(for='desc') Descripción
29. output#desc #{desc}
30. .form-group
31. label(for='url') URL
32. output#url #{url}
33. .form-group
34. label(for='method') Method
35. output#method #{method}
36. .form-group
37. label(for='urlParams') URL Params
38. ul.list-group1
39. each item in urlParams
40. li.list-group-item
41. strong= item.name + ': '
42. span= item.desc
43. if item.require
44. span.badge.badge-warning.badge-pill requerido
45. else
46. li.list-group-item Sin parámetros
47. .form-group
48. label(for='requestFormat') Formato del request
49. <pre><code class="json">#{requestFormat}</code></pre>
50. .form-group
51. label(for='dataParams') Request
52. <pre><code class="json">#{dataParams}</code></pre>
53. .form-group
54. label(for='successResponse') Respuesta OK
55. <pre><code class="json">#{successResponse}</code></pre>
56. .form-group
57. label(for='errorResponse') Respuesta Error
58. <pre><code class="json">#{errorResponse}</code></pre>
59. if secure
60. .alert.alert-danger
61. strong &#128274; Servicio con seguridad
62. p Este es un servicio con seguridad habilitada, para poder ser
63. | ejecutado, es requerido que se le envíe el
64. strong token
65. | dentro del header
66. strong authorization
67. | de lo contrario, el servicio negará el acceso.

Mediante este archivo, creamos un simple formulario, el cual mostrará cada uno
de los valores contenidos en los objetos JSON que acabamos de analizar.

De las líneas 24 a 35 mostramos los campos title, desc, url y method, después
de esto, realizamos un each (línea 39) para cada URL param definido.

Para los campos requestFormat, dataParams, successResponse y errorResponse,


utilizamos la librería highlight, la cual permite darle formato al texto para verse
como código. Es por ese motivo que agregamos los valores dentro de un <pre>
con un atributo correspondiente al tipo de documento (class="json"). Cuando

515 | Página
la librería se active (línea 10), reconocerá las clases de estilo y dará formato
automáticamente.

Finalmente, en la línea 59 validamos si el servicio es seguro para mostrar una


leyenda que advierta de la seguridad del servicio.

Ya con este último archivo hemos completado la documentación del servicio y ya


solo nos queda navegar por todas las secciones y comprobar los resultados.

Algunas observaciones o mejoras al API

Esta sección la defino para nombrar todas las mejores que podríamos agregar al
API, las cuales por practicidad y no complicar mucho más el API, he decidido
darles una solución “rápida”, la cual puede no ser la mejor forma de implementar.
Todas las mejoras aquí planteadas las puedes tomar como ejercicios para
mejorar tus habilidades en el desarrollo de API’s.

Aprovisionamiento de imágenes

Sin duda, el aprovisionamiento de imágenes es por mucho, un de las áreas de


mejor más importantes, debido a que actualmente el API sirve las imágenes en
formato base 64 dentro de una propiedad del Tweet o del Perfil.

Esta estrategia puede resultar cómoda y fácil de implementar, pues solo


agregamos el base 64 al Schema de Mongoose y guardamos. Cuando
consultamos la imagen también es muy simple, ya que simplemente regresamos
el base 64 al navegador y él sabrá como mostrarla.

El problema con esta estrategia es que al retornar la imagen como base 64 dentro
de un objeto JSON, impide que el navegador utilice el cache para no cargar una
imagen que ya cargo antes. Por ejemplo, la foto del perfil:

Página | 516
Fig. 215 - Foto de perfil en base64

En la imagen anterior, vemos al usuario posicionado dentro del detalle de un


Tweet y podemos ver en este momento, 5 veces la foto del perfil. Como la imagen
se carga mediante base 64, el navegador carga la misma imagen cada vez que
la requiere.

La solución a este problema, es siempre regresar un URL de donde podamos


recuperar la imagen, de esta forma el navegador podrá determinar que un
recurso ya lo ha cargado en el pasado y simplemente lo cargará del cache. Un
ejemplo de esto lo podemos ver en el siguiente objeto Profile:

1. {
2. "ok": true,
3. "body": {
4. "_id": "593616dc3f66bd6ac4596328",
5. "name": "Name",
6. "description": "Descripción",
7. "userName": "user name",
8. "avatar": "http://api.site.com/profile/avatar/593616dc3f66bd6ac4596328",
9. "banner": "http://api.site.com/profile/banner/593616dc3f66bd6ac4596328",
10. "tweetCount": 44,
11. "following": 0,
12. "followers": 3,
13. "follow": false
14. }
15. }

Podemos apreciar los campos avatar y banner que en lugar de tener una imagen
en base 64, tiene un URL que lleva a donde está la imagen.

Tenemos dos formas de implementar esto, la primera es utiliza un servicio de


almacenamiento externo como Amazon S3, Google Storage, Dropbox, etc. que
nos permita subir la imagen a la nube y luego simplemente nos proporcione un
URL en donde podamos recuperar la imagen, entonces en MongoDB guardamos
esta URL en lugar del base 64.

517 | Página
La segunda forma es seguir guardado el base 64 dentro de Mongo, pero
proporcionar un servicio del API que recupere la imagen por URL. El servicio
podrá tener URL params para saber qué imagen necesitamos y de que
usuario/tweet, por ejemplo /resources/:userId/avatar y
/resources/:userId/banner, con estos URL params podemos recuperar el Perfl
solicitado y hora si regresar la imagen en base64. Si bien la imagen la seguimos
mandando en base 64, el navegador es lo suficiente inteligente para saber que
un recurso solicitado por URL ya lo tiene en cache y evitar solicitarlo nuevamente.

Guardar la configuración en base de datos

En este momento, toda la configuración relacionada con el API y la aplicación, la


hemos guardado en el archivo config.js, sin embargo, en la práctica, esto no es
una buena solución, ya que cada cambio, requiere un reinicio de la aplicación.

Para solucionar este problema, podríamos guardar toda la configuración en la


base de datos, para lo cual, podríamos crear una colección para guardar la
configuración. Con la única excepción de las propiedades para la conexión a la
base de datos por obvias razones.

Documentar el API por base de datos

Como acabamos de ver, es necesario crear un archivo JavaScript para cada uno
de los servicios que tenemos, lo cual podría ser bastante complicado de
administrar, para ello, podríamos crear una nueva colección para guardar la
documentación de los servicios y simplemente recuperarla cuando sea necesaria.

Página | 518
Resumen

Finalmente, hemos aprendido a crear un API completamente desde cero,


aplicando toda la teoría que hemos venido aprendiendo sobre NodeJS y Express.
También hemos aprendido a utilizar subdominios y como estos influyen en la
creación de un API exitosa, junto con los subdominios, hemos analizado que es
CORS y la forma que tenemos para habilitar el consumo de recursos de un
dominio diferente al de la aplicación.

Junto con el API, hemos creado una página especial para documentar cada uno
de los servicios que ofrece nuestra API, utilizando para ello, el motor de plantillas
Pug.

En este momento, ya estamos solo a un paso de finalizar nuestro proyecto, y


solo nos falta lanzar nuestro proyecto a producción, lo cual, es lo que estaremos
abordando en el siguiente capítulo.

519 | Página
Producción
Capítulo 16

Una de las cosas más importantes cuando desarrollamos una aplicación, es el


pase a producción, pues será cuando finalmente la aplicación estará expuesta al
público en general. En esta etapa, debemos optimizar la página para que
consuma me menor cantidad de recursos y el usuario experimente un menor
tiempo de carga. Por otra parte, definir un canal de comunicación segura es clave
para proteger los datos del cliente y ganar una mayor confianza por parte del
usuario. Finalmente, tener una arquitectura tolerante a fallas, es clave para
garantizar la disponibilidad de la aplicación a lo largo del tiempo.

En este capítulo analizaremos técnicas para que el pase a producción sea lo más
simple posible, pero también implementar todas estas prácticas que harán de
nuestro sitio más rápido, seguro y robusto.

Producción vs desarrollo

Si ya tienes tiempo en el mundo del desarrollo de software, sabrás la diferencia


que existe entre un ambiente de pruebas y otro de producción, sin embargo, me
gustaría abordar el tema para que quede claro para todos.

Primero que nada, definamos que es un ambiente de producción y uno de


desarrollo:

 Desarrollo: es un ambiente de pruebas en donde el desarrollador tiene


carta abierta para realizar casi cualquier maniobra, como actualizar los
sistemas, borrar o insertar información, instalar o desinstalar cosas, etc.
Este ambiente debe de ser utilizado únicamente por los programadores o
personas de TI que siguen construyendo o solucionando issues.
 Producción: es el ambiente utilizado por el usuario final, el cual no
puede ser manipulado con fines de pruebas o incluso actualizado sin un
proceso de calidad previo. Este ambiente no debe de ser manipulado por
desarrolladores o persona de TI sin una justificación, como puede ser,
solucionar un Issue o actualizar la versión para agregar nuevos features.

Desde luego que en las grandes empresas hay más ambientes que solo desarrollo
y producción, como el ambiente de pruebas (para testing), QA (pruebas de UAT)

Página | 520
y Stress (pruebas de carga) y quizás algunos ambientes más. Sin embargo,
nosotros abordaremos solo desarrollo y producción.

Un error muy común es creer que el ambiente de producción, es el ambiente que


apunta la base de datos de producción, o sea a la información de verdad, y es
cierto, pero no es solo eso. Un ambiente de producción además de apuntar a una
base de datos real, debe contar con configuraciones especiales que ayuden a su
desempeño, seguridad y alta disponibilidad.

Habilitar el modo producción

Iniciar la aplicación en modo producción es realmente simple, pues solo basta


con definir la variable de entorno NODE_ENV=production. Esta variable pude ser
accedida desde NodeJS mediante process.env.NODE_ENV, sin embargo, definir la
variable, no hace más que pasarle el parámetro a NodeJS y seremos nosotros los
que tendremos que realizar acciones dependiendo del valor de esta variable.

Para definir esta variable, nos iremos al archivo package.json y modificaremos la


sección scripts para dejarla de la siguiente manera:

En Windows:

1. "scripts": {
2. "start": "set NODE_ENV=production&&nodemon server.js"
3. },

En Linux

1. "scripts": {
2. "start": "NODE_ENV=production&&nodemon server.js"
3. },

La única diferencia entre sistema operativo, es que en Windows es necesario usar


“set” para definir la variable, mientras que en Linux no.

Ahora bien, para comprobar el ambiente, agregaremos la siguiente línea en el


archivo server.js antes de la conexión de la base de datos.

1. console.log("ENV ==> " , process.env.NODE_ENV)

Finalmente ejecutaremos el comando npm start para ver los resultados en la


consola:

521 | Página
Fig. 216 - Validando la variable NODE_ENV.

Como puedes ver, en la consola se aprecia que el ambiente dice “production”,


con lo cual, ya podemos empezar a realizar acciones dentro de nuestra aplicación
para optimizarla.

Empaquetar la aplicación para producción

Como ya sabemos, Webpack es el módulo que se encargar de realizar el


empaquetamiento de toda nuestra aplicación dentro de un archivo único, el cual
conocemos como bundle.js. Este archivo lo hemos estado creando con ayuda de
un módulo llamado webpack-dev-middleware, el cual, como su nombre lo indica,
es únicamente para desarrollo.

Este módulo tiene como finalidad facilitar el ciclo de desarrollo, pues evita tener
que compilar manualmente cada cambio. Sin embargo, el archivo bundle.js lo
crea en memoria y no en la carpeta /public, como debería.

Para solucionar este problema, deberemos desactivar el módulo webpack-dev-


middleware solo para el ambiente de producción. Además, es necesario configurar
el proyecto para que en modo producción empaquete el archivo bundle.js en
modo producción, esto se generará un archivo mucho más reducido, logrando
una carga más rápida para el usurario.

Para desactivar el módulo webpack-dev-middleware, iremos al archivo server.js


y condicionaremos la ejecución del módulo mediante el ambiente:

1. if(process.env.NODE_ENV !== 'production'){


2. app.use(require('webpack-dev-middleware')(compiler, {
3. noInfo: true,
4. publicPath: config.output.publicPath
5. }))
6. }

Como podemos ver en la línea 1, hemos condicionado la ejecución del módulo,


de tal forma que se ejecutará en cualquier ambiente excepto producción.

Página | 522
El siguiente paso será indicarle a webpack que empaquete nuestra aplicación
para producción, para ello, tendremos que ir al archivo webpack.config.js y
agregar las siguientes líneas:

1. var webpack = require('webpack')


2.
3. module.exports = {
4. entry: [
5. ...
6. ],
7. output: {
8. ...
9. },
10.
11. module: {
12. ...
13. },
14. plugins: [
15. new webpack.DefinePlugin({
16. 'process.env': {
17. 'NODE_ENV': JSON.stringify('production')
18. }
19. })
20. ]
21. };

Ahora bien, si en este momento intentamos entrar a la aplicación Mini Twitter,


verás que nos sale una pantalla en blanco. Esto se bebe a que el módulo webpack-
dev-middleware ha dejo de compilar los archivos y ya no está disponible el archivo
bundle.js. Para solucionar este problema, deberemos encargarnos nosotros
mismo de empaquetar el archivo utilizando webpack, para ello, nos podemos
apoyar nuevamente de los scripts del archivo package.json, por lo que en esta
vez, agregaremos los siguientes:

1. "scripts": {
2. "build": "webpack -p",
3. "server": "NODE_ENV=production&&node server.js",
4. "server_windows": "set NODE_ENV=production&&node server.js",
5. "start": "npm run build && npm run server_windows"
6. },

Primero que nada, observa que hemos cambiado el comando existente “start”
para que ejecute dos comandos en serie, primero que nada, ejecutará el
comando “build” (npm run build) y luego server_windows (npm run
server_windows). El script build es el encargado de ejecutar el comando webpack
-p (p=producción) y luego el comando server_windows establecerá la variable de
entorno NODE_ENV y seguido, iniciará el servidor (server.js).

NOTA: si estas en Linux, solo cambia server_windows por server en el comando


“start”.

Otro cambio súper importante, es que hemos dejado de utilizar nodemon


para ejecutar el server, esto por dos razones, primero que nada, ya estamos
en producción y nodemon es un server para desarrollo, el cual consume más
recursos y la segunda, es que al no tener el módulo webpack-dev-middleware
activo, no habrá nadie que actualice los cambios, por lo tanto, aunque nodemon
se reinicie, el archivo bundle.js permanecerá igual. Por tal motivo, a partir de

523 | Página
este momento, será necesario apagar el server por completo e iniciar cada vez
que tengamos un nuevo cambio, de lo contrario, no se reflejará.

Ya con esta explicación, solo nos resta apagar el servidor e iniciarlo nuevamente
con el comando npm start. Una vez iniciado, intentamos entrar a la aplicación
Mini Twitter para comprobar que ahora si podemos ver la aplicación.

Fig. 217 - Aplicación en modo productivo.

Una vez que estés en la aplicación, quiero que observar que el icono de React ha
cambiado de color rojo al color azul, y si le damos click, nos arrojará un mensaje
indicándonos que la aplicación está corriendo en modo productivo. De lado
izquierdo puedes ver como se veía la aplicación antes de los cambios, del lado
derecho, como se ve ahora.

Icono de React
Si no logras ver el icono de React en la parte superior
del navegador, es porque no has instalado el plugin
necesario, en tal caso, puedes regresar al inicio del
libro para aprender como instalar el plugin React
Developer Tools.

NOTA: si quieres volver a ejecutar la aplicación nuevamente en modo desarrollo,


tan solo es necesario ejecutar el comando nodemon server.js.

Puertos
Internet trabaja exclusivamente con los puertos 80 para HTTPS y 443 para
HTTPS, por lo que configurar nuestra aplicación para trabajar en estos puertos

Página | 524
es indispensable, de lo contrario, los usuarios no podrán acceder a nuestra página
con solo poner el dominio, si no que tendrán que adivinar el puerto en el cual
responde la aplicación.

Cuando el usuario entra a google.com, lo que hace el navegador internamente


es enviar una solicitud GET a google.com:80, por otra parte, cuando entramos
por https, por ejemplo https://google.com, el navegador realiza la petición al
puerto 443, es decir https://google.com:443.

Dado que nuestro proyecto escucha en el puerto 8080, será muy difícil que un
usuario pueda acceder a nuestra aplicación. Para solucionar esto tenemos dos
opciones, la primera y más simples, es cambiar el puerto de NodeJS al 80. La
segunda opción es crear un proxy que escuche en el puerto 80 y luego
redirecciones la llamada al nuestro servidor en el puerto configurado en NodeJS,
esto se puedo lograr con Apache, Nginx, etc.

Configurar un servidor proxy queda fuera del alcance de este libro, sin embargo,
quería mencionar la alternativa por si quieres investigar más a profundidad. Esto
nos deja únicamente con la primera opción, que es cambiar el puerto en NodeJS.
Para ello, tendremos que regresar al archivo server.js y modificar el puerto:

1. app.listen(80, function () {
2. console.log('Example app listening on port 80!')
3. })

Tras realizar este simple paso, podremos acceder a la aplicación con tan solo
poner en el navegador “localhost” sin necesidad de especificar el puerto.

Fig. 218 - Probando el puerto 80.

En la imagen podemos ver como la aplicación responde directamente el


“localhost” sin necesidad de un puerto.

525 | Página
Posibles problemas con el puerto 80
El puerto 80 es muy demandado, muchas aplicaciones
lo utilizan sin que nos demos cuenta, por lo que, si al
iniciar la aplicación sale un error de que el puerto está
siendo utilizado, tendremos que identificar que
aplicación lo utiliza y detenerlo. También, en
ocasiones nos solicita privilegios como administrador
para utilizarlo.

Comunicación segura

Proporcionar un canal seguro para que la información viaje entre el cliente y el


servidor, es ciertamente indiscutible, pues cualquier mensaje que viaje
desprotegido, puede ser fácilmente capturado y utilizado para hacer daños.
Alguna de la información que puede ser capturada son, los password, datos de
nuestros clientes, información sensible como datos de las tarjetas de crédito, etc.
En realidad, cualquier dato que viaje puede ser recuperado.

Por este motivo, utilizar conexiones seguras con HTTPS es indispensable, pues
protege la información encriptándola durante su viaje por internet, y una vez que
llega al destinatario, solo el cliente o el servidor sabrán como descifrarla.

Para establecer una comunicación segura por HTTPS, es necesario un certificado,


con el cual cifraremos todas las comunicaciones.

Existen dos formas de obtener un certificado, la primera es crearlo nosotros


mismo y la segunda es comprarla a una autoridad emisora de certificados.

Certificados comprados

Los certificados comprados son emitidos por autoridades de internet, los cuales
recopilan información de nuestra empresa y nos cobran una cantidad para
emitirlos.

Los certificados comprados, pueden ser validados por los navegadores, lo que
habilita el candado verde que podemos ver en la barra de navegación:

Página | 526
Fig. 219 - Validación de un certificado SSL comprado.

Este tipo de certificado es considerado de confianza por el navegador, por lo


que nos permite el acceso al sitio sin ningún problema y advierte al usuario que
todas las comunicaciones con esta página son seguras, lo que le da al usuario la
confianza de introducir datos sensibles como password y tarjetas de crédito.

Existen varios proveedores que nos pueden vender certificados, como GoDaddy,
Comodo, Namecheap, Digicert, etc. Dado que la única diferencia que existe entre
todos los proveedores es el precio, puedes inclinarte por el que tenga el mejor
precio. En lo personal yo utilizo los certificados de Namecheap, pues ofrece
certificados desde 9 usd al año, lo cual es bastante económico.

Certificados ligados a los dominios


Los certificados comprados solo sirven para el dominio
que fue comprado, por lo que si lo utilizamos para un
dominio diferente, el navegador lanzara todas las
alertas y no permitirá al usuario entrar directamente.

Certificados auto firmados

Por otro lado, tenemos los certificados auto firmados, los cuales pueden ser
emitidos por quien sea, incluso, podemos crear nuestros propios certificados
nosotros mismos sin ningún costo.

Los certificados comprados y los auto firmados tiene EXACTAMENTE el mismo


nivel se seguridad, sin embargo, tiene una enorme diferencia, y es que cuando
utilizamos un certificado que no ha sido emitido por una autoridad, el navegador
lanzará una pantalla de “PELIGRO” antes de cargar la página:

527 | Página
Fig. 220 - Advertencia de conexión no segura.

Esta pantalla ara que la gente salga corriendo del sitio y solo los más valientes
tendrán el valor de entrar.

Para que el navegador nos permite acceder a la página, nos pedirá que
agreguemos el sitio a las excepciones de seguridad, por lo que tendremos que
dar click en “Avanzado” y luego en “Añadir excepción”.

Una vez realizado esto, en navegador nos permitirá entrar al sitio, pero nos
indicará que el sitio no es seguro:

Fig. 221 - Advertencia de sitio no seguro.

Página | 528
Como podemos ver en la imagen, el navegador nos arroja un mensaje en rojo
indicando que no es seguro, lo que hará que los pocos que tuvieron el valor de
entrar duden de la seguridad del sitio.

A pesar de todas las advertencias, tengo que resaltar que la seguridad es


EXACTAMENTE la misma que comprar un certificado y nos ponga el candado
verde. El navegador advierte de seguridad que el certificado no es seguro porque
no hay nadie que avale al dueño del certificado y por ende al sitio web.

En este punto, te estarás preguntando, entonces para que pueden servir este
tipo de certificados, la respuesta es simple, se utilizan con regularidad para sitios
de intranet o que solo lo acceder personas de confianza de la misma empresa,
las cuales saben que pueden confiar en el sitio y en el certificado. Sin embargo,
para el público en general, es totalmente desaconsejado.

Instalando un certificado en nuestro servidor

Dado que comprar un certificado y su domino correspondiente como un ejemplo


no es una opción, vamos a aprender instalando un certificado autoformado.
Aunque el procedimiento para instalar uno de paga es el mismo, solo cambia
quien lo genera.

Sea cual sea el certificado que utilicemos, al final del día, tendremos dos archivos,
un certificado y una llave, que será lo que necesitamos para agregar HTTPS a
nuestro servidor.

Para auto generar nuestro certificado, tendremos que descargar una librería
criptográfica que nos permite utilizar SSL. La más popular es OpenSSL, la cual
es OpenSource.

En Windows puedes descargarlo de:

http://gnuwin32.sourceforge.net/packages/openssl.htm

mientras que para Linux lo podemos descargar en:

https://www.openssl.org

El proceso de instalación dependerá del sistema operativo, por lo que tendremos


que seguir las instrucciones correspondientes para cada sistema operativo. Una
vez instalado, probemos lanzar el comando ssl en la línea de comandos, si el
comando no es reconocido, tendremos que agregar la dirección <install-
path>/bin al path de las variables de entorno.

529 | Página
Ya que OpenSSL está correctamente instalado, crearemos la carpeta cert en la
raíz del proyecto (/) y nos ubicaremos dentro de la carpeta desde la línea de
comandos, luego ejecutamos el siguiente comando:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365

Los parámetros son los siguientes:

 Req: comando para solicitar la generación de un certificado


 -x509: indica el formato del certificado
 -newkey: comando para la generación de la llave del certificado y rsa:2048
es el algoritmo de cifrado (RSA) a 2048 bits
 -keyout: parámetro para indicar donde dejar la llave y key.pem es el nombre
de la llave generada.
 -out: comando para indicar donde dejar el certificado y cert.pem es el
nombre del certificado generado.
 -days: indica el número de días que tendrá vigencia el certificado.

Tras ejecutar el comando, nos pedirá un password y una serie de datos


relacionados, con la empresa o la persona que está emitiendo el certificado:

Fig. 222 - Creando el certificado con OpenSSL.

Una vez creados los certificados, los podremos ver desde nuestro editor:

Página | 530
Fig. 223 - Certificados creados exitosamente.

El siguiente paso será iniciar el servidor con los nuevos certificados y


redireccionar las peticiones por HTTP (80) a HTTPS (443), asegurándonos de
siempre llevar al usuario por un canal seguro. Para esto, regresaremos al archivo
server.js y realizaremos las siguientes modificaciones:

1. var fs = require('fs')
2. var https = require('https')
3. var http = require('http')
4.
5. . . .
6.
7. https.createServer({
8. key: fs.readFileSync('./certs/key.pem'),
9. cert: fs.readFileSync('./certs/cert.pem'),
10. passphrase: '1234'
11. }, app).listen(443, () => {
12. console.log('Example app listening on port 443!')
13. });
14.
15. http.createServer(function (req, res) {
16. res.writeHead(301, {"Location": "https://" + req.headers['host'] + req.url})
17. res.end()
18. }).listen(80)
19.
20. app.listen(80, function () {
21. console.log('Example app listening on port 80!')
22. })

Vamos a importar los módulos fs, http y https, los cuales ya viene por default
con NodeJS, por lo que no hay que instalar nada adicional.

Lo siguiente es iniciar el server mediante el método createServer de https (línea


7) , para lo cual se requieren dos parámetros, un objeto de configuración donde
pondremos el certificado y el objeto app de express. En la configuración
pondremos la propiedad key, la cual apunta a la llave del certificado, cert a la
ruta del certificado y passphrase es el password con el que creamos el certificado.
Finalmente, le mandos el objeto app para crear el servidor basado en la
configuración que ya tenemos y definimos el puerto 443.

El siguiente paso es redireccionar al usuario que entren por el puerto 80 al canal


seguro (443), para ello, vamos a crear otro server que escucha en el puerto 80
(línea 15) el cual, al recibir una petición retorna un código 301 y lo redirecciona
a la misma página a la cual entro, pero por el puerto 443 (https).

Finalmente, eliminamos la antigua forma de levantar el server (línea 20 a 22).

531 | Página
Posibles problemas con el puerto 443
Al igual que el puerto 80, el puerto 443 es muy
solicitado por las aplicaciones, por lo que si el puerto
ya está siendo utilizado, tendremos que detener la
aplicación que lo utiliza.

Guardamos los cambios, reiniciamos el server y probamos entrar directamente a


“localhost” para ver cómo nos redireccionará a https:

Fig. 224 - Probando el canal seguro HTTPS.

Alta disponibilidad

La alta disponibilidad es súper esencial para cualquier aplicación en producción,


pues nos garantiza el funcionamiento de nuestra aplicación ante una falla. Lograr
que una aplicación sea infalible es imposible, pero al menos podemos
implementar técnicas que reduzcan ese margen al mínimo.

Cluster

Página | 532
Una de las principales técnicas de alta disponibilidad son los cluster, los cuales
son un conjunto de servidores trabajando como uno solo, de tal forma que, si
uno falla, los demás pueden seguir operando.

Podemos implementar un cluster de dos formas, la primera es tener varias


instancias de la aplicación corriendo en el mismo servidor físico y la segunda es
tener varias instancias de la aplicación corriendo en múltiples servidores físicos.

Desde luego que tener múltiples servidores físicos es lo mejor, pues si todo un
equipo falla, otros podrán seguir operando. sin embargo, esto está fuera del
alcance de este libro, pues para lograr eso es necesario más configuraciones,
herramientas y equipos adicionales que no tenemos en un ambiente local.

Por otra parte, tenemos el cluster en un solo equipo, el cual podemos


implementar fácilmente. Esta técnica consiste en tener varias instancias del
servidor corriendo en el mismo equipo, por lo que, si uno se cae por alguna razón,
el cluster lo apagará e iniciará una nueva instancia, de tal forma que siempre
exista un número determinado de instancias del servidor corriendo.

Otra de las cosas a tomar en cuenta es que, NodeJS se ejecuta en un solo hilo,
por lo que en procesadores multicores, no se aprovechará su potencial, por este
motivo, el cluster es una buena opción para lanzar múltiples procesos y así
mejorar el performance ante la carga de trabajo.

Lo primero que tenemos que hacer es instalar la librería cluster, mediante el


siguiente comando:

npm install --save cluster

El segundo paso será agregar todo el inicio del server dentro de una función, la
cual podremos reutilizar para crear las diferentes instancias del servidor. Para
ello, actualizaremos el archivo server.js para dejarlo de la siguiente manera:

1. var express = require('express');


2. var app = express();
3. var bodyParser = require("body-parser")
4. var path = require('path')
5. var webpack = require('webpack')
6. var config = require('./webpack.config')
7. var compiler = webpack(config)
8. var mongoose = require('mongoose')
9. var configuration = require('./config')
10. var vhost = require('vhost')
11. var api = require('./api/api')
12. var fs = require('fs')
13. var http = require('http')
14. var https = require('https')
15.
16. function startServer() {
17.
18. var opts = {
19. useNewUrlParser: true,
20. appname: "Mini Twitter",
21. poolSize: 10,
22. autoIndex: false,

533 | Página
23. bufferMaxEntries: 0,
24. reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect
25. reconnectInterval: 500,
26. autoReconnect: true,
27. loggerLevel: "error", //error / warn / info / debug
28. keepAlive: 120,
29. validateOptions: true
30. }
31.
32. let connectString = configuration.mongodb.development.connectionString
33. mongoose.connect(connectString, opts, function(err){
34. if (err) throw err;
35. console.log("==> Conexión establecida con MongoDB");
36. })
37.
38. app.use('*', require('cors')());
39.
40. app.use('/public', express.static(__dirname + '/public'))
41. app.use(bodyParser.urlencoded({extended: false}))
42. app.use(bodyParser.json({limit:'10mb'}))
43.
44. if(process.env.NODE_ENV !== 'production'){
45. app.use(require('webpack-dev-middleware')(compiler, {
46. noInfo: true,
47. publicPath: config.output.publicPath
48. }))
49. }
50.
51. app.use(vhost('api.*', api));
52.
53. app.get('/*', function (req, res) {
54. res.sendFile(path.join(__dirname, 'index.html'))
55. });
56.
57. https.createServer({
58. key: fs.readFileSync('./certs/key.pem'),
59. cert: fs.readFileSync('./certs/cert.pem'),
60. passphrase: '1234'
61. }, app).listen(443, () => {
62. console.log('Example app listening on port 443!')
63. });
64.
65.
66. http.createServer(function (req, res) {
67. res.writeHead(301, {
68. "Location": "https://" + req.headers['host'] + req.url })
69. res.end()
70. }).listen(80);
71. }
72.
73. if(require.main === module){
74. startServer();
75. } else {
76. module.exports = startServer;
77. }

De las líneas 16 a 71 hemos agregado todo el inicio del server dentro de la función
startServer, después en las líneas 73 a 77 definimos la forma en que se debe de
ejecutar el server. Cuando un archivo es ejecutado directamente mediante el
comando node, se establece el varlo require.main = ‘module’, por lo que si
ejecutamos esta clase directamente (node server.js) simplemente se ejecutará
la función de inicio del server. Por otra parte, si el archivo es ejecutado por otro

Página | 534
archivo, entonces solamente exportamos la función startServer para ser
utilizada por fuera.

Ahora, crearemos un nuevo archivo llamado cluster.js en la raíz del proyecto


(/), el cual se verá de la siguiente forma:

1. var cluster = require('cluster');


2.
3. console.log("ENV ==> " , process.env.NODE_ENV)
4.
5. if(cluster.isMaster){
6. require('os').cpus().forEach(function(){
7. startWorker()
8. });
9.
10. cluster.on('disconnect', function(worker){
11. console.log(
12. 'CLUSTER: Worker %d disconnected from the cluster.', worker.id)
13. });
14.
15. cluster.on('exit', function(worker, code, signal){
16. console.log(
17. 'CLUSTER: Worker %d died with exit code %d (%s)',
18. worker.id, code, signal);
19. startWorker()
20. });
21.
22. } else {
23. require('./server.js')()
24. }
25.
26. function startWorker() {
27. var worker = cluster.fork()
28. console.log('CLUSTER: Worker %d started', worker.id)
29. }

La idea es que ahora iniciemos el servidor mediante este archivo, en lugar de


server.js. Cuando este archivo es ejecutado, automáticamente se convierte en el
proceso master (línea 5) lo que indica que la expresión cluster.isMaster
retornará true. Esto hará que se ejecute la función startWorker por cada CPU
que tenga el equipo.

Lo siguiente, es registrar un evento para realizar una acción cuando un server se


desconecte (línea 10), en ese caso, solo mandamos un mensaje en pantalla para
saber lo que paso.

Por otra parte, registramos el evento “exit”, el cual nos permitirá realizar una
acción cuando un servidor se apague, en tal caso. Mandamos un mensaje en
pantalla y volvemos a ejecutar la función startWorker para reponer la instancia
del server que fallo.

Ahora bien. La función startWorker tiene como finalidad ejecutar el método fork,
el cual inicia un nuevo proceso, ejecutando de nuevo este archivo pero con una
diferencia, y es que ahora cluster.isMaster será igual a false, lo que hará que
se ejecute la función startServer del archivo server.js (línea 22).

535 | Página
Finalmente actualizaremos los script del archivo package.json para remplazar el
archivo server.js por cluster.js:

1. "scripts": {
2. "build": "webpack -p",
3. "server": "NODE_ENV=production&&node cluster.js",
4. "server_windows": "set NODE_ENV=production&&node cluster.js",
5. "start": "npm run build && npm run server_windows"
6. },

Apagamos el server y volvemos a iniciar el server mediante npm start:

Fig. 225 - Iniciando un cluster.js

Tras ejecutar el cluster, podemos observar cómo se han iniciado 8 procesos, pues
tengo un equipo con 8 cpus. Si vamos al administrador de tareas, podremos ver
varios procesos, los cuales corresponden a cada instancia del cluster + los
procesos propios del cluster:

Página | 536
Fig. 226 - Cluster process.

Una vez en los procesos, probemos con terminar algún, tiendo cuidado de no
matar el proceso del cluster, el cual podemos distinguir porque es el que menos
memoria consume:

Fig. 227 - Cluster recovery.

Como podemos ver, el cluster ha detectado que el nodo 4 ha muerto y ha iniciado


el worker 9.

Hosting y dominios

Una vez que toda nuestra aplicación ha sido configurada para operar en
producción, solo nos restaría conseguir un Hosting y un dominio apropiado para
nuestra aplicación.

Lo primero y más importante, es conseguir el dominio perfecto para nuestra


aplicación, pues será el nombre para que el mundo pueda acceder a la aplicación,
sin embargo, dado la demanda, la gran mayoría de los dominios con nombre
comunes o simples, ya han sido adquiridos, por lo que tendrás que validar si el
domino está disponible.

537 | Página
Una de las herramientas que utilizo para comprobar la disponibilidad de un
dominio es https://who.is/, en la cual, solo tenemos que poder el dominio que
queremos y nos arrojará si está disponible o no, y de no estar disponible, te da
datos del actual dueño, por si quisieras contactarlo para negociar la compra.
Desde aquí mismo te permite hacer la compra del dominio, sin embargo, en mi
experiencia, tiene los costos más altos del mercado, por lo que yo recomiendo
comprarlos a google directamente en https://domains.google.

Para asegurar que el API sigue funcionando en nuestro dominio, tendremos que
modificar el vhost del archivo server.js:

1. app.use(vhost('api.<domain>', api));

Donde <domain> es el dominio que hemos comprado, por ejemplo


minitwitter.com o reactiveprogramming.io, de tal forma que quedaría,
api.minitwitter.com o api.reactiveprogramming.io.

El siguiente paso es comprar un hosting en donde podamos alojar nuestra página,


y para esto, también hay muchas opciones, aunque en lo particular, a mí me
gusta utilizar DigitalOcean, pues permite alquilar servidores desde 5 USD al mes,
un precio bastante accesible y que soportan perfectamente un aplicación en
NodeJS con una carga moderada. Además, si la demanda crece y necesitas
aumentar el server, puede hacerlo dinámicamente con solo crecer el plan y no
hace falta configurar nada.

Una vez que tiene tanto el dominio como el hosting, tendrás que configurar el
DNS apuntar tu dominio a tu servidor. Por suerte, DigitalOcean también tiene el
servicio de DNS sin ningún costo.

En el servidor tendrás que instalar NodeJS como ya lo habíamos explicado al


inicio de este libro, descargar la aplicación e instalar todas las dependencias
mediante:

npm install

Finalmente, arrancamos la aplicación:

npm start

Si hemos hecho todo bien, ya deberíamos poder acceder a la aplicación


directamente con nuestro dominio.

Página | 538
Resumen

En este capítulo hemos aprendido como llevar efectivamente una aplicación hasta
producción. Optimizando muestro aplicación para darle una mejor experiencia al
usuario.

También hemos aprendido a crear un canal seguro mediante HTTPS y hemos


visto los dos tipos de certificados que podemos conseguir, es decir, los auto
firmados y los comprados.

Finalmente, hemos aprendido a sacarle el mejor provecho a nuestro procesador,


creando varias instancias de nuestra aplicación, al mismo tiempo que la hacemos
más robusta ante la caída de uno de sus nodos.

En este punto, hemos terminado por completo todas las unidades de este libro y
hemos aprendido a crear aplicaciones reactivas con React, NodeJS y MongoDB y
hemos aprendido desde cómo crear un Hello World en React, hasta crear toda
una API REST con persistencia en MongoDB.

Sin duda, creo que, tras finalizar este libro, no deberías de tener problemas para
iniciarte con un proyecto real, pues has aprendido todas las fases del desarrollo
de una aplicación con React, desde el FrontEnd hasta el BackEnd, incluso,
pasando por la persistencia con MongoDB y el pase a producción.

539 | Página
CONCLUSIONES
¡Felicidades! Si estas leyendo estas líneas, es por que seguramanete has
concluido de leer este libro, lo que te combierte en un programador Full Stack
capaz de desarrollar una aplicación completa utilizando React, NodeJS, Express
y MongoDB (Stack MERN).

Como ya te habras dado cuenta a lo largo de este libro, desarrollar una aplicación
completa, requiera de varias tecnologías, las cuales, por lo general son
enseñadas de forma independiente, dejando al programador la tarea de
investigar como unir todas las tecnologías para crear una solo aplicación
funcional.

El objetivo que me plante al momento de desarrollar este libro, es que el


estudiante pudiera desarrollar sus propias aplicaciones con MERN e incluso,
pueda acceder a mejores oportunidades de trabajo, por lo que si he logrado
cualquiera de esta dos cosas, me doy por satisfecho.

Recuerda que el mundo de las tecnologías es un ambiente muy dinámico, donde


lo que aprendemos hoy, probablemente mañana no sirve, por lo que te invito a
no dejar de aprender y seguir estudiando diariamente.

Finalmente, no me queda más que agradecerte por permitirme ser parte de tu


crecimiento profesional y por la confianza depositada en mí al momento de
adquirir este libro.

Si crees que este libro alcanzó tus expectativas y te fue de utilidad, te


invito a dejarnos tu reseña en
http://reactiveprogramming.io/react/comentarios y compartir con tus amigos
la referencia a este libro.

GRACIAS.

Página | 540

También podría gustarte