Documentos de Académico
Documentos de Profesional
Documentos de Cultura
ceronsanchezisai@gmail.com
Programación
Reactiva con React,
NodeJS & MongoDB
❝Se proactivo ante los cambios, pues una vez que llegan no tendrás tiempo para
reaccionar❞
Página | 2
Datos del autor:
Ciudad de México
e-mail: oscarblancarte3@gmail.com
Autor y Editor
Composición y redacción:
Edición:
Portada
Primera edición
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.
Página | 4
Otras obras del autor
5 | Página
É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://reactiveprogramming.io/books/es
Página | 6
Introducción a la arquitectura de software – Un enfoque
práctico
La realidad es que a medida que la tecnología avanza, tenemos cada vez más
herramientas a nuestra disposición, como lenguajes de programación, IDE’s,
editores de código, frameworks, librerías, plataformas en la nube y una gran
cantidad de herramientas que nos hacen la vida cada vez más simple, y por
increíble que parezca, los retos de hoy en día no son compilar el código, imprimir
una hoja, guardar en una base de datos; tareas que antes eran muy difíciles; lo
curioso es que hoy en día hay tantas alternativas para hacer cualquier cosa, que
por increíble que parezca, el reto de un programador hoy en día es decidirse por
qué tecnología irse, eso es increíble, tenemos tantas opciones para hacer lo que
sea, que el reto no es hacer las cosas, si no con que tecnologías la aremos.
Ahora bien, yo quiero hacerte una pregunta, ¿crees que hacer un programa hoy
en días es más fácil que hace años?
Seguramente a todos los que les haga esta pregunta concordarán con que hoy
en día es más fácil, sin embargo, y por increíble que parezca, el hecho de que las
tecnologías sean cada vez más simples, nos trae nuevas problemáticas y justo
aquí donde quería llegar.
A medida que las tecnologías son más simples y más accesibles para todas las
personas del mundo, las aplicaciones se enfrentan a retos que antes no existían,
como la concurrencia, la seguridad, la alta disponibilidad, el performance, la
usabilidad, la reusabilidad, testabilidad, funcionalidad, modificabilidad,
portabilidad, integridad, escalabilidad, etc, etc. Todos estos son conceptos que
prácticamente no existían en el pasado, porque las aplicaciones se desarrollaban
para una audiencia muy reducida y con altos conocimientos técnicos, además, se
ejecutaban en un Mainframe, lo que reducía drásticamente los problemas de
conectividad o intermitencia, pues todo se ejecuta desde el mismo servidor.
Adquiérelo en https://reactiveprogramming.io/books/es
7 | Página
Página | 8
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.
9 | 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 entregar un producto
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 un stack de más alto nivel.
Este libro pretende evitarte ese 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
home.
Página | 10
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:
Negritas:
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
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.
11 | Página
Por otra parte, tenemos los íconos, que nos ayudan para resaltar algunas cosas:
Tip
Importante
Error común
Documentación
Página | 12
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
13 | 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.
Página | 14
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.
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ó.
15 | 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.
Página | 16
Índice
Agradecimientos .................................................................................................................................... 9
Prefacio ............................................................................................................................................... 10
Requisitos previos................................................................................................................................ 14
INTRODUCCIÓN ................................................................................................................................... 15
Índice ................................................................................................................................................... 17
17 | Página
Introducción al desarrollo con React .................................................................................................... 64
Programación con JavaScript XML (JSX) ................................................................................................ 64
Diferencia entre JSX, HTML y XML .................................................................................................... 65
Contenido dinámico y condicional .................................................................................................... 72
JSX Control Statements ..................................................................................................................... 75
Transpilación ..................................................................................................................................... 79
Programación con JavaScript puro. ....................................................................................................... 80
Element Factorys .............................................................................................................................. 81
Element Factory Personalizados ....................................................................................................... 82
Resumen ................................................................................................................................................ 84
Página | 18
El enfoque Top-down & Bottom-up ..................................................................................................... 124
Top-down ........................................................................................................................................ 124
Bottom-up ....................................................................................................................................... 125
El enfoque utilizado y porque ......................................................................................................... 125
El API REST del proyecto Mini Twitter .................................................................................................. 126
Invocando el API REST desde React ..................................................................................................... 129
Mejorando la clase APIInvoker ....................................................................................................... 131
El componente TweetsContainer ......................................................................................................... 134
El componente Tweet .......................................................................................................................... 137
Paginando los Tweets .......................................................................................................................... 144
Resumen .............................................................................................................................................. 147
19 | Página
Function componentWillUnmount....................................................................................................... 194
Flujos de montado de un componente ................................................................................................ 195
Flujos de actualización ......................................................................................................................... 196
Flujos de desmontaje de un componente ............................................................................................ 197
Mini Twitter (Continuación 2) .............................................................................................................. 198
Configuración inicial ........................................................................................................................ 198
El componente TwitterApp ............................................................................................................. 199
El componente TwitterDashboard .................................................................................................. 203
El componente Profile .................................................................................................................... 205
El componente SuggestedUsers ..................................................................................................... 209
El componente Reply ...................................................................................................................... 213
Resumen .............................................................................................................................................. 230
Página | 20
Mini Twitter (Continuación 5) .............................................................................................................. 296
El componente TweetDetail............................................................................................................ 296
El componente Modal ..................................................................................................................... 299
Últimos retoques al proyecto ......................................................................................................... 305
Resumen .............................................................................................................................................. 307
Hooks................................................................................................................................................. 322
Introducción a los Hooks ...................................................................................................................... 326
Estado ............................................................................................................................................. 327
Ciclo de vida .................................................................................................................................... 330
Creando nuestros propios Hooks .................................................................................................... 334
Utilizando el Context con los Hooks ............................................................................................... 335
El futuro de las clases ...................................................................................................................... 336
Mini Twitter (Continuación 7) .............................................................................................................. 337
Migrando el componente Followers ............................................................................................... 337
Migrando el componente Followings ............................................................................................. 339
Migrando el componente SuggestedUser ...................................................................................... 340
Migrando el componente Toolbar .................................................................................................. 341
Migrando el componente TwitterDashboard ................................................................................. 343
Migrando el componente TwitterApp ............................................................................................ 343
Migrando el componente UserPage ............................................................................................... 346
Conclusiones ........................................................................................................................................ 352
21 | Página
Resumen .............................................................................................................................................. 394
Página | 22
Schemas del proyecto Mini Twitter ..................................................................................................... 465
Tweet Scheme................................................................................................................................. 465
Profile Scheme ................................................................................................................................ 466
Ejecutar operaciones básicas .......................................................................................................... 468
Resumen .............................................................................................................................................. 475
23 | Página
Producción vs desarrollo ...................................................................................................................... 571
Alta disponibilidad ............................................................................................................................... 572
Cluster ............................................................................................................................................. 572
Puertos ................................................................................................................................................. 577
Comunicación segura........................................................................................................................... 578
Certificados emitidos por autoridades ........................................................................................... 579
Certificados auto firmados .............................................................................................................. 580
Instalando un certificado en nuestro servidor ................................................................................ 581
Habilitar el modo producción .............................................................................................................. 585
Hosting y dominios .............................................................................................................................. 588
Resumen .............................................................................................................................................. 590
Página | 24
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 qué 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.
• Udemy
• Bitbucket
• Anypoint (MuleSoft)
• Facebook
• Courcera
• Airbnb
• American Express
• Atlassian
• Docker
• Dropbox
• Instagram
• Reddit
• Twitter
25 | Página
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
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.
Página | 26
Introducción a React
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, se 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.
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.
27 | Página
Server Side Apps vs Single Page Apps
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.
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.
Página | 28
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.
Si bien, React funciona originalmente como SPA, existe técnicas para realizar el
renderizado del lado del servidor, como es el caso de NextJS, un poderos
framework que permite el renderizado del lado del servidor, sin embargo, esto
querá fuera del alcance de este libro.
29 | Página
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 | 30
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.
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.
31 | Página
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.
Introducción a MongoDB
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.
Página | 32
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 NO requiere de realizar joins para
armar toda la información, pues un documento por sí solo, contiene toda la
información requerida, en pocas palabras, MongoDB permite grabar objetos
completos con todo y sus relaciones.
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:
33 | Página
La relación entre React, NodeJs & MongoDB
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.
Página | 34
Resumen
35 | Página
Preparando el ambiente de
desarrollo
Capítulo 2
Visual Studio Code es un editor de código Open Source lanzado por Microsoft en
abril del 2015, como una estrategia para competir contra los principales editores
de código que existen en el mercado, como es el caso de Atom, Brackets, Sublime
Text, entre otros.
Es importante notar la diferencia entre Visual Studio y Visual Studio Code, los
cuales tienen una diferencia enorme, por un lado, Visual Studio es un IDE de
paga con licenciamiento privado (también tiene una versión community) y que
fue desarrollado para dar soporte principalmente a las tecnologías Microsoft, por
otro lado, Visual Studio Code es un editor de código minimalista, Open Source y
multipropósito que se ha popularizado entre los desarrolladores web.
Cabe resaltar que, Visual Studio Code no es un IDE como tal, sino más bien, un
editor de código, 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 Visual Studio Code, debido a que
es una herramienta ampliamente utilizada y es open source, lo que te permitirá
descargarlo e instalarlo sin pagar una licencia. Sin embargo, si tú te sientes
cómodo en otro editor o IDE, eres libre de utilizarlo.
Página | 36
Instalación de Visual Studio Code
Instalar Visual Studio Code es tan simple como descargarlo de la página oficial.
Y existe una versión compatible para Windows, Linux y Mac, por lo que no
deberías de tener problemas con tu sistema operativo favorito.
La instalación tan simple, como seguir los clásicos pasos de siguiente, siguiente
y finalizar, por lo que no te aburriré haciendo un tutorial de como instalarlo.
• Windows: https://code.visualstudio.com/docs/setup/windows
• Mac: https://code.visualstudio.com/docs/setup/mac
• Linux: https://code.visualstudio.com/docs/setup/linux
37 | Página
Fig 10 - Pantalla de bienvenida de Visual Studio Code
Instalar PlugIns
Una de las principales ventajas de utilizar Visual Studio Code es la gran cantidad
de PlugIns disponibles por la comunidad, las cuales van desde soportar nuevos
lenguajes, hasta formatear el código. Es realmente sorprendente la gran cantidad
de plugins que nos podemos encontrar para hacer prácticamente cualquier cosa
que se nos ocurra.
Página | 38
Ya en esta nueva pantalla utilizaremos la barra de búsqueda que nos sale del
lado izquierdo para buscar los siguientes plugins:
Si todo salió bien, deberás de poder ver los plugins de la siguiente manera:
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.
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/
39 | Página
Para asegurarnos de que todo salió bien, deberemos entrar a la terminal de
comandos y ejecutar el comando “node -v” y “npm -v”, esto nos deberá arrojar
la versión de NodeJS instalada.
Esto será todo lo que tendremos que hacer por el momento con NodeJS. Más
adelante veremos cómo ejecutarlo y descargar módulos con NPM.
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.
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.
Página | 40
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:
41 | Página
• 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.
Página | 42
Fig 14 - 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. }
43 | Página
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, que 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
1. module.exports = {
2. mode: "development", //development
3. entry: [
4. __dirname + "/app/App.js",
5. ],
6. output: {
7. path: __dirname + "/public",
8. filename: "bundle.js",
9. publicPath: "/public"
10. },
11. module: {
12. rules: [{
13. test: /\.jsx?$/,
14. exclude: [/node_modules/],
15. loader: 'babel-loader',
16. options: {
17. presets: ["@babel/preset-env", "@babel/preset-react"],
18. plugins: [
19. "@babel/plugin-proposal-class-properties",
20. "@babel/plugin-proposal-export-default-from",
21. "react-hot-loader/babel"
22. ]
23. }
24. }]
25. }
Página | 44
26. };
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. "devDependencies": {
12. "@babel/cli": "^7.8.4",
13. "@babel/core": "^7.9.0",
14. "@babel/plugin-proposal-class-properties": "^7.8.3",
15. "@babel/plugin-proposal-export-default-from": "^7.8.3",
16. "@babel/preset-env": "^7.9.0",
17. "@babel/preset-react": "^7.9.4",
18. "babel-core": "^6.26.3",
19. "babel-eslint": "^10.1.0",
20. "babel-jest": "^25.2.6",
21. "babel-loader": "^8.1.0",
22. "react-hot-loader": "^4.12.20",
23. "webpack": "^4.42.1",
24. "webpack-cli": "^3.3.11",
25. "webpack-dev-middleware": "^1.10.2",
26. "webpack-dev-server": "^3.10.3"
27. },
28. "dependencies": {
29. "core-js": "^3.6.4",
30. "react": "^16.13.1",
31. "react-dom": "^16.13.1"
32. }
33. }
Una vez aplicados estos últimos cambios, será necesario ejecutar le comando npm
install sobre la carpeta del proyecto para descargar las dependencias.
45 | Página
Fig 15 - Instalando dependencias con npm install
Instalar actualizaciones
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
Página | 46
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
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:
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.
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:
47 | Página
1. {
2. "name": "twitter-app",
3. "version": "1.0.0",
4. "description": "Aplicación de redes sociales",
5. "main": "index.js",
6. "scripts": {
7. "start": "node_modules/.bin/webpack-dev-server --progress"
8. },
9. "author": "Oscar Blancarte",
10. "license": "ISC",
11. "devDependencies": {
12. "@babel/cli": "^7.8.4",
13. "@babel/core": "^7.9.0",
14. "@babel/plugin-proposal-class-properties": "^7.8.3",
15. "@babel/plugin-proposal-export-default-from": "^7.8.3",
16. "@babel/preset-env": "^7.9.0",
17. "@babel/preset-react": "^7.9.4",
18. "babel-core": "^6.26.3",
19. "babel-eslint": "^10.1.0",
20. "babel-jest": "^25.2.6",
21. "babel-loader": "^8.1.0",
22. "react-hot-loader": "^4.12.20",
23. "webpack": "^4.42.1",
24. "webpack-cli": "^3.3.11",
25. "webpack-dev-middleware": "^1.10.2",
26. "webpack-dev-server": "^3.10.3"
27. },
28. "dependencies": {
29. "core-js": "^3.6.4",
30. "react": "^16.13.1",
31. "react-dom": "^16.13.1"
32. }
33. }
La sección Script nos permitirá definir una serie de comandos pre definidos, para
compilar, ejecutar pruebas, empaquetar y ejecutar la aplicación. Estos scripts
son ejecutados con ayuda de npm.
Hello Word!!
Página | 48
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/.
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. Recuerda que
puedes bajar la versión del repositorio para comparar tu código.
Tras haber realizados todos los pasos, el proyecto debería de quedar con la
siguiente estructura:
49 | Página
Creación del proyecto con utilerías
En este caso, la aplicación se iniciará en el puerto 3000, por lo que la URL será
http://localhost:3000/.
Página | 50
La estructura del proyecto se vería de la siguiente manera:
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.
https://github.com/oscarjb1/books-reactiveprogramming.git
51 | Página
Gestión de dependencias con npm
Página | 52
Instalando librerías locales
Como ya lo hablamos, las librerías globales están disponibles para todos los
proyectos si la necesidad de instarlos en cada proyecto.
Librerías globales
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
53 | Página
no llevárnoslas a un ambiente de producción, pues puede hacer mucho más
pesada la página de lo que debería.
Librerías de desarrollo
Otra forma de ver las librerías de desarrollo es, verlas como librerías del Backend,
es decir, son librerías que seguramente utilizará node para ejecutar el API o
transpilar el código, pero no se llevarán al archivo bundle.js.
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. "@babel/cli": "^7.8.4",
3. "@babel/core": "^7.9.0",
4. "@babel/plugin-proposal-class-properties": "^7.8.3",
5. "@babel/plugin-proposal-export-default-from": "^7.8.3",
6. "@babel/preset-env": "^7.9.0",
7. "@babel/preset-react": "^7.9.4",
8. "babel-core": "^6.26.3",
9. "babel-eslint": "^10.1.0",
10. "babel-jest": "^25.2.6",
11. "babel-loader": "^8.1.0",
12. "react-hot-loader": "^4.12.20",
13. "webpack": "^4.42.1",
14. "webpack-cli": "^3.3.11",
15. "webpack-dev-middleware": "^1.10.2",
16. "webpack-dev-server": "^3.10.3"
17. }
Página | 54
• Babel: Todas las librerías que comienzan con @babel/xxx o babel-xxx
son las liberarías necesarias para hacer el proceso de transpilación, es
decir, convertir todos los archivos JavaScript escritos en JSX a un
JavaScript que el navegador pueda interpretar.
• Webpack: Todas las librerías que comienza con webpack-xxx son las
necesarias para crear el famoso archivo bundle.js a partir de los
archivos transpilados por Bebel.
• React-hot-loader: Esta librería habilita el Hot Deployment con Webpack,
el cual permite que todos los cambios realizados en el código se
actualicen en tiempo real en el navegador.
1. "dependencies": {
2. "core-js": "^3.6.4",
3. "react": "^16.13.1",
4. "react-dom": "^16.13.1"
5. }
Desinstalando librerías
55 | Página
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.
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.
Error común
Página | 56
Introducción a 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 7 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).
57 | Página
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.
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.
Sin este módulo, tendríamos que crear un servidor con NodeJS antes de poder
ejecutar un Hello World en React.
Página | 58
El archivo webpack.config.js
1. module.exports = {
2. mode: "development", //development
3. entry: [
4. __dirname + "/app/App.js",
5. ],
6. output: {
7. path: __dirname + "/public",
8. filename: "bundle.js",
9. publicPath: "/public"
10. },
11. module: {
12. rules: [{
13. test: /\.jsx?$/,
14. exclude: [/node_modules/],
15. loader: 'babel-loader',
16. options: {
17. presets: ["@babel/preset-env", "@babel/preset-react"],
18. plugins: [
19. "@babel/plugin-proposal-class-properties",
20. "@babel/plugin-proposal-export-default-from",
21. "react-hot-loader/babel"
22. ]
23. }
24. }]
25. }
26. };
59 | Página
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 > Rules: Permite definir las reglas de cómo será generado el
bundle.js, es decir, que archivos deberán ser tomas en cuenta, como
procesarlos y si hay algún plugin.
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: el loader corresponde al paquete que será utilizado para
procesar los archivos, en este caso, le indicamos que utilice babel para
el procesamiento de los archivos JavaScript.
o Presets y plugins: En su conjunto son librerías que permite realizar
la transpilación y agregar funcionalidad al lenguaje mediante plugins.
No entraré mucho en detalle sobre esta sección porque muy avanzada.
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/
Página | 60
desde Chrome https://chrome.google.com/webstore y en la barra de búsqueda
poner ‘react developer tools’, he instalamos el siguiente plugin:
Una vez instalado probablemente tengas que reiniciar el navegador. 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 azul. Esto nos indica que la página utiliza
React y es posible debugearla con el plugin.
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
Components:
61 | Página
Fig. 3 - Probando el plugin React developer tolos
Desde esta sección, es posible ver los componentes de 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 | 62
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.
63 | Página
Introducción al desarrollo con
React
Capítulo 3
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 | 64
Fig. 4 - Inspector de elementos de Google <h1>
Inspector de elementos
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
65 | Página
Elementos balanceados
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.
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:
Página | 66
Para corregir este error, es necesario cerrar el elemento, y lo podemos hacer de
dos formas:
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.
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:
67 | 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.
Página | 68
Fragments
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.
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.
Otra alternativa más simple es utilizar una par de etiquetas basias <></> como
podemo ver a continuación:
69 | Página
17. import { render } from 'react-dom'
18.
19. class App extends React.Component{
20.
21. render(){
22. return(
23. <>
24. <h1>Hello World</h1>
25. <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React
-icon.svg/210px-React-icon.svg.png"></img>
26. </>
27. )
28. }
29. }
30. render(<App/>, document.getElementById('root'));
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.
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>
Página | 70
4. <img src="https://facebook.github.io/react/img/logo.svg"/>
5. <br/>
6. <button onclick={()=>alert('Hello World')}>Hello!!</button>
7. </div>
8. )
71 | Página
Fig. 9 - Hello World con Camel Case
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 entre un par de llaves { }, tal como lo
vimos hace un momento con el evento onClick.
Veamos que la alerta está dentro de unas llaves, de lo contrario, React no sabrá
cómo interpretar el valor introducido.
Página | 72
Variables
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 lo interprete como código.
Valores condicionales
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.
73 | Página
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. }
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.
Página | 74
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.
Observa que antes de hacer el return tenemos un bloque donde podemos escribir
cualquier instrucción Javascript, por lo que podríamos hacer prácticamente
cualquier cosa, llamadas a métodos, declarar variables, etc.
Instalar jsx-control-statements
Primero que nada, será necesario instalar el módulo mediante npm con el
siguiente comando:
75 | Página
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.
1. module.exports = {
2. mode: "development", //development
3. entry: [
4. __dirname + "/app/App.js",
5. ],
6. output: {
7. path: __dirname + "/public",
8. filename: "bundle.js",
9. publicPath: "/public"
10. },
11. module: {
12. rules: [{
13. test: /\.jsx?$/,
14. exclude: [/node_modules/],
15. loader: 'babel-loader',
16. options: {
17. presets: ["@babel/preset-env", "@babel/preset-react"],
18. plugins: [
19. "@babel/plugin-proposal-class-properties",
20. "@babel/plugin-proposal-export-default-from",
21. "react-hot-loader/babel",
22. "module:jsx-control-statements"
23. ]
24. }
25. }]
26. }
27. }
Solo hemos agregado el plugin en la línea 22, 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
Página | 76
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",
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.
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/>
77 | Página
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>
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
Página | 78
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>.
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.
Transpilación
79 | Página
Babel, el cual toma los archivos en JSX y los convierte en JavaScript nativo, para
que, de esta forma, el navegador pueda comprenderlo y ejecutarlo.
1. module.exports = {
2. mode: "development", //development
3. entry: [
4. __dirname + "/app/App.js",
5. ],
6. output: {
7. path: __dirname + "/public",
8. filename: "bundle.js",
9. publicPath: "/public"
10. },
11. module: {
12. rules: [{
13. test: /\.jsx?$/,
14. exclude: [/node_modules/],
15. loader: 'babel-loader',
16. options: {
17. presets: ["@babel/preset-env", "@babel/preset-react"],
18. plugins: [
19. "@babel/plugin-proposal-class-properties",
20. "@babel/plugin-proposal-export-default-from",
21. "react-hot-loader/babel",
22. "module:jsx-control-statements"
23. ]
24. }
25. }]
26. }
27. }
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.
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.
Página | 80
Mediante JavaScript es posible crear elementos al vuelo mediante la función
React.createElement, la cual recibe 3 parámetros:
Veamos cómo quedaría la clase App habilitándola para usar JavaScript Nativo en
lugar de JSX:
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’.
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.
81 | Página
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>
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.
Página | 82
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.
Página | 84
Introducción a los Componentes
Capítulo 4
-- mozilla.org
85 | Página
Web Components es una tecnología
experimental
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.
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
Página | 86
que el navegador las pueda interpretar, con la gran diferencia que esto lo hace
del lado del cliente.
87 | Página
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.
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.
Este componte utiliza algo que hasta ahora no habíamos utilizado, las
propiedades (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).
Página | 88
Nuevo concepto: Propiedades (Props)
Para completar este ejemplo, modificaremos el componente App para que quede
de la siguiente manera:
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
89 | Página
del producto mediante la propiedad product, la cual podrá ser accedida por el
componente <ItemList> utilizando la instrucción this.props.product.
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.
Página | 90
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)}/>
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'));
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.
91 | Página
Fig. 16 - 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
Components:
Una vez en el tab Components, 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.
Página | 92
Nuevo concepto: Componentes con estado
Jerarquía de componentes
93 | Página
Un detalle super importante es que para que un componente pueda ser utilizado,
es necesario que se exporte (línea 15), de lo contrario no será posible utilizarlo
y marcar error al momento de querer utilizarlo.
Página | 94
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.
1. <ItemList product={item}/>
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.
95 | Página
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:
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:
Página | 96
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'));
Binding functions
97 | Página
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:
Página | 98
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
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.
99 | Página
Fig. 20 - Probando los shape propsTypes
Validaciones avanzadas
La siguiente tabla muestra todos los tipos de datos que es posible validar con
PropTypes.
Página | 100
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 }
101 | Página
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.
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
Página | 102
método getElementById de JavaScript. Mediante los Ref, es posible agregarles un
identificador único a los elementos para después accederlos fácilmente.
103 | Página
Veamos que al campo firstName le hemos agregado el atributo ref (línea 32), el
cual sirve para establecer la referencia al elemento, 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.
Por si no te quedo claro que está pasando en la línea 32, te explico, lo que
estamos haciendo con self => this.firstName = self, es definir un Arrow
function, la cual recibe como parámetro la variable self. Cuando React llame
esta arrow function, le mandará como parámetro el elemento que queremos
referenciar, es decir, al mismo elemento (de allí el nombre de la variable self),
luego en el cuerpo de la función, definimos que this.firtName = self, con esto,
estamos creado un variable a nivel de clase llamada firtName y como valor tendrá
self. Finalmente, cuando en la línea 15 queremos recuperar el elemento, nos
vamos sobre la variable de clase this.firtName la cual ya tiene la referencia
directa al elemento.
Puedes leer más sobre esté método de creación de referencias desde la página
oficial de React, sin embargo, yo solo lo menciono a modo de anécdota, ya que
es funcionalidad que no deberíamos de utilizar más.
Los hooks es uno de los últimos features que se agregaron a React, los cuales
permiten potenciar los componentes sin estado o de función, sin embargo, hemos
decidido explicar todo lo referente a los hooks en la unidad 13, donde
explicaremos todo con detalle. Dicho lo anterior, cuando trabajo con Hooks las
referencias se utilizan de otra forma, pero las analizaremos cuando llegues a esa
unidad.
Página | 104
Alcance los Refs
Por otro lado, las referencia mediante arrow function solo podrán ser accedidas
si tenemos la referencia a la variable a la cual se asignó el elemento, en otro
palabras, this.firtName solo podrá ser accedida por los elementos a los que se
las pase explícitamente esa referencia.
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>.
105 | Página
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} 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:
Como podemos ver en la imagen, nos está solicitando un key para cada elemento
de la lista
Página | 106
podemos utilizar alguna librería que nos genere un
UUID dinámico como la siguiente:
https://www.npmjs.com/package/uuid.
107 | Página
Las 4 formas de crear un Componente
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.
ECMAScript 6 - React.Component
Este es el método que hemos estado utilizando hasta ahora, en la cual los
componentes se cran mediante clases que extienden de React.Component. vemos
un ejemplo rápido:
Página | 108
13. )
14. }
15. }
16.
17. ECMAScript6Class.propTypes = {
18. ...
19. }
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.
109 | Página
ECMAScript 7 - React.Component
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.
Página | 110
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.
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.
111 | Página
Introducción al proyecto Mini
Twitter
Capítulo 5
Página | 112
Página de inicio
Perfil de usuario
113 | Página
Fig. 23 - Página de perfil de Mini Twitter
Página | 114
Página de seguidores
También es posible ver el detalle de cada Tweet, para ver los comentarios que
tiene y agregar nuevos.
115 | Página
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), desde la cual es posible autenticarse ante la aplicación mediante
usuario y password:
Mediante esta página, es posible crear una nueva cuenta para poder acceder a
la aplicación.
Página | 116
Fig. 28 - 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 que 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
componentes que vamos a requerir para crear la aplicación.
En esta sección analizaremos con mucho detalle todos los componentes que
conforman la aplicación Mini Twitter. Pero antes de eso, será interesante regresar
al diagrama donde se muestra la jerarquía de los componentes que conformará
el proyecto:
117 | Página
En las siguientes secciones comenzaremos a hablar sobre estos componentes,
por lo que tener esta imagen a la mano te servirá mucho para entender cómo es
que los componentes se van a ir creado y anidando.
Componente TwitterDashboard
Página | 118
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:
Adicional a los componentes que podemos ver en pantalla, existe uno más
llamado TwitterApp, el cual envuelve toda la aplicación, e incluyendo los demás
componentes, como las páginas de login, signup, y el perfil del usuario.
Componente TweetsContainer
119 | Página
Fig. 30 - Topología del componente TweetsContainer
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 | 120
Fig. 31 - 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:
Componente Signup
Este es el formulario para crear un nuevo usuario, el cual solo solicita datos
mínimos para crear un nuevo perfil.
121 | Página
Fig. 32 - Topología de la página Signup
Componente Login
Página | 122
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.
123 | Página
El enfoque Top-down & Bottom-up
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
Página | 124
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.
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.
125 | Página
El API REST del proyecto Mini Twitter
El proyecto Mini Twiter es una aplicación que la podemos dividir en dos partes,
la primera parte corresponde al Frontend, que es lo que hemos estado haciendo
con React, pero esta la otra parte, que corresponde al Backend, desde la cual
exponer un API REST que podrá utilizar nuestra aplicación.
Debido a que la parte del Backend la dejaremos para el final, es importante tener
un API que consumir en lo que llegamos hasta allí, por lo que de momento
consumiremos un API REST que he expuesto de forma pública en internet, el cual
es compartido entre todos los lectores y a medida que avancemos hasta el final
del libro, reemplazaremos el API de la nube por el API que nosotros mismo
construiremos.
https://minitwitterapi.reactiveprogramming.io/
Esto que estamos viendo ahora mismo, es la página de bienvenida del API REST
del proyecto Mini Twitter, así que tomate un minuto para leer para leer los
términos de uso antes de continuar.
Para ver los servicios que expone el API daremos click en el botón “Ver
documentación”, lo que nos llevará a la siguiente página:
Página | 126
Esta nueva página aporta muchísima información para entender lo que expone
el API, ya que nos dice el nombre y la descripción de cada servicio, así como el
método o verbo HTTP por el cual responde (verde), así como la URL en la cual
está disponible el servicio (azul). Finalmente, los servicios que están protegidos
con seguridad están marcado con una etiqueta roja que dice “secure”.
Además, de toda esta información que tenemos disponible de una forma simple
y clara, siempre podemos dar click en cualquier de los servicios para ver su
detalle.
127 | Página
En esta nueva página podemos ver toda la información anterior, más ejemplos
del formato esperado para la solicitud, un ejemplo de un request válido, una
respuesta exitosa y un ejemplo de una respuesta con error.
Por ahora te recomendaría que te tomaras unos minutos en navegar por el API
para que te familiarices un poco con los servicios expuestos, ya que a medida
que avancemos en el proyecto los iremos utilizando, sin embargo, cada vez que
necesitemos uno nuevo, lo mencionaré para que podamos analizarlo con más
detalle.
Finalmente, podemos probar que el API REST funcione al consultar los últimos
tweets, para eso, solo basta con abrir la siguiente URL en el navegador:
https://minitwitterapi.reactiveprogramming.io/tweets
Página | 128
Invocando el API REST desde React
Una vez que hemos analizado el API y los servicios disponibles, pasaremos a
explicar cómo podemos consumirlo desde React. Para esto, vamos a utilizar
fetch, la cual se utiliza para consumir cualquier recurso desde la WEB mediante
HTTP, por lo que es posible consumir servicios REST mediante el método POST,
GET, PUT, DELETE y PATCH, etc. 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.
1. class APIInvoker {
2.
3. invoke(url, okCallback, failCallback){
4. fetch(`https://minitwitterapi.reactiveprogramming.io${url}`)
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 (línea 3), que 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.
De estos cuatro parámetros solo dos son requeridos, 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 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
129 | Página
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.
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:
Página | 130
Finalmente, los Tweets retornados son mostrados en el método render con ayuda
de un <For> para iterar los resultados.
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:
1. module.exports = {
2. debugMode: false,
3. api: {
4. host: "https://minitwitterapi.reactiveprogramming.io"
5. },
6. tweets: {
7. maxTweetSize: 140
8. }
9. }
131 | Página
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. };
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.api.host}${url}`, params)
48. .then((response) => {
49. if(debug){
50. console.log("Invoke Response => " );
51. console.log(response);
52. }
53. return response.json()
54. })
55. .then((responseData) => {
56. if(responseData.ok){
57. okCallback(responseData)
58. }else{
59. failCallback(responseData)
60. }
61.
62. })
63. }
64. }
65. export default new APIInvoker();
Página | 132
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:
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.
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
propiedad, pues lo analizaremos más adelante cuando veamos la parte de
seguridad.
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 el constructor para que se vea de la siguiente manera:
1. constructor(){
2. . . .
3. APIInvoker.invokeGET('/tweets, response => {
4. this.setState({
5. tweets: response.body
6. })
7. },error => {
8. console.log("Error al cargar los Tweets", error);
9. })
10. }
Podemos apreciar que ahora las invocaciones indican el método HTTP que
estamos utilizando en cada llamada.
133 | Página
El componente TweetsContainer
Página | 134
Fig. 391 - Repaso a la estructura del proyecto.
135 | Página
52. }
53.
54. export default TweetsContainer;
Usuarios registrados
En las líneas 32 a 35 podemos ver cómo se Iteran todos los Tweets consultados
para mostrar el nombre de usuario del Tweet, el ID y el texto del Tweet.
Página | 136
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.
1. render() {
2. return (
3. <div className="container">
4. <TweetsContainer />
5. </div>
6. )
7. }
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.
137 | Página
Lo primero que haremos será crear un nuevo archivo llamado Tweet.js en el path
/app y lo dejaremos de la siguiente manera:
Página | 138
Este no es un libro HTML ni de CSS, por lo que no nos detendremos en explicar
para qué 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.
Con la función render ya nos podemos dar una idea de que este es un
componente más complejo, pues retorna un JSX con varios elementos, por lo que
vamos a desmenuzarlo por partes.
Entre las líneas 8 y 13 solo determinamos las clases de estilo que vamos a ponerle
al Tweet. Dependiendo si el Tweet es nuevo ('tweet fadeIn animated'), si ya
existía con anterioridad ('tweet') o si queremos ver su detalle ('tweet detaill'),
será la clase de estilo que pondremos.
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 16), de esta forma podremos identificar el Tweet más
adelante.
Las líneas 31 a 43 son las que muestran los iconos de like y compartir en la parte
inferior del Tweet. De momento no tendrá funcionalidad, pero más adelante
regresaremos para implementarla.
En la línea 45 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.
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. )
139 | Página
11. }
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:
Página | 140
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.
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{
141 | Página
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. }
Página | 142
Fig. 42 - 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
que retorna el API.
1. {
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
143 | Página
es correcto y el body encapsula todo el mensaje que nos retorna el API. Si
regresamos al componente TweetContainer en la línea 21 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.
Para lograr esto, es necesario dos cosas, la primera es que el API deberá soportar
consultar tweets en páginas, y la segunda, es modificar el componente
TweetsContainer para cargar más tweets a medida que descendemos en la
página.
Página | 144
1. import React from 'react'
2. import Tweet from './Tweet'
3. import APIInvoker from "./utils/APIInvoker"
4. import PropTypes from 'prop-types'
5. import InfiniteScroll from 'react-infinite-scroller'
6.
7. class TweetsContainer extends React.Component {
8. constructor(props) {
9. super(props)
10. this.state = {
11. hasMore: true,
12. tweets: []
13. }
14. this.loadMore = this.loadMore.bind(this)
15. }
16.
17. loadTweets(username, onlyUserTweet, page) {
18. let currentPage = page || 0
19. const url=`/tweets${onlyUserTweet ? "/" + username : ""}?page=${currentPage}`
20.
21. APIInvoker.invokeGET(url, response => {
22. this.setState({
23. tweets: this.state.tweets.concat(response.body),
24. hasMore: response.body.length >= 10
25. })
26. }, error => {
27. console.log("Error al cargar los Tweets", error);
28. })
29. }
30.
31. loadMore(page) {
32. const username = this.props.profile.userName
33. const onlyUserTweet = this.props.onlyUserTweet
34. this.loadTweets(username, onlyUserTweet, page - 1)
35. }
36.
37. render() {
38. console.log("tweets => ", this.state.tweets)
39. return (
40. <main className="twitter-panel">
41. <InfiniteScroll
42. pageStart={0}
43. loadMore={this.loadMore}
44. hasMore={this.state.hasMore}
45. loader={<div className="loader" key={0}>Loading ...</div>} >
46. <For each="tweet" of={this.state.tweets}>
47. <Tweet key={tweet._id} tweet={tweet} />
48. </For>
49. </InfiniteScroll>
50. </main>
51. )
52. }
53. }
54.
55. TweetsContainer.propTypes = {
56. onlyUserTweet: PropTypes.bool,
57. profile: PropTypes.object
58. }
59.
60. TweetsContainer.defaultProps = {
61. onlyUserTweet: false,
62. profile: {
63. userName: ""
64. }
65. }
66.
145 | Página
67. export default TweetsContainer
También hemos agregado el método loadMore, que será el que se ejecuta cada
vez que queramos consultar una nueva página, por lo tanto, recibe como
parámetro el número de página a consultar y luego delega la consulta al método
loadTweets.
Página | 146
Resumen
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.
147 | Página
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.
Página | 148
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.
1. {
2. editMode: false,
3. employee: {
4. name: "Juan Pérez",
5. age: 22,
6. tel: "12334567890",
7. birthdate: "10/10/1900"
8. }
9. }
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.
1. constructor(props){
2. super(props)
3. this.state = {
4. tweets: []
5. }
6. }
149 | Página
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.
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.
Página | 150
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
realizar cambios en el, de tal forma que los componentes puedan ser interactivos
y responder a las acciones del usuario.
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)
151 | Página
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)
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.
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:
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:
Página | 152
Fig. 46 - Object.assign con objetos anidados
La librería immutability-helper
Antes de empezar a trabajar con la librería, debemos instalarla con npm, para
ello, ejecutamos la siguiente instrucción:
npm install --save immutability-helper
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
153 | Página
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á.
Descripción
Operació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:
$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:
Página | 154
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.
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.
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
155 | Página
tener que actualizar la vista ante cada cambio, en su lugar, calcula todos los
cambios y los aplica en Batch al DOM real.
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.
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.
Página | 156
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
onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit onDragLeave
onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave onMouseMove
onMouseOut onMouseOver onMouseUp
Selection Events
onSelect
157 | Página
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 immutability-
helper para actualizar de forma correcta el estado.
Página | 158
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.
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.
159 | Página
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
para comprender esto. Crearemos un nuevo archivo llamado FormTest.js en el
path /app y lo dejaremos de la siguiente forma:
1. render(){
2. return (
3. <FormTest/>
4. )
5. }
Página | 160
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.
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:
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
161 | Página
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>
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>
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
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
Página | 162
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)}/>
6. <br/>
7. <input type="text" name="field2" defaultValue="Init Value 2" />
8. </div>
9. )
10. }
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
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:
163 | Página
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>
12. </div>
13. )
14. }
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
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.
Página | 164
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
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
165 | Página
usuario será redirigido al componente de login, el cual crearemos en la siguiente
sección.
En esta pantalla utilizaremos dos servicios del API REST, el primero nos ayudará
a validar que el nombre de usuario no esté 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 | 166
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{
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 id="usernameLabel"
58. ref={self => this.usernameLabel = self}
59. htmlFor="username"></label>
60.
61. <input type="text" value={this.state.name} placeholder="Nombre"
62. name="name" id="name" onChange={this.handleInput.bind(this)}/>
63. <label id="nameLabel" htmlFor="name"
64. ref={self => this.nameLabel = self}></label>
65.
66. <input type="password" id="passwordLabel"
67. value={this.state.password} placeholder="Contraseña"
68. name="password" onChange={this.handleInput.bind(this)}/>
69. <label ref={self => this.passwordLabel = self}
70. htmlFor="passwordLabel"></label>
71.
72. <input id="license" type="checkbox"
73. ref={self => this.license = self }
167 | Página
74. value={this.state.license} name="license"
75. onChange={this.handleInput.bind(this)} />
76. <label htmlFor="license" > Acepto los terminos de licencia</label>
77.
78. <button className="btn btn-primary btn-lg " id="submitBtn"
79. >Regístrate</button>
80. <label id="submitBtnLabel" htmlFor="submitBtn"
81. ref={self => this.submitBtnLabel = self}
82. className="shake animated hidden "></label>
83. <p className="bg-danger user-test">
84. Crea un usuario o usa el usuario
85. <strong>test/test</strong></p>
86. <p>¿Ya tienes cuenta? Iniciar sesión</p>
87. </form>
88. </div>
89. </div>
90. )
91. }
92. }
93. export default Signup;
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 desde el API REST) y license corresponde
al checkbox para aceptar los términos de licencia. Ambos deberán ser true para
permitir la creación del usuario.
Podemos observar que creamos un input de texto para nombre de usuario (línea
54) y para el nombre (línea 61), los cuales están ligados a su campo
correspondiente del estado this.state.username y this.state.name
respectivamente, para el password (línea 66) 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 72) 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.
Página | 168
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:
169 | Página
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:
Página | 170
Fig. 51 - 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.
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.usernameLabel.innerHTML = response.message
8. this.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.usernameLabel.innerHTML = error.message
15. this.usernameLabel.className = 'fadeIn animated fail'
16. })
17. }
171 | Página
Y agregaremos el evento onBlur al campo username:
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. <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
Página | 172
embargo, si el usuario decide presionar enter sobre algún campo en lugar del
botón, entonces el evento onSubmit procesará la solicitud.
1. signup(e){
2. e.preventDefault()
3.
4. if(!this.state.license){
5. this.submitBtnLabel.innerHTML =
6. 'Acepte los términos de licencia'
7. this.submitBtnLabel.className = 'shake animated'
8. return
9. }else if(!this.state.userOk){
10. this.submitBtnLabel.innerHTML =
11. 'Favor de revisar su nombre de usuario'
12. this.submitBtnLabel.className = ''
13. return
14. }
15.
16. this.submitBtnLabel.innerHTML = ''
17. this.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. alert('Usuario registrado correctamente');
27. },error => {
28. console.log("Error al cargar los Tweets");
29. this.submitBtnLabel.innerHTML = error.error
30. this.submitBtnLabel.className = 'shake animated'
31. })
32. }
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).
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.
173 | Página
si el servicio retorna con algún error (línea 27) entonces actualizamos la vista
con dicho error.
Librería de animación
El componente login
Página | 174
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.
175 | Página
26. this.setState(update(this.state,{
27. [field] : {$set: value}
28. }))
29. }
30.
31. login(e){
32. e.preventDefault()
33.
34. let request = {
35. "username": this.state.username,
36. "password": this.state.password
37. }
38.
39. APIInvoker.invokePOST('/login',request, response => {
40. window.localStorage.setItem("token", response.token)
41. window.localStorage.setItem("username", response.profile.userName)
42. window.location = '/'
43. },error => {
44. this.submitBtnLabel.innerHTML = error.message
45. this.submitBtnLabel.className = 'shake animated'
46. console.log("Error en la autenticación")
47. })
48. }
49.
50. render(){
51.
52. return(
53. <div id="signup">
54. <div className="container" >
55. <div className="row">
56. <div className="col-xs-12">
57. </div>
58. </div>
59. </div>
60. <div className="signup-form">
61. <form onSubmit={this.login.bind(this)}>
62. <h1>Iniciar sesión en Twitter</h1>
63.
64. <input type="text" value={this.state.username}
65. placeholder="usuario" name="username" id="username"
66. onChange={this.handleInput.bind(this)}/>
67. <label ref={self => this.usernameLabel = self} id="usernameLabel"
68. htmlFor="username"></label>
69.
70. <input type="password" id="passwordLabel"
71. value={this.state.password} placeholder="Contraseña"
72. name="password" onChange={this.handleInput.bind(this)}/>
73. <label ref={self => this.passwordLabel = self}
74. 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={self => this.submitBtnLabel = self}
79. id="submitBtnLabel" htmlFor="submitBtn"
80. className="shake animated hidden "></label>
81. <p className="bg-danger user-est">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>
86. </div>
87. )
88. }
89. }
90. export default Login
Página | 176
Nuevamente vamos a dividir la explicación en 3 secciones, el constructor, la
función render y login.
En la función render creamos el campo username (línea 64) 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.
177 | 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. }
Página | 178
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.
En las siguientes secciones analizaremos las rutas con React Router, con la
intención de poder ver un componente diferente para cada URL, por ejemplo
/login, /signup.
179 | Página
Resumen
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.
Página | 180
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 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.
181 | Página
En la imagen anterior puedes ver todos los métodos del ciclo de vida de los
componentes que funcionaban hasta la versión 16.2, donde podemos apreciar de
color verde los métodos que siguen vigentes hasta el día de hoy y en rojo los
métodos que se han desaconsejado su uso (deprecados). Si bien los métodos
deprecados siguen funcionando en todas las versiones de React 16.x, están
programados para ser removidos en la versión 17, por lo que hay que tener
cuidado de utilizarlos.
Hoy en día los métodos deprecados pueden seguir siendo utilizados con su
nombre original, sin embargo, siempre nos arrojará warnings en la consola, a
menos que renombremos los métodos con el prefijo UNSAFE_, por ejemplo
UNSAFE_componentWillMount, UNSAFE_componentWillReceiveProps y
UNSAFE_componentWillUpdate. Finalmente, se espera que para la versión 17 de
React, solo estén disponibles estos métodos con el prefijo UNSAFE_, por lo que
mi consejo es, no utilizar nunca más estos métodos en desarrollos nuevos, pues
están programados para ser removidos por completo en versiones posteriores.
Vamos a tratar de mencionar para que sirve todos estos métodos, incluso los
desaconsejados, con la intención de que si los ves en algún proyecto antiguo
sepas que hacen, pero no nos centraremos de lleno en entender como funcionaba
todo este ciclo de vida, pues consideramos que es algo que ya va de salida y no
vale la pena invertir mucho tiempo en ello.
Una vez que ya hablamos del ciclo de vida antiguo de React, pasaremos a la
nueva configuración del ciclo de vida que está disponible a partir de la versión
16.3:
Página | 182
Hemos tratado de ordenar los métodos del ciclo de vida según el orden en el que
se ejecutan, sin embargo, no siempre se ejecutarán todos los métodos o incluso,
habrá otros que se ejecuten más de una vez, por lo que la imagen anterior
representa el flujo de un componente que se monta, actualiza (una sola vez) y
finalmente de desmonta.
En las siguientes secciones explicaremos con detalle para que sirve cada método
de forma individual y seguido explicaremos como interviene cada método en los
3 escenarios posibles por los que puedes pasar un componente, es decir,
Montaje, Actualización y Desmontaje.
Dicho lo anterior, podemos ver que métodos del ciclo de vida se ejecutan en cada
etapa:
183 | Página
El Constructor
Antes que nada, me gustaría aclarar que el constructor no es como tal un método
del ciclo de vida, sin embargo, lo quise agregar debido a que a partir de la versión
16.3 toma un mayor protagonismo al ser deprecado el método
componentWillMount.
Página | 184
14. })
15. }
16.
17.
18. render() {
19. console.log("render =>")
20. return (
21. <h1>Hello World</h1>
22. )
23. }
24. }
25. render(<App />, document.getElementById('root'));
Lo primero que tenemos que tomar en cuenta es que el constructor recibe como
parámetro las propiedades, por lo que es indispensable agregar dicho parámetro.
Por otra parte, es super importante que la primera línea del cuerpo del
constructor mande llamar al constructor de la super clase mediante super(args),
de lo contrario, el componente no será inicializado correctamente y las props no
estarán disponibles en tiempo de ejecución.
Error común
1. render =>
2. API response =>
185 | Página
Importante
Function getDerivedStateFromProps
Observa que este es el único método del ciclo de vida que es estático, lo que
quiere decir que no tiene acceso a la instancia del componente, por lo tanto, todo
lo relacionado al DOM no estará disponible.
Importante
Página | 186
Function componentWillMount
(deprecated)
1. UNSAFE_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, esto quiere decir que si buscamos un
elemento en el DOM mediante document.getElementById(“id”), este no existirá
porque en este punto no ha sido enviado al DOM.
Importante
Importante
Function render
El método render() es el único de todos los métodos del ciclo de vida que es
obligatorio, pues es el que se encarga de definir como el componente deberá ser
renderizado en pantalla, por lo que como resultado deberá retornar cualquiera
de los siguientes valores:
187 | Página
• Elementos de React: Son elementos creados normalmente con ayuda
de JSX, como por ejemplo un <div/> o un componente <MyComponent/>.
• Arrays y fragmentos: Los fragmentos (<></>) ya los discutimos con
anterioridad, los cuales nos permiten retornar múltiples elementos si la
necesidad de retornar un elemento root concreto, por otro lado, es
posible retornar un array con múltiples elementos.
• Portales. Los portales proporcionan una opción de primera clase para
renderizar hijos en un nodo DOM que existe por fuera de la jerarquía del
DOM del componente padre. Puedes ver más de los portales en la
documentación oficial.
• String y números. Estos son renderizados como nodos de texto en el
DOM.
• Booleanos o nulos. No renderizan nada, pero es utilizado como
estrategia para impedir que el componente se muestre si alguna
condición no se cumple.
1. render(){
2. // Vars and logic sección
3. return (
4. //JSX section
5. )
6. }
La función render debe ser pura, lo que significa que no deberá modifica el estado
del componente, devuelve el mismo resultado cada vez que se invoca con los
mismos parámetros y no interactúa directamente con el navegador.
Error común
Error común
Página | 188
elementos no han sido renderizados en pantalla, por
lo tanto, no existen en el navegador.
Importante
Function getSnapshotBeforeUpdate
Dicho de otra forma, este método nos permite recuperar cierta información del
componente justo antes de que se actualice en pantalla, de esta forma, podemos
mandar lo que encontramos como parámetro al método componentDidUpdate para
hacer alguna acción.
1. getSnapshotBeforeUpdate(prevProps, prevState) {
2. if (prevProps.list.length < this.props.list.length) {
3. const list = this.listRef.current;
4. return list.scrollHeight - list.scrollTop;
5. }
6. return null;
7. }
8.
9. componentDidUpdate(prevProps, prevState, snapshot) {
10. if (snapshot !== null) {
11. const list = this.listRef.current;
12. list.scrollTop = list.scrollHeight - snapshot;
13. }
14. }
189 | Página
Este es quizás el método más complicado de entender y quizás el menos utilizado
de todos, por lo que es probable que no lo utilices en un largo tiempo, pero vale
la pena que al menos comprendas el concepto.
Importante
Function componentDidMount
1. componentDidMount(){
2. //Any action
3. }
Importante
Function componentWillReceiveProps
(deprecated)
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
Página | 190
actuales (this.props) para determinar si tenemos que realizar alguna acción para
actualizar el componente.
1. UNSAFE_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.
Importante
Importante
Function shouldComponentUpdate
191 | Página
le indica a React que el método render deberá ser ejecutado, en otro caso, el
método render será omitido y el componente no se actualizará.
1. shouldComponentUpdate(nextProps, nextState) {
2. // Any action
3. return boolean
4. }
Error común
Importante
Function forceUpdate
1. someMethod() {
2. // Forzar la actualización
3. this.forceUpdate();
4. }
Este método puede ser llamado desde cualquier parte del componente,
provocando que el método render se ejecuta, incluso si las propiedades y el
estado no cambiaron.
Página | 192
Importante
Function componentWillUpdate
(deprecated)
1. componentWillUpdate(nextProps, nextState) {
2. // Any action
3. }
Importante
Importante
193 | Página
Function componentDidUpdate
Este servicio se utiliza a menudo para hacer referencia a elementos del DOM una
vez que ya están disponibles, por otra parte, también es utilizado para consultar
recursos en la red, como es el caso del API, sin embargo, es importante siempre
validar el state y las props para asegurarse de que realmente algo cambio para
justificar la búsqueda en la red.
Importante
Function componentWillUnmount
1. componentWillUnmount() {
2. // Any action
3. }
Página | 194
Actualizar el estado en este método será en vano, pues el componente nunca
más será renderizado.
Importante
195 | Página
basado en las propiedades. Recordemos que este método regresa el nuevo
estado del componte o null para dejar el estado actual.
Flujos de actualización
Página | 196
Cuando React detecta cambios en el estado o las propiedades, lo primero que
hará React 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.
Entre que el método render termina y los cambios son reflejados en el navegador,
el método getSnapshotBeforeUpdate es ejecutado, con la intención de obtener
datos relevantes del DOM actual vs el nuevo DOM para pasar estos datos
relevantes al método componentDidUpdate como parámetros.
197 | Página
Mini Twitter (Continuación 2)
Configuración inicial
Este archivo nos permitirá controlar el historial del navegador para poder
redireccionar al usuario a diferentes páginas a medida que ciertos eventos
ocurran en la aplicación.
Página | 198
No te preocupes si no comprendes nada esta este momento, en la siguiente
sección regresaremos para explicar que está pasando aquí. Finalmente, podrás
ver que en este punto la aplicación aun no funciona, pues todavía faltan
componentes por desarrollar, los cuales veremos a continuación.
El componente TwitterApp
Para que este componente pueda determinar que página mostrar, es necesario
identificar si el usuario ya está autenticado, y una vez determinado eso, podrá
decidir si permite el acceso a la URL solicitad.
199 | Página
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. if(!this.state.load){
47. return null
48. }
49.
50. return (
51. <>
52. <div id="mainApp" className="aminate fadeIn">
53. <Switch>
54. <Route exact path="/" component={ () =>
55. <TweetsContainer profile={this.state.profile} />} />
56. <Route exact path="/signup" component={Signup} />
57. <Route exact path="/login" component={Login} />
58. </Switch>
59. <div id="dialog" />
60. </div>
61. </>
62. )
63. }
64. }
65. export default TwitterApp;
Lo primero que tenemos que observar es el estado inicial del componente que
definimos en el constructor, donde definimos la propiedad load y profile. La
propiedad load es de tipo booleano, lo que indica si ya se llevó a cabo la
comprobación de si el usuario está autenticado, y la propiedad profile guardar
el perfil del usuario solo cuando este ya está autenticado. Pongamos mucha
atención en estas dos propiedades, porque son clave para entender lo que viene
a continuación.
Una vez que el constructor termina, el método render es invocado, lo que implica
que React intentará renderiza el componente, sin embargo, podemos observar
en la línea 46 que tenemos una validación para comprobar la propiedad load, lo
que implica que retornaremos null si la propiedad load es false, es decir, que
no ha concluido la validación del usuario.
En esta misma unidad hablamos del método render y los posibles valores que
puede devolver, y mencionamos que entre unos de los valores válidos es null,
el cual utilizamos saltarnos el renderizado del componente, de esta forma
evitamos que React construya la vista.
Una vez que el método render finaliza, React ejecutará el siguiente método del
ciclo de vida definido, es decir componentDidMount. En este método intentaremos
comprobar si el usuario está autenticado, para esto, será necesario comprobar si
el usuario cuenta con un Token (hablaremos de los tokens más adelante), el cual
Página | 200
es una cadena alfanumérica que expide el API REST para comprobar que el
usuario está autenticado.
En este punto pueden suceder dos cosas: la primera es que el usuario no cuente
con un token (línea 21) y sea redireccionado a la página de login (página 26), la
segunda es que el usuario si cuenta con un token, por lo que tendremos que
mandar el token al API REST (línea 28) para validar su autenticidad. Existen dos
posibles respuestas del API, la primera, es que el token se validó, lo que
implicaría que el API nos regresaría el perfil del usuario y lo utilizaríamos para
actualizar el state (línea 29), actualizando la propiedad load a true y profile
con el perfil retornado por el API y luego guardaremos el token retornado en el
local storage (línea 33), por otra parte, si el token es invalido, borraremos el
token del local storage (línea 38) y redireccionaremos al usuario a la página de
login (línea 40).
Finalmente, podrás observar tres componentes Router, los cuales se activan solo
cuando la URL actual del navegador corresponde con la propiedad path,
renderizando el componente definido en la propiedad component.
201 | Página
Fig. 60 - Usuario no autenticado.
NOTA: Debido a que todavía no tenemos una forma de cerrar la sesión, solo
tenemos una forma de cerrar la sesión, que es ir al local storage del navegador
y borrar el token manualmente:
Página | 202
Para ir al local storage hay que abrir el inspector de elementos del navegador,
ubicar la pestaña Application, seleccionar el local storage, seleccionar el token
y presionar suprimir.
El componente TwitterDashboard
El siguiente paso consistirá mejorar la página de inicia, para no solo mostrar los
Tweets, si nos que nos muestre la barra de navegación en la parte superior, el
perfil del usuario autenticado, los tweets en el centro, y del lado derecho, una
lista de usuarios sugeridos para seguir, la idea es hacer que la página de inicio
se vea como la siguiente imagen:
203 | Página
Puede que esto te resulte confuso en este momento, pero veamos la siguiente
imagen para darnos una idea de los componentes que será necesario para lograr
el resultado esperado:
Página | 204
25. </div>
26. </div>
27. </div>
28. )
29. }
30.
31. TwitterDashboard.propTypes = {
32. profile: PropTypes.object.isRequired
33. }
34.
35. export default TwitterDashboard;
Tip
El componente Profile
205 | Página
Algo a tomar en cuenta es que, el perfil lo obtenemos desde que la aplicación
valida nuestro token, por lo que el perfil será enviado al componente Profile
como una prop. El mapeo de los campos se puede ver a continuación:
Página | 206
21. {props.profile.name}
22. </Link>
23.
24. <Link to={"/" + props.profile.userName}
25. className="profile-username">
26. @{props.profile.userName}
27. </Link>
28. </div>
29. <div className="profile-resumen">
30. <div className="container-fluid">
31. <div className="row">
32. <div className="col-xs-3">
33. <Link to={`/${props.profile.userName}`}>
34. <p className="profile-resumen-title">TWEETS</p>
35. <p className="profile-resumen-value">
36. {props.profile.tweetCount}</p>
37. </Link>
38. </div>
39. <div className="col-xs-4">
40. <Link to={`/${props.profile.userName}/following`}>
41. <p className="profile-resumen-title">SIGUIENDO</p>
42. <p className="profile-resumen-value">
43. {props.profile.following}</p>
44. </Link>
45. </div>
46. <div className="col-xs-5">
47. <Link to={`/${props.profile.userName}/followers`}>
48. <p className="profile-resumen-title">SEGUIDORES</p>
49. <p className="profile-resumen-value">
50. {props.profile.followers}</p>
51. </Link>
52. </div>
53. </div>
54. </div>
55. </div>
56. </aside>
57. )
58. }
59.
60. Profile.propTypes = {
61. profile: PropTypes.object.isRequired
62. }
63.
64. export default Profile;
207 | Página
NOTA: Podremos observar repetidas veces el componente <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, lo que da como resultado un simple link.
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.
Página | 208
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;
95. }
En este punto no es posible probar los cambios, sino hasta terminar el siguiente
componente.
El componente SuggestedUsers
209 | Página
Comenzaremos con crear el archivo llamado SuggestedUsers.js en el path /app,
el cual se deberá ver de la siguiente manera:
Página | 210
48. </For>
49. </If>
50. </aside>
51. )
52. }
53. }
54. export default SuggestedUser;
211 | Página
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. }
1. render() {
2. if(!this.state.load){
3. return null
4. }
5.
6. return (
7. <>
8. <div id="mainApp" className="aminate fadeIn">
9. <Switch>
10. <Route exact path="/" component={ () =>
11. <TwitterDashboard profile={this.state.profile} />} />
12. <Route exact path="/signup" component={Signup} />
13. <Route exact path="/login" component={Login} />
14. </Switch>
15. <div id="dialog" />
16. </div>
17. </>
18. )
19. }
Página | 212
Fig. 64 - Componente TwitterDashboard terminado.
El componente Reply
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. 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.
213 | Página
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.
Página | 214
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:
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
215 | Página
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.
También podemos ver que 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).
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:
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.
Página | 216
<input> y un <label> (líneas 43 a 54), esto debido a que los inputs de tipo file
se ven diferente en cada navegador:
Configuración
1. module.exports = {
2. debugMode: true,
3. server: { ... },
4. tweets: {
5. maxTweetSize: 140
6. }
7. }
217 | Página
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.
Página | 218
Los 3 componentes que tendrá eventos son:
• 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={self => this.reply = self}
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)
219 | Página
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
muestre el botón para cargar una imagen, el contador de caracteres y el botón
para envía el Tweet.
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.
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.reply.blur();
17. }
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.
Página | 220
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){
3. this.reset();
4. }
5. }
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)
221 | Página
17. }
Con ayuda de la función readAsDataURL (línea 16) del objeto FileReader iniciamos
la carga del archivo.
Control Button
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>
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. }
Página | 222
• _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.
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>
223 | Página
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á.
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
Página | 224
crear. Para terminar, llamamos al servicio /secure/tweet del API REST para crear
el Tweet.
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.
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.
225 | Página
Para concluir, no olvidemos realizar el import de la función 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:
Página | 226
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{
91. margin-right: 10px;
92. color: #333;
93. }
94.
95. input[type="file"]{
96. display: none;
97. }
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:
227 | Página
Fig. 70 - Reply componente terminado.
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.
Página | 228
Fig. 72 - Estructura actual del proyecto.
229 | Página
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.
Página | 230
React Routing
Capítulo 9
Cuando la WEB inicio, las URL no representaban nada más que una simple
dirección a un documento HTML, 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/oscarjblancarte/followers
Solo con ver esta URL puedo determinar que estoy en los seguidores (followers)
del usuario oscarjblancarte.
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 algo 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.
231 | Página
Pues bien, para no entrar mucho en detalles, React Router es el módulo de React
que nos ayuda a diseñar la navegación del usuario mediante las URL, utilizando
un concepto llamado Single Page Application, el cual analizaremos a
continuación.
Para comprender que es una SPA es importante analizar como las tecnologías
tradicionales muestran las páginas, en donde el navegador requiere hacer una
petición GET al servidor para recuperar la siguiente página y mostrarla. Durante
este proceso, el servidor determina cual es la página que debe de mostrar al
usuario, consulta la base de datos y luego crea la vista, como resultado, el
servidor responde con la página web (HTML) y los datos incrustados, de tal forma
que el navegador solo se limita a mostrar la página web retornada por el servidor.
Este proceso de consultar la página al servidor se conoce como Server Side
Render (SSE) o renderizado del lado del servidor.
Por otra parte, React crea páginas SPA, es decir, que cuando el usuario accede a
la aplicación, el servidor siempre regresará la misma página para todas las URL,
el cual consiste en un HTML con la referencia al archivo bundle.js. Cuando el
archivo bundle.js se carga, se ejecuta y crea la vista dinámicamente desde el
navegador, y cuando el usuario navega a otra página, este ya no va al servidor,
si no que el archivo bundle.js tiene la lógica para generar la siguiente página.
Página | 232
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.
El siguiente paso para definir las rutas, y aquí es donde nos podemos dar cuenta
de que vamos a necesitar una serie de instancias del componente Route, los
cuales están anidados de forma jerárquica, es decir, todos dentro de Router.
Cada Route tiene un atributo path, el cual indica en que URL se deberá mostrar
el componente definido en la propiedad componente. De esta forma, React Router
solo mostrará los componentes que hagan match el path actual del navegador, y
desmontará todos aquellos que no.
Si bien las rutas pueden ser estáticas (una URL fija que no cambia), pueden
existir situaciones donde necesitamos una URL que soporte una serie de valores.
Para comprender mejor esto, analicemos la URL /login (línea 9) y /signup (línea
10), está dos URL se pueden considerar estáticas, pues nunca cambiaran y ante
233 | Página
esa misma URL dará como resultado la misma página, sin embargo, existen
ocasiones donde necesitamos paths que soporten varios valores, como es el caso
de la página del perfil de los usuarios, en este sentido, podríamos tener URL como
las siguientes: /oscar, /juan o /rene, todas estas URL son diferentes, pero
deberían de mostrar la misma página aunque con datos diferentes y cómo
podemos tener cientos, miles o millones de usuarios, no sería viable crear una
ruta por cada usuario, por lo que en su lugar, se crear path con variables.
Los paths con variables permite indicarle a React Router que ciertas partes de la
URL en realidad son parámetros que deberían de ser propagados al componente
en cuestión, de tal forma que tenemos paths como /:user. Presta atención en
los dos puntos, pues con esto definimos que puede llegar un slash (/) seguido de
cualquier número de caracteres, y todos estos serán propagados al componente
como una propiedad con el mismo nombre.
Los path pueden ser expresiones muy complejas que no vamos a analizar en este
libro, ya que explicarlas podría hacer que nos salgamos demasiado del tema,
además, con lo que vamos a explicar en este libro será suficiente para más del
90% de los casos que se te podrían presentar, sin embargo, si quieres
profundizar en el tema, puedes ir a la documentación de la Path-ToRegExp, la
cual es el motor utilizado internamente por React Router para evaluar las rutas.
El otro atributo importante del componente Route es exact, ya que este permite
indicar que el path es exacto, lo que quiere decir que para que el path se active,
la URL deberá ser exacta y no bastará con que comiencen con el path, veamos
la siguiente tabla para comprender mejor:
En la tabla anterior puedes observar que un path se puede activas cuando la URL
del navegador (location.pathname) comienza con el path del Route (path). Es por
ello que marcamos las rutas como exactas, así solo se activarán solo cual la
coincidencia sea exacta. En pocas palabras, si no agregamos exact, le dice a
React Router que la ruta se activará si la URL comienza con el valor del path, por
otro lado, con exact, le decimos que el componente se activará solo cuando la
coincidencia entre el path y la URL sean exactas.
Switch
Un problema que se presenta con frecuencia es que dos o más rutas pueden
hacer match ante la misma URL, lo cual es válido, incluso, nos puede funcionar
si queremos que dos compontes diferentes se muestran ante la misma URL, sin
embargo, existen ocasiones donde esto puede ser un problema, ya que solo
necesitamos que se muestra la primera coincidencia y descargar el resto.
Página | 234
Un caso práctico de esto que te estoy hablando es el path /login y /signup contra
el path /:user, si recordamos, :user es una variable que puede tener cualquier
número de caracteres, por lo tanto, el path /login y /signup también activarían
este path, lo que diera como resultado que el componente del perfil del usuario
y el login se activaran al mismo tiempo, mostrando el formulario de login y justo
por debajo, el perfil del usuario.
1. render((
2. <Provider store={store}>
3. <Router history={history}>
4. <TwitterApp>
5. <Switch>
6. <Route exact path="/" component={Home} />
7. <Route exact path="/signup" component={Signup} />
8. <Route exact path="/login" component={Login} />
9. <Route exact path="/:user" component={UserPage} />
10. <Route exact path="/:user/:tab" component={UserPage} />
11. </Switch>
12. </TwitterApp>
13. </Router>
14. </Provider>
15. ), document.getElementById('root'));
Error común
Link
Cuando trabajamos con React Router, hay que tener especial cuidado en la forma
en que creamos los enlaces (<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, 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
actúa exactamente como la etiqueta <a>, pero esta tiene la ventaja de que no
lanza un request al servidor, si no que solo actualiza la URL del navegador pero
sin hacer una llamada al servidor, lo que provoca que las nuevas rutas sean
activadas.
235 | Página
Link se renderiza como una etiqueta <a> al memento de mostrarse en el
navegador, sin embargo remplaza su funcionamiento estándar por el mencionado
anteriormente.
1. <Link to={"/login"}>
2. //Any element
3. </Link>
NOTA: Debemos siempre procurar utilizar Link para la navegación del usuario,
sin embargo, este solo sirve para URL relativas a nuestro dominio, por lo que si
queremos llevar al usuario fuera de nuestra página, entonces podemos utilizar la
clásica etiqueta <a>.
NavLink
Página | 236
Redirect
Redirect solo acepta la propiedad to, la cual deberá indicar la URL a la cual
queremos redireccionar el usuario, pero tomemos en cuenta que la URL que
pongamos deberá ser dentro de nuestro mismo sitio web.
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.
237 | Página
1. <Route path=":user" 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.
Página | 238
Mini Twitter (Continuación 3)
Error común
239 | Página
22. <AuthRouter isLoged={this.state.profile != null} exact path="/"
23. component={() =>
24. <TwitterDashboard profile={this.state.profile} />} />
25. <Route exact path="/signup" component={Signup} />
26. <AuthRouter isLoged={this.state.profile != null}
27. exact path="/login" component={Login} />
28. </Switch>
29. <div id="dialog" />
30. </div>
31. )
32. }
33. }
34. export default TwitterApp;
Creo que con la explicación anterior, nos debería de quedar más claro lo que está
pasando entre las líneas 21 y 28, con la única duda quizás de lo que está pasando
en la línea 22, por lo que vamos explicar esto un poco mejor. El atributo
component de Route acepta un componente o una función que retorne un
componente, en este sentido, definimos una arrow function que retorna el
componente TwitterDashboard, esto lo hacemos así porque necesitamos pasarle
el profile como prop.
El componente AuthRouter
Debido a que habrá algunas rutas que solo se podrá tener acceso cuando estemos
autenticados, será necesario crear un componente que nos ayude a redireccionar
a la página de login a todos los usuarios que intenten entra a estas páginas y
que no estén autenticados, es por ello que crearemos el componente
AuthRouter.js en el path /app.
Página | 240
11. : <Redirect to='/login' />
12. )} />)
13. }
14. }
15.
16. export default AuthRoute
Este componente puede llegar a resultar algo confuso, sobre todo por que
utilizamos algunas cosas que no vemos normalmente, pero quiero hacerte un
resumen antes de entrar a los detalles.
El componente Toolbar
Iniciaremos con la creación del archivo Toolbar.js en el path /app, el cual deberá
tener la siguiente estructura:
241 | Página
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. <Link className="navbar-brand" to="/">
31. <i className="fa fa-twitter" aria-hidden="true"/>
32. </Link>
33. <ul id="menu">
34. <li id="tbHome" className="selected">
35. <Link to="/">
36. <p className="menu-item">
37. <i className="fa fa-home menu-item-icon"
38. aria-hidden="true" />
39. <span className="hidden-xs hidden-sm">
40. Inicio</span>
41. </p>
42. </Link>
43. </li>
44. </ul>
45. </div>
46. <If condition={this.props.profile != null} >
47. <ul className="nav navbar-nav navbar-right">
48. <li className="dropdown">
49. <a href="#" className="dropdown-toggle"
50. data-toggle="dropdown" role="button"
51. aria-haspopup="true"
52. aria-expanded="false">
53. <img className="navbar-avatar"
54. src={this.props.profile.avatar}
55. alt={this.props.profile.userName}/>
56. </a>
57. <ul className="dropdown-menu">
58. <li>
59. <Link to={`/${this.props.profile.userName}`}>
60. Ver perfil</Link>
61. </li>
62. <li role="separator" className="divider"></li>
63. <li>
64. <Link to="#" onClick={this.logout.bind(this)}>
65. Cerrar sesión</Link>
66. </li>
67. </ul>
68. </li>
69. </ul>
70. </If>
71. </div>
72. </div>
73. </nav>
74. )
75. }
76. }
77.
78. Toolbar.propTypes = {
79. profile: PropTypes.object
80. }
81.
82. export default Toolbar
Página | 242
Este componte es sin duda uno de los más simples, pues en realidad solo muestra
un botón de inicio (línea 35) la cual regresará al usuario al inicio de la aplicación
( / ).
Por otra parte, mostramos una foto del avatar del usuario (línea 50), que al
presionarse, arrojará un menú con dos opciones, la primera nos lleva al nuestro
perfil de usuario (/:user) (línea 59), y la segunda opción es cerrar la sesión (línea
64). La opción de cerrar sesión detonará en la ejecución de la función logout.
1. return (
2. <>
3. <Toolbar profile={this.state.profile} />
4. <div id="mainApp" className="aminate fadeIn">
5. <Switch>
6. <Route exact path="/" component={() =>
7. <TwitterDashboard profile={this.state.profile} />} />
8. <Route exact path="/signup" component={Signup} />
9. <Route exact path="/login" component={Login} />
10. </Switch>
11. <div id="dialog" />
12. </div>
13. </>
14. )
243 | Página
15. left: 50%;
16. transform: translateX(50%);
17. }
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{
Página | 244
81. color: #1DA1F2;
82. }
83.
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. }
245 | Página
Fig. 73 - Componente Toolbar terminado.
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
Login.js y modificaremos la función render para dejarla de la siguiente manera:
1. render(){
2. return(
3. <div id="signup">
4. <div className="container" >
5. <div className="row">
6. <div className="col-xs-12">
7. </div>
8. </div>
9. </div>
10. <div className="signup-form">
11. <form onSubmit={this.login.bind(this)}>
12. <h1>Iniciar sesión en Twitter</h1>
13.
14. <input type="text" value={this.state.username}
15. placeholder="usuario" name="username" id="username"
16. onChange={this.handleInput.bind(this)}/>
17. <label ref="usernameLabel" id="usernameLabel"
18. htmlFor="username"></label>
19.
20. <input type="password" id="passwordLabel"
21. value={this.state.password} placeholder="Contraseña"
22. name="password" onChange={this.handleInput.bind(this)}/>
23. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
24.
25. <button className="btn btn-primary btn-lg " id="submitBtn"
26. onClick={this.login.bind(this)}>Regístrate</button>
27. <label ref="submitBtnLabel" id="submitBtnLabel" htmlFor="submitBtn"
28. className="shake animated hidden "></label>
29. <p className="bg-danger user-test">Crea un usuario o usa el usuario
30. <strong>test/test</strong></p>
31. <p>¿No tienes una cuenta? <Link to="/signup">Registrate</Link> </p>
32. </form>
33. </div>
34. </div>
Página | 246
35. )
36. }
En el caso del componente Signup pasa algo similar al componente Login, pues
en este tendremos que agregar un Link 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. return (
3. <div id="signup">
4. <div className="container" >
5. <div className="row">
6. <div className="col-xs-12">
7. </div>
8. </div>
9. </div>
10. <div className="signup-form">
247 | Página
11. <form onSubmit={this.signup.bind(this)}>
12. <h1>Únete hoy a Twitter</h1>
13. <input type="text" value={this.state.username}
14. placeholder="@usuario" name="username" id="username"
15. onBlur={this.validateUser.bind(this)}
16. onChange={this.handleInput.bind(this)}/>
17. <label ref="usernameLabel" id="usernameLabel"
18. htmlFor="username"></label>
19.
20. <input type="text" value={this.state.name} placeholder="Nombre"
21. name="name" id="name" onChange={this.handleInput.bind(this)}/>
22. <label ref="nameLabel" id="nameLabel" htmlFor="name"></label>
23.
24. <input type="password" id="passwordLabel"
25. value={this.state.password} placeholder="Contraseña"
26. name="password" onChange={this.handleInput.bind(this)}/>
27. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
28.
29. <input id="license" type="checkbox" ref="license"
30. value={this.state.license} name="license"
31. onChange={this.handleInput.bind(this)} />
32. <label htmlFor="license" >Acepto los terminos de licencia</label>
33.
34. <button className="btn btn-primary btn-lg " id="submitBtn"
35. onClick={this.signup.bind(this)}>Regístrate</button>
36. <label ref="submitBtnLabel" id="submitBtnLabel" htmlFor="submitBtn"
37. className="shake animated hidden "></label>
38. <p className="bg-danger user-test">Crea un usuario o usa el usuario
39. <strong>test/test</strong></p>
40. <p>¿Ya tienes cuenta? <Link to="/login">Iniciar sesión</Link> </p>
41. </form>
42. </div>
43. </div>
44. )
45. }
Página | 248
Fig. 75 - Nuevo link para iniciar sesión.
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. },error => {
28. console.log("Error al cargar los Tweets");
29. this.refs.submitBtnLabel.innerHTML = response.error
30. this.refs.submitBtnLabel.className = 'shake animated'
249 | Página
31. })
32. }
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
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
Página | 250
Fig. 76 - Apariencia final del componente UserPage terminado.
251 | Página
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. componentDidMount() {
23. let user = this.props.match.params.user
24.
25. APIInvoker.invokeGET('/profile/' + user, response => {
26. this.setState({
27. edit: false,
28. profile: response.body
29. });
30. }, error => {
31. console.log("Error al cargar los Tweets");
32. window.location = '/'
33. })
34. }
35.
36. follow(e) {
37. let request = {
38. followingUser: this.props.match.params.user
39. }
40. APIInvoker.invokePOST('/secure/follow', request, response => {
41. if (response.ok) {
42. this.setState(update(this.state, {
43. profile: {
44. follow: { $set: !response.unfollow }
45. }
46. }))
47. }
48. }, error => {
49. console.log("Error al actualizar el perfil");
50. })
51. }
52.
53. render() {
54. let profile = this.state.profile
55. let storageUserName = window.localStorage.getItem("username")
56.
57. let bannerStyle = {
58. backgroundImage: 'url(' + (profile.banner) + ')'
59. }
60.
61. return (
62. <div id="user-page" className="app-container">
63. <header className="user-header">
64. <div className="user-banner" style={bannerStyle}>
65. </div>
66. <div className="user-summary">
67. <div className="container-fluid">
68. <div className="row">
69. <div className="hidden-xs col-sm-4 col-md-push-1
70. col-md-3 col-lg-push-1 col-lg-3" >
71. </div>
Página | 252
72. <div className="col-xs-12 col-sm-8 col-md-push-1
73. col-md-7 col-lg-push-1 col-lg-7">
74. <ul className="user-summary-menu">
75. <li>
76. <NavLink to={`/${profile.userName}`}
77. activeClassName="selected">
78. <p className="summary-label">TWEETS</p>
79. <p className="summary-value">{profile.tweetCount}</p>
80. </NavLink>
81. </li>
82. <li>
83. <NavLink to={`/${profile.userName}/following`}
84. activeClassName="selected">
85. <p className="summary-label">SIGUIENDO</p>
86. <p className="summary-value">{profile.following}</p>
87. </NavLink>
88. </li>
89. <li>
90. <NavLink to={`/${profile.userName}/followers`}
91. activeClassName="selected">
92. <p className="summary-label">SEGUIDORES</p>
93. <p className="summary-value">{profile.followers}</p>
94. </NavLink>
95. </li>
96. </ul>
97.
98. <If condition={profile.follow != null &&
99. profile.userName !== storageUserName} >
100. <button className="btn edit-button"
101. onClick={this.follow.bind(this)} >
102. {profile.follow
103. ? (<span><i className="fa fa-user-times"
104. aria-hidden="true"></i> Siguiendo</span>)
105. : (<span><i className="fa fa-user-plus"
106. aria-hidden="true"></i> Seguir</span>)
107. }
108. </button>
109. </If>
110. </div>
111. </div>
112. </div>
113. </div>
114. </header>
115. <div className="container-fluid">
116. <div className="row">
117. <div className="hidden-xs col-sm-4 col-md-push-1 col-md-3
118. col-lg-push-1 col-lg-3" >
119. <aside id="user-info">
120. <div className="user-avatar">
121. <div className="avatar-box">
122. <img src={profile.avatar} />
123. </div>
124. </div>
125. <div>
126. <p className="user-info-name">{profile.name}</p>
127. <p className="user-info-username">@{profile.userName}</p>
128. <p className="user-info-description">
129. {profile.description}</p>
130. </div>
131. </aside>
132. </div>
133. <div className="col-xs-12 col-sm-8 col-md-7
134. col-md-push-1 col-lg-7">
135. </div>
136. </div>
137. </div>
253 | Página
138. </div>
139. )
140. }
141. }
142. export default UserPage
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,
estaremos haciendo una update. Por ahora estaremos utilizando el método GET,
pues solo requerimos consultar los datos del usuario (línea 25) para mostrarlos
en pantalla. Si la consulta termina correctamente, actualizamos el estado con el
nuevo perfil (línea 26); mientras que, si el perfil no se encuentra, regresamos al
usuario a la pantalla de inicio (/) (línea 32).
Página | 254
background, el fondo es puesto por medio de estilos, que definimos previamente
en variable bannerStyle (línea 57).
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:
Este botón mandará llamar la función follow (línea 36), 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 aplicar la operación
correspondiente.
255 | Página
Documentación: Seguir o dejar de seguir a un
usuario
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:
Página | 256
18. <Switch>
19. <Route exact path="/" component={() =>
20. <TwitterDashboard profile={this.state.profile} />} />
21. <Route exact path="/signup" component={Signup} />
22. <Route exact path="/login" component={Login} />
23. <Route exact path="/:user" component={UserPage} />
24. </Switch>
25. <div id="dialog" />
26. </div>
27. </>
28. )
29. }
30. }
31. export default TwitterApp
Hemos agregado el Route con el path (/:user) para atender las peticiones /{user}
y lo hemos ligado al componente UserPage (línea 23), así como hemos agregado
el import correspondiente de este nuevo componente (línea 1).
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:
257 | Página
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%;
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. }
Página | 258
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;
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 a.selected::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.
259 | Página
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.
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) {
Página | 260
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;
259. }
260. }
261 | Página
Edit mode
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>
Página | 262
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.
263 | Página
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 13 dentro
del <div> que contiene el banner. Esto habilitará que el banner permita cambiar
la imagen mediante un click.
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. }
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.
Página | 264
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.
1. <div className="avatar-box">
2. <img src={profile.avatar} />
3. </div>
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á la imagen (<img>) que ya teníamos (línea 19 a 21). 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.
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>
265 | Página
4. <p className="user-info-description">
5. {profile.description}</p>
6. </div>
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 y un textarea en caso
de estar en edición y respeta el funcionamiento anterior en caso de estar en
modo solo lectura.
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 | 266
1. <If condition={profile.follow != null &&
2. profile.userName !== storageUserName} >
3. <button className="btn edit-button"
4. onClick={this.follow.bind(this)} >
5. {profile.follow
6. ? (<span><i className="fa fa-user-times"
7. aria-hidden="true"></i> Siguiendo</span>)
8. : (<span><i className="fa fa-user-plus"
9. aria-hidden="true"></i> Seguir</span>)
10. }
11. </button>
12. </If>
13.
14. <If condition= {this.state.edit}>
15. <button className="btn edit-button" onClick=
16. {this.cancelEditMode.bind(this)} >Cancelar</button>
17. </If>
Este fragmento de código deberá quedar después de terminar el bloque <If> para
agregar el botón Siguiendo/Seguir.
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 solo guardamos los
cambios, actualiza el navegador y podremos empezar a editar nuestro perfil.
267 | Página
Fig. 82 - Estado actual del componente UserPage.
El componente MyTweets
Página | 268
Fig. 83 - MyTweets component.
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
269 | Página
aprendido, por lo que explicaré rápidamente las partes claves y sin entrar en los
detalles.
1. .tweet-container-header{
2. padding: 10px;
3. font-size: 19px;
4. }
Página | 270
Fig. 84 - MyTweets integrado con UserPage.
1. componentDidMount() {
2. let user = this.props.match && this.props.match.params.user
3. this.getUserProfile(user)
4. }
271 | Página
5.
6. componentDidUpdate(prevProps, prevState, snapshot) {
7. let newProfile = this.props.match.params.user
8. let prevProfile = prevProps.match.params.user
9. if (newProfile != prevProfile) {
10. this.getUserProfile(newProfile)
11. }
12. }
13.
14. getUserProfile(user) {
15. APIInvoker.invokeGET('/profile/' + user, response => {
16. this.setState({
17. edit: false,
18. profile: response.body
19. });
20. }, error => {
21. console.log("Error al cargar los Tweets");
22. window.location = '/'
23. })
24. }
Página | 272
Actualización de los componentes
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.
Algo que nos quedó pendiente cuando creamos el componente Tweet fue habilitar
los Links del usuario que creo el tweet, para que de esta forma cualquier usuario
pueda ver ir a su perfil dando click en su nombre o nombre de usuario.
1. return (
2. <article className={tweetClass} id={"tweet-" + this.state._id}>
3. <img src={this.state._creator.avatar} className="tweet-avatar" />
4. <div className="tweet-body">
5. <div className="tweet-user">
6. <Link to={`/${this.state._creator.userName}`}>
7. <span className="tweet-name" data-ignore-onclick>
8. {this.state._creator.name}</span>
9. </Link>
10. <Link to={`/${this.state._creator.userName}`}>
11. <span className="tweet-username">
12. @{this.state._creator.userName}</span>
13. </Link>
14. </div>
15. <p className="tweet-message">{this.state.message}</p>
16. <If condition={this.state.image != null}>
17. <img className="tweet-img" src={this.state.image} />
18. </If>
19. <div className="tweet-footer">
20. <a className={this.state.liked ? 'like-icon liked' : 'like-icon'}
21. data-ignore-onclick>
22. <i className="fa fa-heart " aria-hidden="true"
23. data-ignore-onclick></i> {this.state.likeCounter}
24. </a>
25. <If condition={!this.props.detail} >
26. <a className="reply-icon" data-ignore-onclick>
27. <i className="fa fa-reply " aria-hidden="true"
28. data-ignore-onclick></i> {this.state.replys}
29. </a>
30. </If>
31. </div>
32. </div>
273 | Página
33. <div id={"tweet-detail-" + this.state._id} />
34. </article>
35. )
Con este cambio podremos ver que los Tweets ahora te pueden llevar al perfil
del usuario con tan solo dar click sobre su nombre.
Página | 274
Resumen
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 los 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 editarlo.
275 | 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 vea bien, tenga algunos efectos o
animaciones que las haga más agradable a la vista, como lo son las animaciones
y las transacciones.
1. .card{
2. background-color: black;
3. transition: background-color 500ms;
4. }
5.
6. .card:hover{
7. background-color: blue;
Página | 276
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:
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.
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. }
277 | 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.
1. .card {
2. animation: card-animation 5s
3. }
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 | 278
Introducción a CSSTranstion
Seguramente te estarás preguntando qué tiene que ver las transiciones y las
animaciones con CSSTransaction, 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>
• transitionAppear
• transitionEnter
• transitionLeave
279 | 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.
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. }
Página | 280
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. }
Cabe resaltar no es requerido definir los estilos para todas las transiciones, si no
solo las que habilitemos desde el componente CSSTransactionGroup.
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 cómo
se ve este componente:
281 | Página
Fig. 87 - UserCard component.
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
Página | 282
que nada, observemos que el componente recibe como prop el objeto user (línea
5), el cual está marcado como obligatorio en los PropTypes.
Para concluir, solo faltaría agregar las clases de estilo correspondientes al archivo
styles.css:
283 | Página
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 eso, vamos a crear el archivo Followings.js en el path /app, el cual deberá
tener la siguiente estructura:
Página | 284
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({
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;
285 | Página
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.
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.
Página | 286
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.
Debido a que ahora podemos mostrar los tweets, los seguidores o las personas
que nos siguen, ya no basta con tan solo poner un Route para cada componte,
sino a que además, es necesario agregar un Switch para asegurarnos de que solo
un componente se muestra a la vez.
El componente Followers
287 | Página
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){
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;
Página | 288
Documentación: Consulta de seguidores
1. return (
2. <>
3. <Toolbar profile={this.state.profile} />
4. <div id="mainApp" className="animated fadeIn">
5. <Switch>
6. <Route exact path="/" component={() =>
7. <TwitterDashboard profile={this.state.profile} />} />
8. <Route exact path="/signup" component={Signup} />
9. <Route exact path="/login" component={Login} />
10. <Route exact path="/:user" component={UserPage} />
11. </Switch>
12. <div id="dialog" />
13. </div>
14. </>
15. )
289 | Página
Para solucionar este problema basta con quitar el atributo exact, con lo cual todas
las rutas que comiencen con /:user serán válida y el componente se montará.
1. return (
2. <>
3. <Toolbar profile={this.state.profile} />
4. <div id="mainApp" className="animated fadeIn">
5. <Switch>
6. <Route exact path="/" component={() =>
7. <TwitterDashboard profile={this.state.profile} />} />
8. <Route exact path="/signup" component={Signup} />
9. <Route exact path="/login" component={Login} />
10. <Route path="/:user" component={UserPage} />
11. </Switch>
12. <div id="dialog" />
13. </div>
14. </>
15. )
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.
Página | 290
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.
291 | Página
Componentes modales
Capítulo 11
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)
Página | 292
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.
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 componentes, 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.
293 | Página
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:
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 superior
al resto de los elementos. Esta propiedad moverá nuestro elemento en el eje Z,
es decir lo traerá hasta el frente.
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.
Página | 294
React Portal
Durante todo este libro hemos hablado y explicado que React organiza los
componentes en forma jerárquica, donde un componente padre se compone de
otros componentes hijos y así sucesivamente, de tal forma que para que un
componente sea hijo de otro, debe de de ser un descendiente directo del padre,
sin embargo, existe ocasiones donde necesitamos que un componente hijo sea
montado sobre un nodo del DOM que está fuera de la jerarquía.
Un caso típico de esto son los modales, pues por lo general utilizamos un
elemento que está en lo más alto de la jerarquía del DOM para poder hacer que
se sobre ponga al resto de los elementos y es justo aquí donde entran los
portales.
Debido a que vamos a estar utilizando un Portal para seguir avanzado en nuestro
proyecto, vamos a explicar cómo funcionan los portales para después pasar a
nuestro proyecto Mini Twitter.
Para crear un Portal, tenemos que hacer uso del método createPortal que
proporciona el paquete react-dom, el cual recibe dos parámetros, el primero
corresponde al componente que vamos a montar dentro del portal, y el segundo
corresponde a un elemento DOM en donde se va a montar el portal.
Cuando creamos un portal, este retorna un elemento renderizable por React, por
lo que es normal retornarlo desde la función render de un componente. Un
ejemplo de cómo utilizarlo es crear un componente especial que retorno el portal:
295 | Página
7. class Modal extends React.Component {
8.
9. render() {
10. return createPortal(child, container)
11. }
12. }
13. export default Modal
El componente TweetDetail
El último componente que nos falta por ver es TweetDetail y con él, estaríamos
concluyendo el proyecto Mini Twitter. Este es un componte modal que nos
permite visualizar un tweet con todas las interacciones de los demás usuarios.
Página | 296
Antes de comenzar, quiero decir que este componente a pesar de ser visualizado
de forma modal, no es modal per se, sí no que lo diseñaremos como si fuera un
componente cualquiera y al final utilizaremos por Portales para hacerlo modal.
297 | Página
58. <div className="tweet-details-reply">
59. <Reply profile={this.state._creator}
60. operations={{addNewTweet: this.addNewTweet.bind(this)}}
61. key={"detail-" + this.state._id} newReply={false}/>
62. </div>
63. <ul className="tweet-detail-responses">
64. <If condition={this.state.replysTweets != null} >
65. <For each="reply" of={this.state.replysTweets}>
66. <li className="tweet-details-reply" key={reply._id}>
67. <Tweet tweet={reply} detail={true}/>
68. </li>
69. </For>
70. </If>
71. </ul>
72. </div>
73. </Otherwise>
74. </Choose>
75. </>
76. )
77. }
78. }
79. export default TweetDetail
Justo debajo del tweet podemos ver el componente Reply (línea 59), que
utilizaremos para que los usuarios puedan contestar el tweet.
Página | 298
Como parte de la respuesta del tweet, tenemos el detalle, es decir, todos los
comentarios que han dejado los demás usuarios en el tweet, por lo que cada
respuesta es tratada como un tweet más. En la línea 65 podemos ver que
iteramos las respuestas para representarlas como un componente tweet (línea
67).
El componente Modal
Antes que nada, realizaremos algunos ajustes al archivo App.js para agregar un
div sobre el cual se mostraran las pantallas modales.
299 | Página
El siguiente paso será crear el archivo Modal.js en el path /app:
Los portales pueden llegar a ser confusos, pues rompen la forma de trabajar de
React para permitir estructuras no jerárquicas a nivel del DOM, pero estoy seguro
que si te detienes un momento a analizar lo que está pasando podrás
comprenderlo mejor.
Página | 300
El siguiente paso será utilizar el componente Modal y TwitterDetail, para esto
tendremos que modificar el componente UserPage para agregar una nueva Route:
Las líneas 10 y 11 son importantes, porque son claves para entender cómo es
que el Portal se crea, para empezar, puedes ver que el componente TweetDetail
se crea dentro del componente Modal, por lo tanto, TweetDetail es un hijo de
Modal, es por ello que en Modal utilizamos la instrucción this.props.children.
this.props.children es una instrucción especial de React para hacer referencia
a los elementos hijos de un componente, por lo tanto, cuando desde Modal la
utilizamos, estamos obteniendo la referencia a TweetDetail. Finalmente, puedes
ver que le pasamos unos argumentos mediante {…params}, con esto, lo que
hacemos es pasarle los parámetros de React Router, para que tenga acceso a los
Path params.
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. }
301 | Página
El estilo anterior es utilizado para crear un elemento que cubra por completo la
pantalla con un color negro transparente, de tal forma que permita ver el fondo
pero sin poder interactuar con él.
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. }
El último ajuste que nos falta para terminar será sobre el componente Tweet, al
cual le agregaremos el evento de onClick para que nos lleve al detalle al
momento de darle click:
1. render(){
Página | 302
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">
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. }
Una última cosa interesante es, analizar el inspector de elementos del navegador,
para ver que el modal se encuentra fuera de la jerarquía:
303 | Página
Observa que el componente TwitterApp se monta con el id=mainApp, y es un
hermando del nodo dialog.
Por otra parte, si nos vamos al debuger de React, podremos ver la jerarquía de
otra forma, donde Modal es hijo de UserPage:
Página | 304
Últimos retoques al proyecto
Otra funcionalidad que no implementamos, es darle like a los tweets, por lo que
si quieres terminar de implementar esta funcionalidad solo falta hacer los
siguientes ajustes al componente Tweet.
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. }
Documentación: Like/Dislike
305 | Página
Página | 306
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 agregar Hooks, Context y Redux, los cuales
los estaremos analizando más adelante.
307 | Página
Context
Capítulo 12
Uno de los principales problemas de trabajar con React, es que para poder llevar
datos globales de la aplicación a todos los componentes, es necesario replicar
estos valores por toda la estructura de la aplicación mediante props, de tal forma
que los componentes padres deben de replicar estos valores a sus hijos y así
sucesivamente.
Otro de los problemas es que muchas veces estas propiedades no son requeridas
por todos los componentes, pero solo el hecho de que alguno de sus
descendientes lo necesite, implica modificar toda la cadena de componentes
hacia arriba para recibir y replicar los valores globales.
Página | 308
Si bien, el Context nos permite compartir cualquier datos a toda la aplicación, no
fue diseñado para utilizarse arbitrariamente para pasar lo que sea, en su lugar,
se utiliza únicamente para compartir datos que se consideren “globales”, es decir,
datos que pueden ser necesario en varios componentes de la aplicación sin
importar el nivel en la jerarquía en que se encuentre, como puede ser el usuario
autenticado, el tema, idioma o incluso formatos de fecha o moneda predefinidos
por el usuario.
createContext
El primer paso para utilizar el Context es crear uno, para esto, React proporciona
el método createContext, que recibe como parámetro el valor por default del
dato global o que queremos compartir en toda la aplicación. Veamos un ejemplo
de cómo sería:
Algo importante a tomar en cuenta es que, el contexto deberá ser creado como
un archivo independiente, pues eso permite que lo podamos referencias desde
cualquier parte, lo que hace que sea independiente de la estructura de
componentes.
309 | Página
Podríamos decir que en este punto estamos como en la imagen anterior, donde
tenemos un contexto (azul) independiente de la estructura y con un valor inicial
por default, sin embargo, podemos ver que el contexto aún está aislado de la
aplicación.
Una aplicación puede tener más de un Context, donde cada uno puede guardar
valores globales independientes, por lo que podemos crear diferentes Context
con createContext.
Provider
El componente Provider viene por defecto en cada Context, por lo que para
crearlo, lo hacemos atreves de la referencia del contexto que creamos
previamente, y recibirá como parámetro la propiedad value, la cual se utiliza para
establecer el valor actual del Context. Veamos un ejemplo:
Página | 310
6. constructor(props) {
7. super(props)
8. this.state = {
9. ...
10. }
11. }
12.
13. componentDidMount() {
14. ...
15. setState({
16. //new state
17. })
18. }
19.
20. render() {
21. return (
22. <Context.Provider value={this.state}>
23. <MyApp />
24. </Context.Provider>
25. )
26. }
27. }
28. export default MyComponent
Lo primero que tenemos que observar es que para crear el Provider, es necesario
importar el Contexto (línea 2) que creamos en la sección pasada con el método
createContext, una vez con la referencia, podemos ver que creamos el Provider
por medio de la referencia al Context (línea 22).
311 | Página
En este punto podemos decir que estamos como en la imagen anterior, donde el
Context ya está creado y estamos sincronizando el valor del Context por medio
del estado del componente. De esta forma, cada vez que cambie el estado del
componente, el Provider tomará este nuevo estado como el nuevo valor del
contexto.
Hay que tomar en cuenta que, así como podemos tener múltiples Context,
podemos tener múltiples Providers, por lo que solo habría que anidarlos de la
siguiente manera:
Consumer
El último paso es crear el Consumer, el cual es un componente provisto también
por el Context, que permite suscribirse a los cambios del Context.
Como regla, los Consumer deberán ser descendientes en cualquier nivel del
Provider, ya que será este último quien notifique los cambios del contexto al
Consumer.
Página | 312
6. render() {
7. return (
8. <Context.Consumer>
9. {context =>
10. <p>{context.value}</p>
11. }
12. </Context.Consumer>
13. )
14. }
15. }
16. export default MyChildComponent
La sintaxis del Consumer puede llegar a ser un poco confusa la primera vez, pero
verás que ahora que la expliquemos será más fácil. Lo primero que tenemos que
ver es que el Consumer se crear por medio del Context y no requiere de ninguna
propiedad. En segundo lugar, podrás ver que el Consumer no recibe un
componente directamente como hijo, sino más bien un arrow function, el cual
recibe como parámetro el valor actual del context, además deberá de retornar
un elemento renderizable.
El arrow function que recibe el Consumer será ejecutado cada vez que el Provider
actualice el valor del contexto mediante la propiedad value, de esta forma, todos
los componentes se podrán actualizar con el nuevo valor del Context, pero con
la única diferencia de que cuando el Contexto es actualizado, el método
shouldComponentUpdate es ignorado.
Finalmente, cabe mencionar que de la misma forma que podemos tener múltiples
Context y Providers, también podemos anidar múltiples Consumers para
recuperar el valor de múltiples Contexts. Veamos un ejemplo:
313 | Página
En este punto, se podrá decir que estamos como en la imagen siguiente:
En esta nueva imagen podemos ver que todo el ciclo se cierra, pues ya tenemos
el Context creado, el Provider está siendo actualizado y los componentes hijos
están recibiendo las actualizaciones del contexto por medio de los Consumers.
contextType
Página | 314
8. <p>{this.context.name}</p>
9. )
10. }
11. }
12.
13. MyComponent.contextType = Context
14.
15. export default MyComponent
Analicemos las partes de este componente. Primero que nada, tendremos que
importar el Context como ya lo veníamos haciendo, luego, tendremos que asignar
el contexto a la propiedad contextType de la clase (línea 13). Observa que esta
asignación se hace a nivel de clase y fuera de la declaración de la misma. Esto
hace que el contexto esté disponible en la propiedad this.context, tal y como
podemos ver que lo utilizamos en la línea 8.
Creado el UserContext
El único dato que podríamos considerar global en nuestro proyecto de Mini Twitter
es el usuario, y es utilizado en varios componentes de la aplicación, por lo que
tenerlo en Context nos podría ayudar mucho, facilitando su acceso a todos los
componentes de la aplicación, por tal motivo, comenzaros creando el archivo
UserContext.js en el path /app/context, el cual se verá de la siguiente forma:
Podrás notar que hemos inicializado en Context sin ningún dato por default, ya
que o sabes cuál es el usuario por medio del API o no lo tenemos, por lo que no
podemos tener un dato por default.
315 | Página
Una vez creado el Context, seguirá crear un Provider, al cual nos podremos
suscribir para recibir los cambios en el Context, por ello, deberemos modificar el
archivo TwitterApp.js, ya que es el punto desde el cual obtenemos el usuario
autenticado por medio del API REST:
Primero que nada, tendremos que importar el UserContext (línea 1), luego,
tendremos que englobar toda la aplicación con el Provider que creamos a partir
del UserContext (línea 14), finalmente, utilizamos el state del componente para
definir la propiedad value del Provider. Recordemos que el Perfil del usuario es
cargado en el método componentDidMount y el resultado guardado en el estado
bajo la propiedad profile.
Finalmente, hemos removido la propiedad profile del Toolbar (línea 15), ya que
ahora en adelante, obtendrá el usuario por medio del Context.
Página | 316
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. import UserContext from './context/UserContext'
7.
8. const TwitterDashboard = (props) => {
9.
10. return (
11. <UserContext.Consumer>
12. {context => <div id="dashboard" className="animated fadeIn">
13. <div className="container-fluid">
14. <div className="row">
15. <div className="hidden-xs col-sm-4 col-md-push-1
16. col-md-3 col-lg-push-1 col-lg-3" >
17. <Profile profile={context} />
18. </div>
19. <div className="col-xs-12 col-sm-8 col-md-push-1
20. col-md-7 col-lg-push-1 col-lg-4">
21. <TweetsContainer profile={context} />
22. </div>
23. <div className="hidden-xs hidden-sm hidden-md
24. col-lg-push-1 col-lg-3">
25. <SuggestedUser />
26. </div>
27. </div>
28. </div>
29. </div>}
30. </UserContext.Consumer>
31. )
32. }
33.
34. export default TwitterDashboard
También podrás ver que hemos cambiado la fuente de donde tomamos el usuario
para enviárselo al componente Profile (línea 17) y TweetsContainer (línea 21),
ya que antes replicábamos el usuario que llegaba como prop y ahora lo
obtenemos del Context.
Aquí hay un punto importante que me gustaría recalcar, y es que podrás ver que
el componente TwitterDashboard en realidad no utiliza para el nada el Context,
más bien solo lo recupera para luego pasarlo al componente Profile y
TweetsContainer, porque quizás sería mejor que estos dos componentes
recuperaran el contexto por sí mismo, sin embargo, una de las mejores prácticas
para hacer componentes reutilizables es, hacer los componentes reutilizables
independientes de la ubicación de los datos, de esta forma, si el día de mañana
quieres mostrar un usuario distinto al perfil autenticado solo basta con marlo
como prop, del mismo modo, si quieres mostrar los tweets de un usuario
diferente, solo se lo mandas, es por ello que hemos decidido hacerlo de esta
manera, sin embargo, siente libre de agregar directamente el Context sobre estos
dos componentes si lo crees más conveniente.
317 | Página
El Toolbar.js será el siguiente archivo a modificar, el cual requiere del usuario
para ver nuestra foto de perfil o para llevarnos a la página de nuestro perfil:
Página | 318
61. </li>
62. <li role="separator" className="divider">
63. </li>
64. <li>
65. <Link to="#" onClick={logout}>
66. Cerrar sesión</Link>
67. </li>
68. </ul>
69. </li>
70. </ul>
71. </If>
72. </div>
73. </div>
74. </nav>}
75. </UserContext.Consumer>
76. )
77. }
78.
79. Toolbar.propTypes = {
80. profile: PropTypes.object
81. }
82.
83. export default Toolbar
El último cambio que haremos será sobre el componente Login, el cual lo hago
más que nada para demostrar cómo utilizar el contextType más que para otra
cosa. Lo que vamos a hacer es que si un usuario autenticado intenta entrar a la
página /login, sea automáticamente redireccionado al home (/), pues como ya
está autenticado, no tiene sentido que llegue al login.
319 | Página
21. </div>
22. </div>
23. <div className="signup-form">
24. <form onSubmit={this.login.bind(this)}>
25. <h1>Iniciar sesión en Twitter</h1>
26.
27. <input type="text" value={this.state.username}
28. placeholder="usuario" name="username" id="username"
29. onChange={this.handleInput.bind(this)}/>
30. <label ref="usernameLabel" id="usernameLabel"
31. htmlFor="username"></label>
32.
33. <input type="password" id="passwordLabel"
34. value={this.state.password} placeholder="Contraseña"
35. name="password" onChange={this.handleInput.bind(this)}/>
36. <label ref="passwordLabel" htmlFor="passwordLabel"></label>
37.
38. <button className="btn btn-primary btn-lg " id="submitBtn"
39. onClick={this.login.bind(this)}>Regístrate</button>
40. <label ref="submitBtnLabel" id="submitBtnLabel"
41. htmlFor="submitBtn"className="shake animated hidden "></label>
42. <p className="bg-danger user-test">Crea un usuario o usa el usuario
43. <strong>test/test</strong></p>
44. <p¿No tienes una cuenta? <Link to="/signup">Registrate</Link> </p>
45. </form>
46. </div>
47. </div>
48. )
49. }
50. }
51.
52. Login.contextType = UserContext
53.
54. export default Login
Página | 320
Conclusiones
En esta unidad hemos descubierto el poder del contexto, el cual nos permite
guardar y acceder a toda aquella información que consideramos global dentro de
la aplicación, evitando tener que pasar como propiedad los datos por toda la
estructura.
321 | Página
Hooks
Capítulo 13
Los hooks es una de las características más aplaudida por la comunidad de React,
pues ha venido a revolucionar por completo la forma en que construimos
componentes, al mismo tiempo que ha corregido ciertos problemas que React
arrastraba desde su lanzamiento, el cual tenía una sintaxis con poco confusa y
un ciclo de vida algo complejo.
Para comprender los hooks hace falta entender la historia de React y como este
ha venido evolucionando para adaptarse a las nuevas versiones de ECMAScript
(especificación sobre la cual está construido JavaScript).
Página | 322
8. }
9. });
10.
11. export default Contacts;
Este cambio sin duda fue un acierto en el rumbo de React, pues permitía explotar
las características nativas del lenguaje en lugar de solo simularlas, sin embargo,
algo inesperado pasó, y es que la comunidad rápidamente se comenzó a topar
con ciertos “problemas” al momento de desarrollar, los cuales describo a
continuación:
Constructor
323 | Página
Autobinding
Uno de los problemas que más se reportaban con frecuencia entre la comunidad
fue referente a como referenciar a métodos dentro de un componente de clase,
pues teníamos siempre que utilizar la instrucción this, y en algunos casos,
this.xxx.bind(this), tal como podemos ver en el siguiente ejemplo:
Ciclo de vida
Página | 324
16. }
17.
18. componentDidUpdate(prevProps) {
19. if (this.props.user !== prevProps.user) {
20. fetchUser(this.props.user) //🤮
21. }
22. }
23.
24. fetchUser(user){
25. let users = fetch(`/users/${user}`)
26. .then(res => res.json())
27. .then(res => this.setState(res))
28. }
29.
30. render(){
31. return(
32. <button onClick={this.click.bind(this)}
33. )
34. }
35. }
36.
37. export default MyComponent
En este nuevo caso se puedes ver cómo hay que separar la lógica para inicializar
el componente para poder luego ser llamada en el método componentDidMount y
componentDidUpdate.
Otro de los problemas que tiene React es que no tiene un mecanismo para
reutilizar la lógica no visual, lo que nos obliga a crear componentes que retornan
otro componente o lo que conocemos como high order component. Un ejemplo
claro de esto el Context como tal, en el cual necesitamos envolver un componente
dentro de otro y luego ese dentro de otro más y así susevimente, lo que hace
una sintaxis demasiado compleja:
325 | Página
21. }
22. export default MyComponent
Todo es que acabo de explicar lo explico con lujo en el video que dejo a
continuación: https://www.youtube.com/watch?v=beR07AOppkk
Página | 326
Como ya lo mencionamos anteriormente, se dijo que los hooks permite agregar
estados a los componentes de función, sin embargo, ese es solo un comunicado
de Marketing para que la gente pueda entender rápidamente que es, pero como
también analizamos hace un momento, los componentes de clase tiene una serie
de inconvenientes que son resueltos con los hooks, entonces, podríamos decir
que los hooks son en realidad una nueva forma de escribir componentes que
mejora la composición y reutilización del código.
Estado
Lo primero que tienes que saber sobre los Hooks es que absolutamente todo lo
relacionado con ellos aplican sobre componentes de función, los que cuales
anteriormente conocíamos como componentes sin estado (Stateless).
Lo primero que veremos será el Hook useState, el cual nos permite definir un
estado para el componente. useState recibe como parámetro el valor inicial del
estado y retorna un array de dos posiciones, donde la primera posición
corresponde al valor actual del estado, mientras que la segunda posición
corresponde a una función para actualizar estado.
La línea 5 puede ser especialmente confusa, por que utiliza algo llamada
desestructuración, el cual consiste en declarar y asignar los valores del array
según su posición. Para entender mejor que está pasando veamos un ejemplo
totalmente equivalente al anterior:
327 | Página
6. const count = state[0]
7. const setCount = state[1]
8.
9. return (
10. <div>
11. <p>You clicked {count} times</p>
12. <button onClick={() => setCount(count + 1)}>
13. Click me
14. </button>
15. </div>
16. );
17. }
Página | 328
14. );
15. }
Observa que el componente renderiza un botón que cuando le damos click hace
una llamada a la función setCount, a la cual le incrementamos el valor del
contador de clicks.
1. // constructor
2. this.state = {
3. a: 1,
4. b: 2
5. }
6.
7. // componentDidMount
8. this.setState({
9. a: 5
10. })
11. console.log(this.state) // {a: 5, b: 2}
Observa que el estado se mescla para crear un nuevo estado de la unión del
anterior con el nuevo, sin embargo, en los Hooks no pasa esto. Veamos un
ejemplo:
Para realizar una correcta actualización del estado respetando los valores
anteriores podemos utilizar el operador de propagación (Spread operator), el cual
consiste en anteponer 3 puntos (…) al estado para extraer sus propiedades y
agregarlas al nuevo estado, veamos un ejemplo:
329 | Página
1. const [state, setState] = React.useState({
2. a: 1,
3. b: 2
4. })
5.
6.
7. setState({
8. ...state,
9. a: 5
10. })
11. console.log(state) // {a: 5, b: 2}
Este pequeño truco hace que todas las propiedades del estado actual pasan al
nuevo estado, y luego actualizo puntualmente la propiedad que queremos
actualizar.
Observa que hemos creado un segundo estado que guarda una bandera para
saber si la página ha terminado de cargar, la cual tiene su propio método set.
Ciclo de vida
Otra de las limitantes de los compontes de función era que no tenía un ciclo de
vida, por lo tanto, solo funcionaban como componente de representación, es
decir, tomaban los props y luego renderizaban la vista a partir de ellos.
Con los Hooks eso cambia, pues podemos agregar algo llamado efectos
especiales o simplemente efectos (effects), los cuales son un tipo especial de
hooks que permite agregar comportamiento a los componentes de función. De
forma resumida, un efecto combina los métodos componentDidMount,
Página | 330
componentDidUpdate y componentWillUnmount en una sola API, lo que simplifica
significativamente el ciclo de vida. Veamos un ejemplo:
Si analizamos el ejemplo anterior, podemos ver que este efecto actúa como el
método componendDidMount y componentDidUpdate al mismo tiempo, pues se
ejecutará justo después del primer renderizado, pero también se ejecutará
durante la actualización.
331 | Página
3. const MyComponent = (props) => {
4.
5. const [user, setUser] = useState()
6.
7. // Similar a componentDidMount y componentDidUpdate:
8. useEffect( () => {
9. fetch(`/users/${props.user}`)
10. .then(res => res.json())
11. .then(res => setUser(res))
12. }, [props.user] )
13.
14. return(
15. <p>{user.name}</p>
16. )
17. }
18. export default MyComponent
En la línea 12 hemos agregado un arreglo con el nombre del usuario, lo que hará
que el efecto se ejecute la primera vez, pero después de esto, solo se volverá a
ejecutar cuando el valor de la propiedad cambie.
Toma en cuente que este array puede tener varias posiciones, permitiendo tener
múltiples razones por las que el efecto debería de ejecutarse.
Error común
De la misma forma que podemos tener varios estados, también podemos tener
diferentes efectos, y cada efecto puede tener reglas diferentes para que se
ejecuten bajo diferentes circunstancias, solo toma en cuenta que los efectos
serán ejecutados en el orden en que están declarados:
Página | 332
15. // Similar a componentDidMount y componentDidUpdate:
16. useEffect( () => {
17. fetch(`/tweets`)
18. .then(res => res.json())
19. .then(res => setTweets(res))
20. }, [ ])
21.
22. return(
23. <p>{user.name}</p>
24. )
25. }
26. export default MyComponent
Observe que en esta ocasión tenemos dos efectos, con la única diferencia de que
las reglas para ejecutarlos cambian y aquí es donde quiero darles un tip, si
quieres que un efecto solo se ejecuta una vez, agrega un arreglo vacío, así los
valores nunca cambiarán y dará como resultado la ejecución inicial justo después
del primer render.
Tip
333 | Página
18. }, [props.user])
19.
20. return(
21. <p>{user.name}</p>
22. )
23. }
24. export default MyComponent
Cuando digo que es una regla no escrita me refiero a que React o JavaScript no
podrán impedir que les pongas otro nombre, sin embargo, si está definido en la
documentación de React, por lo tanto, se podría decir que debemos de comenzar
los hooks con “use” aunque nadie nos puede impedir que lo hagamos.
Página | 334
13. }
14. export default useTweets
Este hook nos permite recuperar los últimos tweets o los tweets de un solo
usuario. Para lograr esto, requiere que se le envié el usuario del cual se buscarán
los tweets, pero si ese valor no se envía, se buscan los últimos tweets de todos
los usuarios. Ahora bien, para utilizar el hook anterior, solo tenemos que
mandarlo llamar:
Para reutilizar el hook useTweets solo hace falta llamarlo desde otro componente,
y con esto, estaríamos reutilizando lógica no visual de forma simple.
Uno de los hooks más populares es useContext, el cual nos permite utilizar el
Context sin la necesidad de encapsular nuestro componente dentro de un
Consumer, evitando el famoso infierno de las envolturas, veamos un ejemplo:
335 | Página
Si observas la línea 6, podrás ver que es posible recuperar el contexto con tan
solo utilizar el hook useContext, el cual recibe como parámetro el contexto que
queremos recuperar.
Algo a tomar en cuenta, es que a pesar de utilizar los hooks para obtener el
contexto, es que solo nos ahorra la creación del Consumer, pero seguiremos
necesitando crear el Provider, de por lo que cuando usemos useContext, deberá
ser sobre un componente que sea descendiente del Provider.
Muchos se ha especulado que ahora con la llegada de los hooks, los componentes
de clase pasaran a ser posteriormente desaconsejados (deprecados), sin
embargo, el mismo equipo de React ha salido a desmentir esto, incluso, insisten
en que no hay planes para desaconsejar el uso de clases en el futuro, por lo que
podemos seguir usando clases en nuestros proyectos la garantía de que seguirán
por mucho tiempo más.
Otro de los comentarios que puedes ver mucho en Internet es personas diciendo
que hora debemos de mirar todos nuestros componentes para utilizar Hooks, sin
embargo, pueden existir proyectos que combinen componentes de clases y
componentes de función, incluso, es normal ver proyectos donde existen
componentes de función y de clase al mismo tiempo.
Página | 336
Mi recomendación es que solo migremos a hooks solo los nuevos compontes que
creemos y migrar uno que otro componente de clase que se nos complique por
el uso de clases, pero fuera de eso, no tiene caso hacer una migración total del
proyecto.
En esta unidad vamos a aplicar todos los conocimientos que hemos adquirido de
los hooks para mejorar nuestro proyecto Mini Twitter, por lo que comenzaremos
con los componentes más simples e iremos pasando a los más complejos a
medida que avancemos.
337 | Página
24. <div className="row no-padding">
25. <CSSTransitionGroup
26. transitionName="card"
27. transitionEnter={true}
28. transitionEnterTimeout={500}
29. transitionAppear={false}
30. transitionAppearTimeout={0}
31. transitionLeave={false}
32. transitionLeaveTimeout={0}>
33. <For each="user" of={state}>
34. <div className="col-xs-12 col-sm-6 col-lg-4"
35. key={user._id}>
36. <UserCard user={user} />
37. </div>
38. </For>
39. </CSSTransitionGroup>
40. </div>
41. </div>
42. </section>
43. )
44. }
45.
46. Followers.propTypes = {
47. profile: PropTypes.object
48. }
49.
50. export default Followers
Debido a que este componente ya no es una clase, los métodos del ciclo de vida
ya no funcionarán, por lo que el siguiente paso es extraer la lógica que teníamos
en el método componentDidMount y pasarla dentro de un useEffect, tal y como
podemos ver en la línea (11). Para garantizar que el efecto no se ejecute de
forma indeterminada, le decimos que no se ejecute solo cuando la propiedad
props.profile.userName cambien, en otro caso, solo se ejecutará la primera vez
cuando el componente se monte.
Página | 338
Migrando el componente Followings
339 | Página
Nuevamente, convertimos el componente de clase a componente de función y
agregamos las props como parámetro de entrada (línea 7). Seguido, declaramos
el estado del componente con useState (línea 9). Pasamos la lógica del método
componentDidMount a un useEffect (línea 11) y nos aseguramos de que solo se
ejecute cuando el usuario cambie (línea 18). Finalmente cambiamos el método
render por un simple return.
Página | 340
45. }
46. export default SuggestedUser;
Para ponerle un poco más de acción a esto, vamos a utilizar otra de las
características de los hooks, que es la posibilidad de utilizar el Context sin la
necesidad de declarar un componente Consumer. Veamos a migrar el componente
Toolbar para ver como quedaría:
341 | Página
35. <i className="fa fa-home menu-item-icon"
36. aria-hidden="true" />
37. <span className="hidden-xs hidden-sm">
38. Inicio</span>
39. </p>
40. </NavLink>
41. </li>
42. </ul>
43. </div>
44. <If condition={userContext != 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={userContext.avatar}
52. alt={userContext.userName} />
53. </a>
54. <ul className="dropdown-menu">
55. <li>
56. <Link to={`/${userContext.userName}`}>
57. Ver perfil</Link>
58. </li>
59. <li role="separator" className="divider"/>
60. <li>
61. <Link to="#" onClick={logout}>
62. Cerrar sesión</Link>
63. </li>
64. </ul>
65. </li>
66. </ul>
67. </If>
68. </div>
69. </div>
70. </nav>
71. )
72. }
73.
74. export default Toolbar
Página | 342
Migrando el componente TwitterDashboard
Otro componente que también requiere los mismos pasos que los anteriores es
TwitterDashboard, donde pasaremos de un componente de clase a uno de
función:
No te voy a aburrir nuevamente con la explicación, pues creo que ya hemos visto
varios ejemplos de lo que tenemos que hacer, por lo que solo te marcaré los
cambios para que sea tú mismo quien analice los cambios.
343 | Página
Desde este punto, los cambios se comienzan a poner interesantes, pues vamos
a comenzar a crear nuestros propios hooks con la intención de reutilizar
funcionamiento.
Para explicar los cambios en este componente, vamos a omitir los pasos que ya
sabemos de pasar de clase a función y todo eso, para centrarnos en la línea 15.
Observa que estamos utilizando un nuevo hook llamado useLogin.
Lo primero que nos debemos de cuestionar es, ¿cómo sabemos que se trata de
un hook? Bueno, si recuerdas la teoría al inicio de este capítulo, dijimos que los
hooks comienza con “use”, por lo tanto, podemos determinar que se trata de un
hook, ahora bien, si vemos el import de ese hook, veremos que no es estándar
de React, y es que este es un hook que yo he creado para reutilizar la lógica de
autenticación.
Página | 344
2. import APIInvoker from '../utils/APIInvoker'
3. import browserHistory from '../History'
4.
5. const useLogin = () => {
6. const [loginStatus, setLoginStatus] = useState({
7. load: false,
8. user: null
9. })
10.
11.
12. useEffect(() => {
13. let token = window.localStorage.getItem("token")
14. if (token == null) {
15. setLoginStatus({
16. load: true,
17. user: null
18. })
19. } else {
20. APIInvoker.invokeGET('/secure/relogin', response => {
21. setLoginStatus({
22. load: true,
23. user: response.profile
24. })
25. window.localStorage.setItem("token", response.token)
26. window.localStorage.setItem("username",response.profile.userName)
27. }, error => {
28. console.log("Error al autenticar al autenticar al usuario ");
29. window.localStorage.removeItem("token")
30. window.localStorage.removeItem("username")
31. setLoginStatus({
32. load: true,
33. user: null
34. })
35. })
36. }
37. }, [window.localStorage.getItem("token")])
38.
39. return [loginStatus.load, loginStatus.user]
40. }
41.
42. export default useLogin
¿Recuerdas que dijimos que uno de los propósitos de los hooks es reutilizar la
lógica no visual? Pues es justo lo que estamos haciendo aquí. Si prestas mucha
atención, el contenido del efecto declarado en la línea 12, es lo mismo que
teníamos en el método componentDidMount del componente TwitterApp. Entonces
podemos decir que este hooks lo que hace es hacer la llamada al API para validar
el token y regresarnos el usuario autenticado (si es que tenemos un token).
Para ayudarte a comprender que es un hook, te daré una pista, dijimos que los
hooks permiten reutilizar la lógica no visual, por lo tanto, un hook no retorna una
interfaz, en su lugar retorna datos (o incluso nada), dicho lo anterior, quiero que
prestes atención para que te des cuenta que un hook es casi lo mismo que un
345 | Página
componente de función, solo que no regresa un elemento gráfico. Imagina que
la línea 39 regresara un elemento renderizable… te das cuenta… sería un
componte de función. Solo revisa sus elementos, es una función, puede tener un
estado con useState (línea 6), tiene efectos (línea 12) y en lugar de retornar un
elemento renderizable, retorna datos, es por eso que decimos que los hooks
permite reutilizar lógica no visual.
Por el momento solo utilizaremos este hook en TwitterApp, pero debido a que
ahora es un hook, ya lo podríamos reutilizar en cualquier otra parte de la
aplicación para logear al usuario por medio del token y que nos retorne el
usuario autenticado.
Página | 346
27. useEffect(() => {
28. let username = props.match.params.user
29. APIInvoker.invokeGET(`/profile/${username}`, response => {
30. setEdit(false)
31. setProfile(response.body)
32. }, error => {
33. console.log("Error al cargar los Tweets");
34. window.location = '/'
35. })
36. }, [props.match.params.user])
37.
38.
39. const follow = (e) => {
40. let request = {
41. followingUser: props.match.params.user
42. }
43. APIInvoker.invokePOST('/secure/follow', request, response => {
44. if (response.ok) {
45. setProfile(update(profile, {
46. follow: { $set: !response.unfollow }
47. }))
48. }
49. }, error => {
50. console.log("Error al actualizar el perfil");
51. })
52. }
53.
54. const changeToEditMode = (e) => {
55. if (edit) {
56. let request = {
57. username: profile.userName,
58. name: profile.name,
59. description: profile.description,
60. avatar: profile.avatar,
61. banner: profile.banner
62. }
63.
64. APIInvoker.invokePUT('/secure/profile', request, response => {
65. if (response.ok) {
66. setEdit(false)
67. }
68. }, error => {
69. console.log("Error al actualizar el perfil");
70. })
71. } else {
72. let currentState = profile
73. setEdit(true)
74. setProfile({
75. ...profile,
76. currentState
77. })
78. }
79. }
80.
81. const imageSelect = (e) => {
82. let id = e.target.id
83. e.preventDefault();
84. let reader = new FileReader();
85. let file = e.target.files[0];
86.
87. if (file.size > 1240000) {
88. alert('La imagen supera el máximo de 1MB')
89. return
90. }
91.
92. reader.onloadend = () => {
347 | Página
93. if (id == 'bannerInput') {
94. setProfile(update(profile, {
95. banner: { $set: reader.result }
96. }))
97. } else {
98. setProfile(update(profile, {
99. avatar: { $set: reader.result }
100. }))
101. }
102. }
103. reader.readAsDataURL(file)
104. }
105.
106. const handleInput = (e) => {
107. let id = e.target.id
108. setProfile(update(profile, {
109. [id]: { $set: e.target.value }
110. }))
111. }
112.
113. const cancelEditMode = (e) => {
114. setEdit(false)
115. setProfile(profile.currentState)
116. }
117.
118.
119. return (
120. <div id="user-page" className="app-container">
121. <header className="user-header">
122. <div className="user-banner"
123. style={{ backgroundImage: 'url(' + (profile.banner) + ')' }}>
124. <If condition={edit}>
125. <div>
126. <label htmlFor="bannerInput" className="btn select-banner">
127. <i className="fa fa-camera fa-2x" aria-hidden="true"></i>
128. <p>Cambia tu foto de encabezado</p>
129. </label>
130. <input href="#" className="btn"
131. accept=".gif,.jpg,.jpeg,.png"
132. type="file" id="bannerInput"
133. onChange={imageSelect} />
134. </div>
135. </If>
136. </div>
137. <div className="user-summary">
138. <div className="container-fluid">
139. <div className="row">
140. <div className="hidden-xs col-sm-4 col-md-push-1
141. col-md-3 col-lg-push-1 col-lg-3" >
142. </div>
143. <div className="col-xs-12 col-sm-8 col-md-push-1
144. col-md-7 col-lg-push-1 col-lg-7">
145. <ul className="user-summary-menu">
146. <li>
147. <NavLink to={`/${profile.userName}`} exact
148. activeClassName="selected">
149. <p className="summary-label">TWEETS</p>
150. <p className="summary-value">{profile.tweetCount}</p>
151. </NavLink>
152. </li>
153. <li>
154. <NavLink to={`/${profile.userName}/following`}
155. activeClassName="selected">
156. <p className="summary-label">SIGUIENDO</p>
157. <p className="summary-value">{profile.following}</p>
158. </NavLink>
Página | 348
159. </li>
160. <li>
161. <NavLink to={`/${profile.userName}/followers`}
162. activeClassName="selected">
163. <p className="summary-label">SEGUIDORES</p>
164. <p className="summary-value">{profile.followers}</p>
165. </NavLink>
166. </li>
167. </ul>
168.
169. <If condition={profile.userName === userContext.userName}>
170. <button className="btn btn-primary edit-button"
171. onClick={changeToEditMode} >
172. {edit ? "Guardar" : "Editar perfil"}</button>
173. </If>
174.
175.
176. <If condition={profile.follow != null &&
177. profile.userName !== userContext.userName} >
178. <button className="btn edit-button"
179. onClick={follow} >
180. {profile.follow
181. ? (<span><i className="fa fa-user-times"
182. aria-hidden="true"></i> Siguiendo</span>)
183. : (<span><i className="fa fa-user-plus"
184. aria-hidden="true"></i> Seguir</span>)
185. }
186. </button>
187. </If>
188.
189. <If condition={edit}>
190. <button className="btn edit-button" onClick=
191. {cancelEditMode} >Cancelar</button>
192. </If>
193. </div>
194. </div>
195. </div>
196. </div>
197. </header>
198. <div className="container-fluid">
199. <div className="row">
200. <div className="hidden-xs col-sm-4 col-md-push-1 col-md-3
201. col-lg-push-1 col-lg-3" >
202. <aside id="user-info">
203. <div className="user-avatar">
204. <Choose>
205. <When condition={edit} >
206. <div className="avatar-box">
207. <img src={profile.avatar} />
208. <label htmlFor="avatarInput"
209. className="btn select-avatar">
210. <i className="fa fa-camera fa-2x"
211. aria-hidden="true"></i>
212. <p>Foto</p>
213. </label>
214. <input href="#" id="avatarInput"
215. className="btn" type="file"
216. accept=".gif,.jpg,.jpeg,.png"
217. onChange={imageSelect}
218. />
219. </div>
220. </When>
221. <Otherwise>
222. <div className="avatar-box">
223. <img src={profile.avatar} />
224. </div>
349 | Página
225. </Otherwise>
226. </Choose>
227. </div>
228. <Choose>
229. <When condition={edit} >
230. <div className="user-info-edit">
231. <input maxLength="20" type="text" value={profile.name}
232. onChange={handleInput} id="name" />
233. <p className="user-info-username">
234. @{profile.userName}
235. </p>
236. <textarea maxLength="180" id="description"
237. value={profile.description}
238. onChange={handleInput} />
239. </div>
240. </When>
241. <Otherwise>
242. <div>
243. <p className="user-info-name">{profile.name}</p>
244. <p className="user-info-username">
245. @{profile.userName}
246. </p>
247. <p className="user-info-description">
248. {profile.description}</p>
249. </div>
250. </Otherwise>
251. </Choose>
252. </aside>
253. </div>
254. <div className="col-xs-12 col-sm-8 col-md-7
255. col-md-push-1 col-lg-7">
256. <Switch>
257. <Route exact path="/:user" component={
258. () => <MyTweets profile={profile} />} />
259. <Route exact path="/:user/followers" component={
260. () => <Followers profile={profile} />} />
261. <Route path="/:user/following" component={
262. () => <Followings profile={profile} />} />
263. <Route exact path="/:user/tweet/:tweet" component={
264. (params) => <Modal> <TweetDetail {...params} /> </Modal>} />
265. </Switch>
266. </div>
267. </div>
268. </div>
269. </div>
270. )
271. }
272. export default UserPage
Bueno, podrás ver que es un componente muy extenso como para explicar cada
cambio, por lo que haremos un resumen de los cambios más simples y después
pasaremos a explicar aquellos más complicados.
Página | 350
El siguiente paso es eliminar el método componentDidMount y pasar la
funcionalidad al hook useEffect (línea 27), el cual lo hemos limitado para que se
ejecute solo cuando el URL param user cambie (línea 36).
Otro de los cambios simple que realizamos fue reemplazar todas las funciones de
la clase a funciones de flecha y de esta forma, dejar de utilizar el this para
acceder a la funcionalidad.
Otro de los cambios también simples pero demasiado numerosos como para
detenernos a explicar uno por uno, es que hemos eliminado la referencia al
this.state para reemplazarlo los dos estados definidos con useState, me refiero
a profile y edit, también hemos cambiado las referencias a los métodos,
eliminando la necesidad de utilizar this.xxxx.bind(this).
351 | Página
Conclusiones
Como hemos analizando en esta unidad, los hooks son una nueva forma de crear
componentes de función que permitan tener estado, pero también mencionamos
que el estado es solo una parte de lo que realmente nos ofrecen los hooks, pues
analizamos como mediante los hooks es posible evitar algunas cosas incomodas,
como tener que inicializar la super clase en el constructor, evitamos el uso de
this y bind, nos permite reutilizar lógica no visual y nos permite evitar el famoso
infierno de los envoltorios.
Sin embargo, si tuviéramos que definir qué es lo más importante que nos traen
los hooks, yo diría que son 3 cosas, la posibilidad de tener estado, añadir efectos
como una sustitución al complicado ciclo de vida de los componentes de clase y
la posibilidad de reutilizar lógica no visual.
Página | 352
Redux
Capítulo 14
Para lograr que un cambio se propague de esta forma, es necesario que todos
los componentes involucrados en la cadena, conozcan la propiedad y las
repliquen a sus descendientes, sin embargo, pasar propiedades solo aplica para
replicar los cambios de arriba abajo, y para replicar los cambios de abajo arriba,
es necesario que los padres maden funciones como props para que los hijos las
ejecuten para notificar al padre de los cambios.
353 | Página
Este problema se puede repetir varias veces en una aplicación, sobre todo en
aquellas páginas muy complejas donde hay una gran cantidad de elementos, lo
que puede convertirse rápidamente un problema. Ya que administrar props y
funciones por toda la estructuractura crea componentes sumamente complejos y
difíciles de entender y mantender.
Introducción a Redux
Redux es una herramienta que nos ayuda a gestionar la forma en que accedemos
y actualizamos el estado de la aplicación de una forma centralizada y controlada.
Mediante Redux es posible centralizar el estado general de la aplicación en algo
llamado store, liberando a los componentes la responsabilidad de administrar un
estado interno.
Este último pude resultar confuso, por que no queda claro cuando algo de global
y cuando no, sin embargo, hay una forma de saberlo, si un determinado dato es
relevante para toda la aplicación, entonces se considera global, por otro lado, si
un determinado dato, solo es relevante solo para una determinada página,
entonces es un dato que podríamos gestionar con Redux.
Página | 354
Como funciona Redux
Antes que nada, es importante aclarar que Redux es una librería que nace para
gestionar el estado de cualquier aplicación de una sola página (SPA) basada en
JavaScript, por lo que posible utilizarla con React, JQuery, Angular o una simple
página que utiliza JavaScript puro. Dicho lo anterior, pasemos a explicar como
funciona Redux.
Lo primero que debemos aprender son los componentes que conforma Redux y
como estos encajan para administrar el estado de la aplicación:
Ahora bien, de nada sirve listar los componentes que conforman Redux sin no
entendemos como interactúan, por lo que vamos a ver con una serie de
imágenes, cual es el procesos desde el cual se crea el store, se lanza una acción,
se actualiza el estado con los reducers y finalmente, los cambios son propagados
por los componentes.
El primer paso para utilizar Redux es crear el store, el cual dijimos que es un
objeto independiente a la estructura de la jerarquía de componentes, el cual está
totalmente desconectado de los componentes al momento de su creación:
355 | Página
Para crear el store, Redux nos proporciona la función createStore, el cual recibe
como parámetro un reducer, o un conjunto de estos. Si nuestro store solo esta
compuesto de un reducer, lo podemos pasar directamente como parámetro la
función createStore, sin embargo, la mayoría de las veces, es necesario utilizar
más de un reducer, por lo que utilizamos la función combineReducer para
agruparlos.
Observa que para crear el store estamos utilizando 3 reducers, los cuales
analizaremos más adelante.
Página | 356
Paso 2: Registrandonos al store
357 | Página
Para conectar un componente al store, tenemos el hook useSelector, el cual
recibe como parámetro una función, dicha función recibirá como parámetro el
estado general del store, y la función deberá de retornar la sección del store que
nos interesa, de esta forma, cada vez que el store cambie, el hook actualizará el
componente con los nuevos valores, lo que detonará un nuevo renderizado del
componente para reflejar los nuevos valores.
Para despachar una acción son necesarios dos cosas, un referencia al dispatcher
y el objeto action que describe los cambios a realizar en el store. Para recuperar
el dispatcher contamos con el hook useDispatch:
Página | 358
11. <p>{state.name}</p>
12. </MyComponent/>
13. )
14.
15. }
16. export default MyComponent
Para despachar una acción, es necesario enviar un objeto que tenga al menos la
propiedad type, la cual es utilizada por los reducers para identificar el cambio
que se quiere realizar, además, es posible enviar cualquier otra propiedad para
complementar la acción, el cual puede tener los nuevos datos para el store.
359 | Página
En este punto, podemos ver que el componente I ha despachado una acción con
la intención de actualizar el estado.
Importante
Página | 360
Si bien solo puede existir un solo Store con un solo estado, el Store puede tener
varios reducers que modifican el estado. Un reducer es básicamente una función
JavaScript pura, que recibe como entrada el estado actual de la aplicación y el
Action que describe el intento por actualizar el estado, de esta forma, es el
reducer quien determina como deberá ser actualizado el estado basado en el
action.
1. const initialState = {
2. values: []
3. }
4.
5. export const myReducer = (state = initialState, action) => {
6. switch (action.type) {
7. case "INIT":
8. return {
9. values: action.value
10. }
11. case "CREAR":
12. return initialState
13. default:
14. return state
15. }
16. }
17. export default myReducer
361 | Página
Si observas el reducer anterior, te podrás dar cuenta que se trata de función
JavaScript como y corriente, la cual recibe como entrada el state actual y el
action, también podrás ver que el state es igualado a la constante initialValue,
lo que significa que declaramos el estado inicial del store.
Dentro del cuerpo del reduce podemos ver un switch utilizado para realizar una
acción diferente basado en el valor de la propiedad type del action. Si el type del
action corresponde con alguno de los cases, entonces procederemos ha aplicar
algún cambio en el estado del estore, de esta forma, el valor que retornemos
será el que se guardará en el nuevo estado del store, pero si por otra parte, el
type no corresponde con ninguna acción, entonces simplemente retornamos el
mismo estado que recibimos para indicarle al store que no se ha aplicando
ningún cambio.
Un punto que no hemos abordado es que, la estructura del estado dentro del
reducer es determianda por los reducers, de esta forma, un store tiene una forma
de árbol, donde cada reducer crea una rama, por ejemplo, si tenermos 3
reducers, estos crearían 3 ramas con el nombre establecidos al momento de crear
el store. Veamos un ejemplo de como se crear el store:
Página | 362
Este store dará como resultado un estado con la siguiente estructura:
1. {
2. tweets: {
3. ...
4. },
5. user: {
6. ...
7. },
8. config: {
9. ...
10. }
11. }
363 | Página
Finalmente, cabe mencionar que durante todo el tiempo de vida de la aplicación,
cualquier componente podrá despachar acciones, lo que implicaría que los pasos
3, 4 y 5 se podrían dar decenas, cientos o miles de veces, todo depende de que
con que frecuencia la aplicación cambie.
Página | 364
365 | Página
Los tres principios de Redux
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.
Una de las restricciones de Redux es que, no existe una forma para actualizar el
estado directamente, en su lugar, es necesario enviar un action al Store,
describiendo las intenciones de actualizar el estado.
Los reducers deberán ser siempre funciones puras. Para que una función sea
pura, debe de cumplir con las siguientes características:
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.
Página | 366
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 para el 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 immutability-helper, por lo que no
debería de presentar una sorpresa para nosotros.
Otro de las cosas 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",
9. profile: {
10. id: "1234",
11. userName: "oscar",
12. name: "oscar blancarte"
13. }
14. }
367 | Página
Redux sin hooks
Algo de lo que no hablamos es, como utilizar Redux sin hooks, lo cual puede ser
importante si estamos dando mantenimiento a un proyecto más antiguo o que
por alguna razón se decidio trabajar con componentes de clase.
Antes que nada, la forma de trabajar con clases es muy parecido a como si lo
hiciéramos con hooks, con la única diferencia en que cambiamos la forma en que
nos suscribimos al store y como despachamos las acciones, veamos un ejemplo:
Lo primero que cambia es que ahora, tenemos que usar el fomoso infierno de los
envoltorios para retornar nuetro componente envuelto en el componenten
connect (línea 28). Connect es una función que retorna otra función, la cual a su
vez, recibe un componte como parámetro y finalmente retorna un nuevo
componente que envuelve a nuestro componente.
Página | 368
Por otra parte, mapDispatchToProps es muy parecido a usar useDispatch, con la
única diferencia de que mapDispatchToProps vaciará las operaciones como props.
369 | Página
Mini Twitter (Continuación 8)
Una vez que hemos explicado como funciona Redux, vamos a pasar a
implementarlo en nuestro proyecto. Sin embargo, me gustaría comentar algo,
hay personas que les gusta utilizar Redux en absolutamente toda la aplicación y
hay personas que lo reservan únicamente para las pantallas mas complejas. En
lo particular, a mi gusta utilizar Redux solo en las pantallas más complejas, donde
se requiere que varios componentes se comuniquen entre sí y dejar las páginas
más simples con el estado tradicional, pues utilizar Redux para todas las páginas
agrega una complejidad no necesaria.
Para completar lo que haremos en esta unidad, será necesario instalar las
siguiente dependencias.
Página | 370
Creando el Store y los reducers
El store tiene varios elementos adicionales que no son obligatorios, pero los
hemos agregado para habilitar alguna herramientas de debuger muy
interesantes que vamos a explicar más adelante, por lo que procederemos a
explicar que estamos haciendo.
Para la creación del store solo es necesario la línea 13, donde utilizamos el
método createStore para su creación, además, requeriría al menos un reducer
como parámetro, por ello le enviamos el reducer de la línea 14, fuera de esto, lo
demás es para agregar algunas características adicionales. Por ejemplo, la línea
6 la utilizamos para habilitar un plugin de Chrome para analizar en tiempo real
el estado del store, la línea 8 la utilizamos para agregar redux-thunk, una librería
que nos permite agregar acciones más avanzadas que explicaremos más
adelante, finalmente, la línea 10 es para agregar redux-logger, otra herramienta
de debuger para Redux. A medida que avancemos en el proyecto, iremos
analizando estas herramientas adicionales.
Si nos fijamos en la línea 14, vemos que estamos creando un reducer a partir del
archivo index.js que importamos en la línea 4, el cual es un nuevo archivo que
deberemos de crear en el path /app/Redux/reducer, el cual se ve de la siguiente
forma:
371 | Página
7. tweets: tweetsReduce
8. })
1. {
2. userPage: {
3. ...
4. }
5. tweets: {
6. ...
7. }
8. }
Ahora bien, la estructura interna del atributo UserPage y tweets dependerá del
valor retornado por el reducer correspondiente. Hora bien, este reducer se crea
a patir de userPageReducer y tweetsReducer, los cuales será necesario definir.
1. import {
2. USERPAGE_RESET,
3. LOAD_USERPROFILE,
4. LOAD_FOLLOWERS,
5. LOAD_FOLLOWINGS,
6. TOGGLE_EDIT_MODE,
7. FOLLOW_USER,
8. EDIT_PROFILE,
9. SAVE_PROFILE,
10. } from '../consts'
11. import update from 'immutability-helper'
12.
13. const initialState = {
14. edit: false,
15. profile: null,
16. followers: null,
17. followings: null
18. }
19.
20. export const userPageReducer = (state = initialState, action) => {
21. switch (action.type) {
22. case USERPAGE_RESET:
23. return initialState
24. case TOGGLE_EDIT_MODE:
25. if (!state.edit) {
26. //change to edit mode and backup current state
27. return update(state, {
28. edit: { $set: !state.edit },
29. profileBackup: { $set: state.profile }
30. })
31. } else {
32. //cancel edir mode and restore previous state
Página | 372
33. return update(state, {
34. edit: { $set: !state.edit },
35. profile: { $set: state.profileBackup }
36. })
37. }
38. case SAVE_PROFILE:
39. return update(state, {
40. edit: { $set: false },
41. profileBackup: { $set: undefined }
42. })
43. case LOAD_USERPROFILE:
44. return update(state, {
45. profile: { $set: action.value }
46. })
47. case FOLLOW_USER:
48. return update(state, {
49. profile: {
50. follow: { $set: action.value }
51. }
52. })
53. case EDIT_PROFILE: {
54. return update(state, {
55. profile: {
56. [action.value.id]: { $set: action.value.value }
57. }
58. })
59. }
60. case LOAD_FOLLOWERS: {
61. return update(state, {
62. followers: { $set: action.value }
63. })
64. }
65. case LOAD_FOLLOWINGS: {
66. return update(state, {
67. followings: { $set: action.value }
68. })
69. }
70. default:
71. return state
72. }
73. }
74. export default userPageReducer
1. import {
2. LOAD_TWEETS,
3. ADD_NEW_TWEET,
373 | Página
4. LIKE_TWEET_REQUEST,
5. RESET_TWEETS
6. } from '../consts'
7.
8. import update from 'immutability-helper'
9.
10. const initialState = {
11. tweets: [],
12. hasMore: false
13. }
14.
15. export const tweetsReducer = (state = initialState, action) => {
16. switch (action.type) {
17. case LOAD_TWEETS:
18. let newState = action.reset
19. ? action.tweets
20. : update(state.tweets, { $push: action.tweets })
21.
22. return {
23. tweets: newState,
24. hasMore: action.tweets.length >= 10
25. }
26. case LIKE_TWEET_REQUEST:
27. let targetIndex =
28. state.tweets.map(x => { return x._id }).indexOf(action.tweetId)
29. return update(state, {
30. tweets: {
31. [targetIndex]: {
32. likeCounter: { $set: action.likeCounter },
33. liked: { $apply: (x) => { return !x } }
34. }
35. }
36. })
37. case ADD_NEW_TWEET:
38. return update(state, {
39. tweets: { $splice: [[0, 0, action.value]] }
40. })
41. case RESET_TWEETS:
42. return initialState
43. default:
44. return state
45. }
46. }
47.
48. export default tweetsReducer
Algo de lo que tenemos que tener claro es que, cuando un action sea despachado,
todos los reducers recibirán el action, por lo que es posible que más de un
reducer pueda actuar en consecuencia, aunque lo más normal es que un solo
reducer procesa cada action.
Quiero que observes que hemos estado utilizando una serie de constantes para
los cases del switch, esto lo hacemos así para asegurarnos de tener un mejor
control de las acciones que podemos mandar y no tener un error al mandar el
type, es por ello que vamos a crear el archivo consts.js en el path /app/redux:
Página | 374
1. // UserPage
2. export const USERPAGE_RESET = 'USERPAGE_RESET'
3. export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE'
4. export const LOAD_USERPROFILE = 'LOAD_USERPROFILE'
5. export const LOAD_FOLLOWERS = 'LOAD_FOLLOWERS'
6. export const LOAD_FOLLOWINGS = 'LOAD_FOLLOWINGS'
7. export const FOLLOW_USER = 'FOLLOW_USER'
8. export const EDIT_PROFILE = 'EDIT_PROFILE'
9. export const SAVE_PROFILE = 'SAVE_PROFILE'
10.
11. // Tweets
12. export const RESET_TWEETS = 'RESET_TWEETS'
13. export const LOAD_TWEETS = 'LOAD_TWEETS'
14. export const ADD_NEW_TWEET = 'ADD_NEW_TWEET'
15. export const LIKE_TWEET_REQUEST = 'LIKE_TWEET_REQUEST'
Implementando el Provider
Una vez que el store ya está listo, procederemos a crear el objeto Provider, con
la intención de que nuestros componentes se puedan registrar a los cambios en
el estado del store. Para esto, vamos a regresar al archivo TwitterApp.js y
agregar las siguientes líneas:
375 | Página
29. <AuthRoute isLoged={user != null} exact path="/"
30. component={TwitterDashboard} />
31. <Route exact path="/signup" component={Signup} />
32. <Route exact path="/login" component={Login} />
33. <AuthRoute isLoged={user != null} path="/:user"
34. component={UserPage} />
35. </Switch>
36. </div>
37. </Provider>
38. </UserContext.Provider>
39. )
40. }
41.
42. return render()
43. }
44. export default TwitterApp
Redux logger
Redux logger es un módulo que permite debugear el store a media que los
actions son depachados al store. De esta forma, el plugin imprimirá en la consola
el valor del estado previo a un action, el action despachado y el nuevo valor del
estado una vez que el action ha sido aplicado.
Página | 376
3. import { createLogger } from 'redux-logger'
4. import reducers from '../redux/reducers/index'
5.
6. const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
7.
8. const middleware = [thunk];
9. if (process.env.NODE_ENV !== 'production') {
10. middleware.push(createLogger());
11. }
12.
13. const store = createStore(
14. reducers,
15. composeEnhancers(
16. applyMiddleware(...middleware)
17. )
18. )
19. export default store
Lo que hay que hacer el momento de crear el store es muy simple, ya que solo
tenemos que importar el método createLogger (línea 3), y condicionar su registro
(línea 10) para que solo se ejecute cuando la aplicación corra en un entorno no
productivo (Al final del libro hablaremos de como correr la aplicación en
producción). Finalmente, agregamos el logger cuando creamos el store (línea
16). Y eso será todo.
Para comprobar como esta librería funciona, basta con ir al navegador, abrir la
consola del navegador y dirigirse al perfil de cualquier usuario:
Ahora bien, si expandimos cada uno de los tres nodos, podremos ver el detalle,
de cada uno:
377 | Página
Estos registros se imprimierán en la consola cada vez que una acción sea
depachada al store, así que es una excelente forma de saber que está pasando
dentro del store y como el estado se esta afectando a medida que las acciones
son despachadas.
Redux DevTool
Página | 378
5.
6. const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
7.
8. const middleware = [thunk];
9. if (process.env.NODE_ENV !== 'production') {
10. middleware.push(createLogger());
11. }
12.
13. const store = createStore(
14. reducers,
15. composeEnhancers(
16. applyMiddleware(...middleware)
17. )
18. )
19. export default store
Si hicimos todo bien, deberemos ver como el ícono del plugin se habilita en el
navegador, y al dar click en, podremos ver en tiempo real el valor del store:
En esta nueva pantalla podremos hacer mucho más cosas que solo ver el estado
actual, si no que del lado izquierdo podemos ver las acciones lanzadas, y en la
parte de abajo tenemos como un reproductor, que nos permite retroceder y
adelantar entre los diferentes estado de la aplicación, de esta forma podemos
analizar con más cuidado y ver como la aplicación se actualiza a medida que
retrocedemos o adelantamos el estado.
379 | Página
Migrando el componente UserPage
Para realizar la migración de un componente hay que seguir una serie de pasos,
los cuales se repetirán para todos los componentes que migremos a Redux, los
cuales son:
Página | 380
26. dispath(resetTweets())
27. dispath(rest())
28. dispath(loadProfile(props.match.params.user))
29.
30. return () => {
31. dispath(rest())
32. }
33. }, [props.match.params.user])
34.
35. const handleImageChange = (e) => {
36. e.preventDefault();
37.
38. let file = e.target.files[0];
39. if (file.size > 1240000) {
40. alert('La imagen supera el máximo de 1MB')
41. return
42. }
43.
44. let reader = new FileReader()
45. reader.onloadend = () => {
46. dispath(handleInput(e.target.id, reader.result))
47. }
48. reader.readAsDataURL(file)
49. }
50.
51.
52. const handleInputChange = (e) => {
53. let id = e.target.id
54. let value = e.target.value
55. dispath(handleInput(id, value))
56. }
57.
58. const render = () => {
59. if(profile === null) return null
60.
61. return (
62. <div id="user-page" className="app-container animated fadeIn">
63. <header className="user-header">
64. <div className="user-banner" style={{
65. backgroundImage: 'url(' + (profile.banner) + ')' }}>
66. <If condition={edit}>
67. <div>
68. <label htmlFor="banner" className="btn select-banner">
69. <i className="fa fa-camera fa-2x" aria-hidden="true"></i>
70. <p>Cambia tu foto de encabezado</p>
71. </label>
72. <input href="#" className="btn"
73. accept=".gif,.jpg,.jpeg,.png"
74. type="file" id="banner"
75. onChange={handleImageChange} />
76. </div>
77. </If>
78. </div>
79. <div className="user-summary">
80. <div className="container-fluid">
81. <div className="row">
82. <div className="hidden-xs col-sm-4 col-md-push-1
83. col-md-3 col-lg-push-1 col-lg-3" >
84. </div>
85. <div className="col-xs-12 col-sm-8 col-md-push-1
86. col-md-7 col-lg-push-1 col-lg-7">
87. <ul className="user-summary-menu">
88. <li>
89. <NavLink to={`/${profile.userName}`} exact
90. activeClassName="selected">
91. <p className="summary-label">TWEETS</p>
381 | Página
92. <p className="summary-value">{profile.tweetCount}</p>
93. </NavLink>
94. </li>
95. <li>
96. <NavLink to={`/${profile.userName}/following`}
97. activeClassName="selected">
98. <p className="summary-label">SIGUIENDO</p>
99. <p className="summary-value">{profile.following}</p>
100. </NavLink>
101. </li>
102. <li>
103. <NavLink to={`/${profile.userName}/followers`}
104. activeClassName="selected">
105. <p className="summary-label">SEGUIDORES</p>
106. <p className="summary-value">{profile.followers}</p>
107. </NavLink>
108. </li>
109. </ul>
110.
111. <If condition={profile.userName === userContext.userName}>
112. <button className="btn btn-primary edit-button"
113. onClick={() =>
114. dispath(edit ? save() : toggleEditMode())} >
115. {edit ? "Guardar" : "Editar perfil"}</button>
116. </If>
117.
118.
119. <If condition={profile.follow != null &&
120. profile.userName !== userContext.userName} >
121. <button className="btn edit-button"
122. onClick={() => dispath(follow())} >
123. {profile.follow
124. ? (<span><i className="fa fa-user-times"
125. aria-hidden="true"></i> Siguiendo</span>)
126. : (<span><i className="fa fa-user-plus"
127. aria-hidden="true"></i> Seguir</span>)
128. }
129. </button>
130. </If>
131.
132. <If condition={edit}>
133. <button className="btn edit-button" onClick=
134. {() => dispath(toggleEditMode())} >Cancelar</button>
135. </If>
136. </div>
137. </div>
138. </div>
139. </div>
140. </header>
141. <div className="container-fluid">
142. <div className="row">
143. <div className="hidden-xs col-sm-4 col-md-push-1 col-md-3
144. col-lg-push-1 col-lg-3" >
145. <aside id="user-info">
146. <div className="user-avatar">
147. <Choose>
148. <When condition={edit} >
149. <div className="avatar-box">
150. <img src={profile.avatar} />
151. <label htmlFor="avatar"
152. className="btn select-avatar">
153. <i className="fa fa-camera fa-2x"
154. aria-hidden="true"></i>
155. <p>Foto</p>
156. </label>
157. <input href="#" id="avatar"
Página | 382
158. className="btn" type="file"
159. accept=".gif,.jpg,.jpeg,.png"
160. onChange={handleImageChange}
161. />
162. </div>
163. </When>
164. <Otherwise>
165. <div className="avatar-box">
166. <img src={profile.avatar} />
167. </div>
168. </Otherwise>
169. </Choose>
170. </div>
171. <Choose>
172. <When condition={edit} >
173. <div className="user-info-edit">
174. <input maxLength="20" type="text" value={profile.name}
175. onChange={handleInputChange} id="name" />
176. <p className="user-info-username">
177. @{profile.userName}</p>
178. <textarea maxLength="180" id="description"
179. value={profile.description}
180. onChange={handleInputChange} />
181. </div>
182. </When>
183. <Otherwise>
184. <div>
185. <p className="user-info-name">{profile.name}</p>
186. <p className="user-info-username">
187. @{profile.userName}</p>
188. <p className="user-info-description">
189. {profile.description}</p>
190. </div>
191. </Otherwise>
192. </Choose>
193. </aside>
194. </div>
195. <div className="col-xs-12 col-sm-8 col-md-7
196. col-md-push-1 col-lg-7">
197. <Switch>
198. <Route exact path="/:user" component={
199. () => <MyTweets profile={profile} />} />
200. <Route exact path="/:user/followers" component={
201. () => <Followers profile={profile} />} />
202. <Route path="/:user/following" component={
203. () => <Followings profile={profile} />} />
204. <Route exact path="/:user/tweet/:tweet" component={
205. (params) => <Modal> <TweetDetail {...params} /> </Modal>} />
206. </Switch>
207. </div>
208. </div>
209. </div>
210. </div>
211. )
212. }
213.
214. return render()
215. }
216. export default UserPage;
El primer paso, era conectar el componente con el store, por eso, en la línea 22
y 23 utilizamos el hook useSelector para recupera el estado del store, de esta
forma, eliminamos la dependencia a un estado interno, y en su lugar, dejamos
383 | Página
que el hook useSelector nos actualize cada vez que el store sea modificado por
una acción.
El otro paso, es modificar todas las llamadas a donde actualizemos el estado, por
un dispatch, con la intención de que sean los reducer quienes procesen los
actions y actualicen el estado por nosotros. Podemos ver como hacemos esto en
las líneas 46 y 55, que es donde gestionamos los cambios de las imágenes del
perfil o el nombre y descripción.
Página | 384
31.
32. export const follow = () => (dispatch, getState) => {
33. let request = {
34. followingUser: getState().userPage.profile.userName
35. }
36. APIInvoker.invokePOST('/secure/follow', request, response => {
37. if (response.ok) {
38. dispatch({
39. type: FOLLOW_USER,
40. value: !response.unfollow
41. })
42. }
43. }, error => {
44. console.log("Error al actualizar el perfil");
45. })
46. }
47.
48. export const toggleEditMode = () => (dispatch, getState) => {
49. dispatch({
50. type: TOGGLE_EDIT_MODE
51. })
52. }
53.
54. export const handleInput = (field, value) => (dispatch, getState) => {
55. dispatch({
56. type: EDIT_PROFILE,
57. value: {
58. id: field,
59. value
60. }
61. })
62. }
63.
64. export const save = () => (dispatch, getState) => {
65. const profile = getState().userPage.profile
66.
67. let request = {
68. username: profile.userName,
69. name: profile.name,
70. description: profile.description,
71. avatar: profile.avatar,
72. banner: profile.banner
73. }
74.
75. APIInvoker.invokePUT('/secure/profile', request, response => {
76. dispatch({
77. type: SAVE_PROFILE
78. })
79. }, error => {
80. console.log("Error al actualizar el perfil");
81. })
82. }
83.
84.
85. export const loadFollowers = () => (dispatch, getState) => {
86. let username = getState().userPage.profile.userName
87. APIInvoker.invokeGET(`/followers/${username}`, response => {
88. dispatch({
89. type: LOAD_FOLLOWERS,
90. value: response.body
91. })
92. }, error => {
93. console.log("Error en la autenticación");
94. })
95. }
96.
385 | Página
97. export const loadFollowings = () => (dispatch, getState) => {
98. let username = getState().userPage.profile.userName
99. APIInvoker.invokeGET(`/followings/${username}`, response => {
100. dispatch({
101. type: LOAD_FOLLOWINGS,
102. value: response.body
103. })
104. }, error => {
105. console.log("Error en la autenticación");
106. })
107. }
1. import {
2. LOAD_TWEETS,
3. RESET_TWEETS,
4. ADD_NEW_TWEET
5. } from '../consts'
6. import APIInvoker from '../../utils/APIInvoker'
Página | 386
7.
8. export const resetTweets = () => (dispatch, getState) => {
9. dispatch({
10. type: RESET_TWEETS
11. })
12. }
13.
14. export const getTweet = (username, onlyUserTweet, page) =>
15. (dispatch, getState) => {
16. let currentPage = page || 0
17. const url = `/tweets${onlyUserTweet ? "/" + username : ""}?page=${currentPage
}`
18. APIInvoker.invokeGET(url, response => {
19. dispatch({
20. type: LOAD_TWEETS,
21. tweets: response.body,
22. reset: currentPage == 0
23. })
24.
25. }, error => {
26. console.log("Error al cargar los Tweets")
27. })
28. }
29.
30. export const addTweet = (newTweet) => (dispatch, getState) => {
31. APIInvoker.invokePOST('/secure/tweet', newTweet, response => {
32. dispatch({
33. type: ADD_NEW_TWEET,
34. value: {
35. ...newTweet,
36. _id: response.tweet._id
37. }
38. })
39. }, error => {
40. console.log("Error al cargar los Tweets");
41. })
42. }
387 | Página
1. import React, { useEffect } from 'react'
2. import UserCard from './UserCard'
3. import PropTypes from 'prop-types'
4. import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
5. import { useDispatch, useSelector } from 'react-redux'
6. import { loadFollowers } from './redux/actions/userPageActions'
7.
8. const Followers = (props) => {
9.
10. const dispath = useDispatch()
11. const state = useSelector(state => state.userPage.followers)
12.
13. useEffect(() => {
14. if (state === null) {
15. dispath(loadFollowers())
16. }
17. }, [props.profile.userName])
18.
19. return (
20. <section>
21. <div className="container-fluid no-padding">
22. <div className="row no-padding">
23. <CSSTransitionGroup
24. transitionName="card"
25. transitionEnter={true}
26. transitionEnterTimeout={500}
27. transitionAppear={false}
28. transitionAppearTimeout={0}
29. transitionLeave={false}
30. transitionLeaveTimeout={0}>
31. <For each="user" of={state || []}>
32. <div className="col-xs-12 col-sm-6 col-lg-4"
33. key={user._id}>
34. <UserCard user={user} />
35. </div>
36. </For>
37. </CSSTransitionGroup>
38. </div>
39. </div>
40. </section>
41. )
42. }
43.
44. Followers.propTypes = {
45. profile: PropTypes.object
46. }
47.
48. export default Followers;
Página | 388
Migrando el componente Followings
389 | Página
50.
51. export default connect(mapStateToProps, {loadFollowings})(Followings)
Observa que el método connect (llínea 51) recibe dos parámetros, y después nos
retorna una nueva función, esta nueva función recibe como parámetro un
componente, es por ello que vemos dos pares de paréntesis.
En este punto ya hemos migrado la sección del perfil del usuario junto con las
secciones de los seguidores y las personas que seguimos, sin embargo, falta por
migrar la sección de los tweets, sin embargo, este componente se reutiliza en la
sección de Mis Tweets y en el Home de cada usuario, por lo que al final, las dos
secciones utilizan el mismo componente TweetsContainer para mostrar los datos,
aunque la información que se muestra varía según la sección en la que lo
mostrarmos.
Página | 390
19. dispatch(getTweet(username, onlyUserTweet, 0))
20. }, [props.profile.userName, props.onlyUserTweet])
21.
22. const addNewTweet = (newTweet) => {
23. dispatch(addTweet(newTweet))
24. }
25.
26. const loadMore = (page) => {
27. const username = props.profile.userName
28. const onlyUserTweet = props.onlyUserTweet
29. dispatch(getTweet(username, onlyUserTweet, page - 1))
30. }
31.
32. return (
33. <main className="twitter-panel">
34. <Choose>
35. <When condition={props.onlyUserTweet} >
36. <div className="tweet-container-header">
37. TweetsDD
38. </div>
39. </When>
40. <Otherwise>
41. <Reply profile={props.profile} operations={{ addNewTweet }} />
42. </Otherwise>
43. </Choose>
44. <InfiniteScroll
45. pageStart={1}
46. loadMore={loadMore}
47. hasMore={hasMore}
48. loader={<div className="loader" key={0}>Loading ...</div>} >
49.
50. <For each="tweet" of={tweets}>
51. <Tweet key={tweet._id} tweet={tweet} />
52. </For>
53. </InfiniteScroll>
54. <If condition={!hasMore} >
55. <p className="no-tweets">No hay tweets que mostrar</p>
56. </If>
57. </main>
58. )
59.
60. }
61.
62. TweetsContainer.propTypes = {
63. onlyUserTweet: PropTypes.bool,
64. profile: PropTypes.object
65. }
66.
67. TweetsContainer.defaultProps = {
68. onlyUserTweet: false,
69. profile: {
70. userName: ""
71. }
72. }
73.
74. export default TweetsContainer;
391 | Página
de un determinado usuario, si es true, entonces tomamos el parámetro userName
para consultar solo los tweets de ese usuaro.
Por otra parte, hemos creado un efecto (línea 16) que se encargará despachar
las acciones necesarias para cargar los tweets desde el API, y hemos
condicionado el efecto para que solo se ejecute cuando las dos propiedades que
explicamos hace un momento cambien.
Página | 392
16. return () => dispatch(resetTweets())
17. })
18.
19. return (
20. <div id="dashboard" className="animated fadeIn">
21. <div className="container-fluid">
22. <div className="row">
23. <div className="hidden-xs col-sm-4 col-md-push-1
24. col-md-3 col-lg-push-1 col-lg-3" >
25. <Profile profile={userContext} />
26. </div>
27. <div className="col-xs-12 col-sm-8 col-md-push-1
28. col-md-7 col-lg-push-1 col-lg-4">
29. <TweetsContainer profile={userContext} onlyUserTweet={false}/>
30. </div>
31. <div className="hidden-xs hidden-sm hidden-md
32. col-lg-push-1 col-lg-3">
33. <SuggestedUser />
34. </div>
35. </div>
36. </div>
37. </div>
38. )
39. }
40.
41. export default TwitterDashboard;
393 | 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 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.
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 | 394
Introducción a NodeJS
Capítulo 15
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.
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.
395 | Página
Fig. 94 - Posiciones abiertas
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 | 396
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.
Introducción a Express
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
397 | 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:
El archivo package.json
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.
Página | 398
name
version
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
399 | 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
reimplementación 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
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" : "project@hostname.com"
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 | 400
license
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
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:
401 | Página
Fig. 95 - Carpeta node_module en Atom
Lo interesante es que en esta carpeta podremos ver todos los módulos que hemos
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.
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.
Para aprender desde cero, crearemos un nuevo archivo llamado express-
server.js con la intención de hacer algunas pruebas. La intención es usar este
nuevo archivo solo durante este capítulo, después de esto, podremos borrarlo.
Página | 402
7.
8. app.listen(8181, function () {
9. console.log('Example app listening on port 8181!');
10. });
Para hacer funcionar este servidor solo basta con ejecutar el siguiente comando
desde la carpeta raíz del proyecto:
node ./express-server.js
Tan solo con estas pocas líneas hemos creado un servidor Express que responde
en el puerto 8181. Veamos que está pasando, en la línea 1 estamos importando
el módulo de Express, el cual instalamos mediante el comando npm install --
sav-deve express. En la línea 2, estamos creando una nueva instancia de Express
que ponemos en la variable app (línea 2).
403 | Página
Express Verbs
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.
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.
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
O Postman.
Página | 404
Método PUT
Método DELETE
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.
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:
405 | Página
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.
Ahora bien. Para probar esto, podemos utilizar SoapUI y ejecutar la URL
http://localhost:8181 en los cuatro métodos disponibles:
Página | 406
Fig. 97 - Probando el método GET
407 | Página
Fig. 99 - Probando el método PUT.
Página | 408
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.
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 hacer o donde te van a enviar.
http://api.com/users/oscar/profile
Si analizamos esta URL, nos da mucha información, pues cada sección de la URL
nos da una pista de lo que hace. En este caso, queda claro que estamos
consultando el perfil del usuario oscar.
Query params
http://api.com/?param1=val1¶m2=val2¶m3=val3
409 | Página
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:
http://localhost:8181/?name=Oscar&lastname=Blancarte
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/users/test/profile, en esta URL, test, es un URL Param, y
puede ser recuperado para ser utilizado como un parámetro.
Página | 410
Actualizaremos nuevamente el archivo express-server.js y agregaremos las
siguientes líneas:
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, ya 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.
http://localhost:8181/Oscar/Blancarte
Body params
411 | Página
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 recibe
el mensaje primero que los Router y procese el payload, para el final dejarlo en
el objeto request. Esto quedaría de la siguiente manera:
Más adelante veremos qué es esto de los Middleware, pero por ahora, 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 parsear el payload, mediante
el cual es posible convertir el payload a el formato que necesitemos y dejarlo
disponible en el objeto request.
Página | 412
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.
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. });
413 | Página
Fig. 103 - Express parse-body
Middleware
Página | 414
Fig. 104 - Ejecución en cadena de Middleware.
415 | Página
• Middleware de nivel de aplicación
• Middleware de nivel direccionador
• Middleware de terceros
• Middleware incorporado
• Middleware de manejo de errores
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.
Página | 416
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 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
Middleware incorporado
417 | Página
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')));
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.
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().
Página | 418
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).
419 | Página
Resumen
Hemos aprendido las distintas formas que tiene express para recibir parámetros,
mediante url params, query params y body param o payload.
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.
Página | 420
Introducción a MongoDB
Capítulo 16
Como siempre, me gusta explicar por qué MongoDB es una tecnología que vale
la pena aprender y cómo es que 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.
421 | Página
Fig. 105 - 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, sino 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:
Página | 422
Fig. 106 - Posiciones de trabajo abiertas.
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 MongoDB al 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 muchas de ellas ya están
empezando a migrar partes de sus aplicaciones a MongoDB, por lo que se espera
que, en los próximos años, las bases de datos NoSQL tomen mucha 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.
423 | Página
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 y 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
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
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.
Instalando MongoDB
Lo primero que deberemos hacer será crea una cuenta en MongoDB Atlas:
Página | 424
Fig. 107 - Crear cuenta en MongoDB
Presionaremos el botón que dice “Try free” para iniciar el registro. Nos llevará a
un pequeño formulario en donde tendremos que capturar algunos datos para
crear la cuenta.
En esta nueva pantalla daremos click en el botón verde que dice “Build a
Cluster”, lo que nos llevará a seleccionar el paquete que queremos adquirir, por
425 | Página
supuesto, seleccionaremos la opción “Free”, lo que nos llevará finalmente a la
página de la configuración del Cluster.
Importante
En esta nueva pantalla vamos a dejar todos los valores que viene por default,
con la única excepción del Cluster Name, al cual le pondremos Mini-Twitter,
finalmente presionamos el botón verde que dice “Create Cluster”.
El proceso de creación del cluster puede tardar algunos minutos, por lo que
deberemos de ser pacientes. Una vez terminado el proceso veremos algo como
lo siguiente:
Página | 426
El siguiente paso será crear un usuario para conectarnos a la base de datos, el
cual es diferente al usuario que creamos para crear el cluster. Para crear el
usuario de la base de datos tendremos que dar click en la opción “Database
Access” que se encuentra en las opciones del lado izquierdo y luego en el botón
“Add new database user”:
En la siguiente pantalla tendremos que crear al nuevo usuario, por lo que nos
aseguramos de seleccionar “password” como método de autenticación, y darle
acceso de escritura y lectura a todas las bases de datos. Finalmente, ponemos
como nombre usuario “twitter” y ponemos un password.
427 | Página
Fig. 109 - Captura de usuario y password para crear la base de datos.
Una vez que el usuario está creado, es hora de configurar desde que IP’s nos
podemos conectar, por lo que nos dirigiremos a la sección “Network Access” que
se encuentra en el menú del lado izquierdo:
Página | 428
En la nueva pantalla presionaremos el botón “ALLOW ACCESS FROM ANYWHERE”, es
decir, aceptar el acceso desde cualquier IP. Finalmente presionamos el botón
“Confirm”
Te recomiendo que 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
características que nos da gratis es más que suficiente para empezar.
429 | Página
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/
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.
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.
Página | 430
Fig. 110 - Download MongoDB Compass
431 | Página
Fig. 111 – Descargando MongoDB Compass.
Página | 432
Fig. 112 - 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 avancemos 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.
node ./AnimalMongoDBTest.js:
433 | Página
Que son los Schemas de Mongoose
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:
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.
Página | 434
indicar si se trata de un index. En estos casos, debemos crear un nuevo objeto y
definir todas las propiedades requeridas separadas por coma (,) como en el
primero ejemplo.
En este nuevo ejemplo, podemos ver que creamos una estructura en blanco y
pasar como segundo argumento la propiedad strict en false. Con tan solo hacer
esto, 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.
435 | Página
Statics methods
Las funciones estáticas permiten definir funciones a nivel del schema, que pueden
ser invocadas sin necesidad de tener una instancia del Schema
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.
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.
Query Helpers
Página | 436
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.
5. var Animal = mongoose.model('Animal', animalSchema);
6. Animal.find().byName('fido').exec(function(err, animals) {
7. console.log(animals);
8. });
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);
Plugins
1. //Global plugin
2. mongoose.plugin(<plugin>, <params>)
3.
4. //Schema plugin
437 | Página
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.
Por lo voy a dejar una liga a todos los plugin que tenemos disponibles y más
adelante veremos cómo implementar un plugin.
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.
Página | 438
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. })
El segundo paso crear los parámetros de conexión con MongoDB, los cuales
vemos en la línea 5, donde creamos un objeto de configuración para establecer
la conexión a la base de datos, este objeto 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.
439 | Página
reconnectInterval Ligada a autoReconnect, determina el tiempo que esperará
entre cada intento de reconexión(reconnectTries)
1. module.exports = {
2. mongodb: {
3. connectionString: "<CONNECTION_STRING"
4. }
5. }
Ahora bien, para conectarnos a MongoDB, será necesario crear nuestra cluster
de MongoDB, pero esto lo analizaremos en a continuación:
Página | 440
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.
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
tweets. 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:
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 a los usuarios y los tweets por separado, pero en ningún momento,
definimos la estructura que puede tener una colección.
441 | Página
Que son los Documentos
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. }
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.
Página | 442
Una vez allí, presionamos el botón “CREATE COLLECTION” y nos arrojará una nueva
pantalla:
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:
443 | Página
Fig. 116 - 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.
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.
Página | 444
Fig. 118 - 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”:
Así de fácil es crear un documento, claro que estos documentos son simples, pero
cualquier valor podrá contener otro objeto dentro.
445 | 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.
Eliminar un documento
Página | 446
Consultar un documento
447 | 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.
Filter
Página | 448
Fig. 123 - 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
449 | 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:
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.
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.
Operador OR
Página | 450
Fig. 125 - 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.
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”:
451 | Página
Fig. 126 - 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.
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 | 452
Fig. 127: operador NOT
Operadores de comparación
Operador Descripción
$eq Valida si un campo es igual a un valor determinado, su
nombre proviene de “equals”.
453 | Página
1. { <field>: { $ne: <value> } }
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 | 454
Fig. 128 - 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:
455 | 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.
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.
$all Este operador valida si un array del documento contiene todos los
valores solicitados.
Caso 1:
Página | 456
Fig. 130 - 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:
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:
457 | Página
Caso 3:
Otro ejemplo sería buscar sobre los elementos de un array con ayuda del
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:
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:
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 | 458
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.
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:
Ahora bien, si queremos que no nos regrese el _id, hay que decirle explícitamente
de la siguiente manera:
459 | Página
Fig. 136 - 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:
Página | 460
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.
461 | 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:
Ahora bien, imaginemos que queremos ordenar por nombre ascendente y color
descendente:
Página | 462
Fig. 141 - Ordenamiento ascendente y descendente.
Podemos ver qué cambio el cuarto registro, pues el orden decreciente del color
provoco un reordenamiento.
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.
463 | Página
Fig. 142 - 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:
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 | 464
Fig. 144 - Ejemplo de paginación en bloques de 3.
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 respuestas 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.
465 | Página
17. var Tweet = mongoose.model('Tweet', tweet);
18. module.exports= Tweet
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
Página | 466
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
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.
467 | Página
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.
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
Save
Página | 468
Observemos que el método save se ejecuta sobre el objeto myTweet, el cual es
una instancia del schema,
Create
Cabe mencionar, que los dos ejemplos que mostramos a continuación, dan el
mismo resultado.
Find
Método find
Método findOne
469 | Página
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.
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){
7. //Any action
8. })
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
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.
Página | 470
1. Profile.update(
2. {userName: 'oscar'},
3. {name: 'Oscar Blancarte, description: 'Nuevo en Twitter'}
4. function( err, response){
5. //Any action
6. })
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. }
Método updateMany
1. Profile.updateMany(
2. {userName: 'oscar'},
3. {name: 'Oscar Blancarte, description: 'Nuevo en Twitter'}
4. function( err, response){
5. //Any action
6. })
471 | Página
Método save
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.
Remove
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.
Página | 472
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. })
1. Tweet.find({})
2. .populate("_creator")
3. .exec(function(err, tweets){
4. //Any action
5. })
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. }
1. {
2. "_id": "5a0657ad3ccd98529d83a9b9",
3. "_creator": {
4. "_id": "5a05286db5371dffe40bafae",
5. "name": "Jaime",
6. "userName": "jaime",
7. "avatar": "<img base64>"
8. },
473 | Página
9. "date": "2017-11-11T01:51:41.421Z",
10. "message": "test",
11. "likeCounter": 0,
12. "replys": 0,
13. "image": null
14. }
Página | 474
Resumen
475 | Página
Desarrollo de API REST con
NodeJS
Capítulo 17
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:
Página | 476
• 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.
REST vs SOA
477 | Página
Para responder esta pregunta, es necesario entender que es SOA. SOA es un
estilo 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.
El problema con este servidor es que nos permite hacer cosas muy básicas, que
para correr una aplicación de React es más que suficiente, sin embargo, si
necesitamos construir un API REST, entonces las cosas se complican, por lo que
Página | 478
estamos obligados a construir nuestro propio servidor y personalizarlo como
mejor se adapte a nuestras necesidades.
Por otro lado, nodemon nos permite reiniciar automáticamente el servidor cada
vez que detecta un cambio en el código, un poco contradictorio con lo que hace
Webpack, sin embargo, debemos de tener en cuenta que webpack solo compila
los archivos de la app, es decir, todo los que tenga que ver con React, por lo
tanto, cada vez que realicemos un cambio en los servicios, entonces no se
reflejarán sino hasta reiniciar el servidor, y como esto puede ser una terea
bastante molesta, el módulo nodemon se encarga precisamente de detectar los
cambios y realizar el reinicio automático del servidor.
Finalmente, serán necesaras todas librerías que ya conocemos de Babel para que
webpack pueda realizar la transpilación, sin embargo, no veo necesarios
mencionarlas en este punto.
479 | Página
Configuración inicial del servidor
Página | 480
En las líneas 19 a 21 creamos un router para atender todas las peticiones en el
método GET sin importar el path (/*), el cual regresará el archivo index.html.
Con esto nos aseguraremos que sin importar a que URL entremos, la aplicación
se montará, sin embargo, recordemos que al final, React Router determinar que
componentes mostrar dependiendo la URL.
En este punto nuestro servidor ya debería de estar operativo, por lo que solo
restaría ejecutarlo con el comando:
node ./server.js
481 | Página
reflejando, pero en realidad, nunca los aplicamos. Para solucionar este problema,
tenemos nodemon, el cual nos permite ejecutar la aplicación exactamente igual
que con el comando node, con la única diferencia de que nodemon reiniciar el
servidor cada vez que apliquemos un cambio.
Para ejecutar la aplicación con nodemon es necesario primero que nada instarlos
como librería global, que fue lo que hicimos al comienzo de esta unidad, el
siguiente paso es ejecutar comando:
nodemon ./server.js
Página | 482
42. app.listen(configuration.server.port, function () {
43. console.log(`Example app listening on port ${ configuration.server.port}!`)
44. });
1. module.exports = {
2. server: {
3. port: 8080
4. },
5. mongodb: {
6. connectionString: "<CONNECTION_STRING>"
7. }
8. }
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:
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).
483 | Página
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:
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.
Página | 484
Implementando un Virtual Host en NodeJS
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:
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:
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.
485 | Página
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
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')
Página | 486
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 {
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,
487 | Página
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;
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;
Página | 488
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{
148. margin-left: 20px;
149. }
150.
151. .label{
152. margin-left: 20px;
153. font-size: 12px;
154. }
Una vez hecho esto, tendremos que regresar al archivo server.js y agregar solo
las líneas marcadas:
489 | Página
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.
Para solucionar este problema, será necesario hacer un pequeño ajuste a nivel
de configuración de sistema operativo, con la intención de que identifique el
subdominio api como parte del localhost, para esto, tendremos que hacer un
procedimiento diferente según nuestro sistema operativo:
En Windows:
1. 127.0.0.1 api.localhost
En Linux
Página | 490
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
Configurar subdominio
491 | Página
Cross-origin resource sharing (CORS)
Con este simple paso, el API ya podrá ser invocado desde la aplicación.
Página | 492
Desarrollo del API REST
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ñada,
puede decirle al usuario que hace sin necesidad de leer la documentación.
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:
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
493 | Página
URL simples, compactas y concretas
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
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:
Página | 494
En REST hay una infinidad de código de respuesta, por lo que mencionar todos
aquí, está de más, así que solo nos limitamos a los más importantes y que
corresponden a más del 95% de los casos. Si aun así te queda la curiosidad de
conocer todos los demás códigos, puedes darte una vuelta por el siguiente
enlace: códigos de respuesta.
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 para apuntar a nuestra API.
1. module.exports = {
2. debugMode: false,
3. api: {
4. host: "http://api.localhost:8080"
5. },
6. tweets: {
7. maxTweetSize: 140
8. }
9. }
En esta sección pasaremos de lleno a implementar los servicios REST que le dan
vida a la aplicación Mini Twitter.
Antes de comenzar
495 | Página
las cuales nos permite trabajar de una mejor forma con los procesos asíncronos,
por lo que si no sabes que son las promesas, te invito a que veas uno de mis
videos donde lo explico: https://www.youtube.com/watch?v=3-jCVorlCZs
Así como las promesas nos permite trabajar mejor con todos los procesos
asíncronos, también tenemos el problema de manejar las callback, lo que hace
sumamente difícil de dar mantenimiento al código, por lo que vamos a utilizar la
instrucción Async/Await para controlar los procesos asíncronos de forma
síncrona, por lo que si tampoco sabes qué es esto de Async/Await, te invito a
que veas el siguiente video donde lo explico:
https://www.youtube.com/watch?v=USuhP9F56UE
Servicio - usernameValidate
Página | 496
URL /usernameValidate/:username
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.
497 | Página
12. if (profiles.length > 0) throw new Error("Usuario existente")
13.
14. // Confirm to the username is available
15. res.send({
16. ok: true,
17. message: "Usuario disponible"
18. })
19. } catch (err) {
20. res.send({
21. ok: false,
22. message: err.message || "Error al validar el nombre de usuario"
23. })
24. }
25. }
26.
27. module.exports = {
28. usernameValidate
29. }
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 4) será
ejecutada por express, es por ese motivo que debemos recibir los parámetros
req, res y err.
Finalmente, exportamos la función (línea 28) para que pueda ser referencia
desde el Route de Express.
Página | 498
15. res.status(400).send({message: "Servicio inválido"})
16. })
17.
18. module.exports = router;
499 | Página
Servicio - Signup
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. }
Página | 500
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:
501 | Página
Mediante la función hashSync (línea 8) 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 12) que proporciona el Schema. Si todo sale bien,
el perfil retornado por Mongoose contiene el ID generado. Por otra parte, si hay
algún error, el flujo se va a catch y el error es retornado a la aplicación.
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.
Una de las grandes características de un API es que, deben de ser seguras, por
eso, implementar un sistema de autenticación es indiscutible. La importancia de
implementar seguridad del lado del servidor es impedir que los usuarios no
autenticados puedan acceder a recursos restringidos.
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.
Página | 502
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.
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 apoyar 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.
503 | Página
(línea 18). Si el proceso de autenticación del token falla, entonces, simplemente
igualamos la propiedad user a null.
1. module.exports = {
2. server: {
3. port: 8080
4. },
5. mongodb: {
6. connectionString: "<CONNECTION_STRING>"
7. },
8. jwt: {
9. secret: "#$%EGt3eT##$EG%Y$Y&U&/IRTRH45W$%whth$Y$%YAFG"
10. }
11. }
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:
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.
Página | 504
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.
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. }
505 | Página
que crearemos dentro de una nueva carpeta /api/services/, el archivo se verá
de la siguiente manera:
Página | 506
26. banner: profile.banner || '/public/resources/banners/4.png',
27. tweetCount: profile.tweetCount,
28. following: profile.following,
29. followers: profile.followers
30. },
31. token: token
32. })
33. } catch (error) {
34. res.send({
35. ok: false,
36. message: error.message || "Error al validar el usuario"
37. })
38. }
39. }
507 | Página
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
Página | 508
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, y es que este
servicio, se utiliza para autenticar a los usuarios que ya tiene un token.
URL /secure/relogin
Método GET
Request N/A
Response
40. {
41. "ok": true,
42. "body": {
43. "profile": {
44. "__v": 0,
45. "name": "Juan Perez",
46. "userName": "juan",
47. "password": "",
48. "_id": "5a1f40142aab1e2318d65e98",
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. }
509 | Página
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.
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 8). Seguido, buscamos al usuario por medio del ID (línea 10) y
retornamos 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.
Página | 510
Finalmente, solo nos falta agregar el router en el archivo api.js, el cual quedaría
de la siguiente manera:
511 | Página
Servicio - Consultar los últimos Tweets
Este servicio es el que nos permite recuperar los últimos tweets publicados por
todos los usuarios. Normalmente una red social utiliza IA para determinar los
tweets que deberás ver en tu home, sin embargo, nosotros no tenemos esas
capacidades, por lo que optamos por los últimos tweets.
URL /tweets
Método GET
Headers N/A
Request N/A
Response
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
Página | 512
17. },
18. ...
19. ]
20. }
Como podemos observar, este servicio regresa un array dentro del body, donde
cada posición corresponde a un Tweet.
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:
513 | Página
49. message: "Error al cargar los Tweets",
50. error: error
51. })
52. }
53. }
54.
55. module.exports = {
56. getNewTweets
57. }
Lo siguiente es buscar todos los Tweet donde el campo tweetParent sea null
(línea 14), para asegurar de no recuperar Tweet que correspondan a respuestas.
También hace uso de la instrucción skyp y limit para paginar los resultados
(líneas 17 y 18), de esta forma, recuperamos Tweets en páginas de 10. Limit
determina el número de tweets que debemos de regresar, mientras que skyp
determina los registros que debemos saltarnos antes de llegar a los 10 que vamos
a seleccionar.
Lo siguiente es hacer un populate (join) con los Perfiles (línea 15), 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 16) los tweets de forma descendiente.
En la línea 21 iteramos todos los tweets para construir el array que retornaremos.
Solo cabe detenernos en 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 28). 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 40.
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:
Con estos últimos cambios, ya podremos ver los tweets en nuestro proyecto:
Página | 514
Fig. 152 - probando el servicio de obtención de los últimos tweets.
515 | Página
Servicio - Consultar se usuarios sugeridos
El siguiente servicio tiene como propósito mostrar al usuario una lista de usuarios
sugeridos para comenzar a seguir. Sin embargo, 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 usuario
registros en el proyecto.
URL /secure/suggestedUsers
Método GET
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.
Página | 516
8. .limit(6)
9.
10. res.send({
11. ok: true,
12. body: users.map(x => {
13. return {
14. _id: x._id,
15. name: x.name,
16. description: x.description,
17. userName: x.userName,
18. avatar: x.avatar || '/public/resources/avatars/0.png',
19. banner: x.banner || '/public/resources/banners/4.png',
20. tweetCount: x.tweetCount,
21. following: x.following,
22. followers: x.followers
23. }
24. })
25. })
26. } catch (error) {
27. res.send({
28. ok: false,
29. message: error.message || "Error al validar el usuario"
30. })
31. }
32. }
Este servicio es bastante simple, pues solo realiza la búsqueda de Perfiles (línea
6), los ordena por fecha de creación en forma descendente (línea 7) y luego
recupera los 6 primeros registros (línea 8).
Los resultados son iterados en la línea 12 para crear un array con los perfiles
sugeridos para el usuario.
Tras agregar estos cambios, ya podremos ver los perfiles sugeridos en la página
de inicio:
517 | Página
Fig. 153 - Probando los usuarios sugeridos.
Página | 518
Servicio – Consulta de perfiles de usuario
URL /profile/:username
Método GET
Request N/A
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,
519 | Página
89. "followers": 2,
90. "follow": false
91. }
92. }
93.
Página | 520
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 6).
Una vez que terminamos los cambios, es posible dirigirse a la sección del perfil
de nuestro usuario o el de cualquier otro:
521 | Página
Fig. 154 - Probando la consulta de perfil de usuario.
Página | 522
Servicio – Consulta de Tweets por 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 ú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.
URL /tweets/:username
Método GET
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>"
523 | Página
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. }
Página | 524
48. })
49. }
50. }
Si actualizamos la pantalla de perfil, podremos ver que solo salen tweet del
usuario en cuestión.
525 | Página
Servicio – Actualización del perfil de usuario
URL /tweets/:username
Método PUT
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. }
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. }
Página | 526
Agregaremos la función updateProfile al archivo UserController.js, la cual se
verá de la siguiente manera:
Este es un método bastante simple, pues solo se crea un objeto con los cambios
(línea 6) 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.
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.
527 | Página
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:
URL /followings/:user
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. }
Página | 528
13. _id: x._id,
14. userName: x.userName,
15. name: x.name,
16. description: x.description,
17. avatar: x.avatar || '/public/resources/avatars/0.png',
18. banner: x.banner || '/public/resources/banners/4.png'
19. }
20. })
21.
22. res.send({
23. ok: true,
24. body: response
25. })
26.
27. } catch (error) {
28. res.send({
29. ok: false,
30. message: error.message || "Error al consultara los seguidores",
31. })
32. }
33. }
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 7), 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 11).
Para comprobar los resultados, solo basta con ir a la sección de “Siguiendo” del
perfil del usuario:
529 | Página
Fig. 155 - Probando la sección de "siguiendo".
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:
URL /followers/:user
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",
Página | 530
21. "name":"Emmauel Lopez",
22. "description":"Nuevo en Twitter",
23. "avatar":"<Base 64 Image>",
24. "banner":"<Base 64 Image>"
25. },
26. ...
27. ]
28. }
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 7), 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 10).
531 | Página
8. router.get('/followings/:user',userController.getFollowing)
9. router.get('/followers/:user',userController.getFollower)
Para comprobar los resultados, solo basta con ir a la sección de “Seguidores” del
perfil del usuario:
Servicio – Seguir
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.
Nombre Seguir
URL /secure/follow
URL N/A
params
Método GET
Página | 532
Request
• followingUser: nombre de usuario del perfil
que deseamos seguir/dejar de seguir
1. {
2. "followingUser":"jperez"
3. }
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. }
533 | Página
38. ok: true,
39. unfollow: following,
40. })
41.
42. session.commitTransaction()
43. } catch (err) {
44. console.log("error => ", err.message)
45. session.abortTransaction()
46. res.send({
47. ok: false,
48. message: err.message || "Error al ejecutar la operación",
49. })
50. }
51. }
Para el servicio follow vamos a utilizar una nueva característica de MongoDB que
son las transacciones, ya que seguir a un usuario requiere de dos pasos, por un
lado, tenemos que actualizar nuestro perfil para agregar a la persona que
seguimos, pero por otro lado, tenemos que actualizar el perfil de la otra persona
para agregar nuestro perfil a las personas que lo siguen a él, por tal motivo,
necesitamos crear una transacción y garantizar que los dos cambios se apliquen
al mismo tiempo, y en caso de una falla, las dos operaciones sean descartadas.
En la línea 12 hacemos una búsqueda para recuperar los dos perfiles, el nuestro
y el de la persona que queremos comenzar a seguir, por ello utilizamos el $in.
En la línea 15 validamos si la respuesta retorno los dos perfiles, ya que en caso
de no traerlos, quiere decir que uno de los perfiles no existe, y deberemos lanzar
un error.
Una vez con los dos perfiles, los separamos en variables diferentes para tener
una referencia más rápido a ellos, en la variable my (línea 16) guardamos nuestro
perfil y en la variable other (línea 17), guardamos el perfil de la persona que
queremos comenzar a seguir.
Debido a que este servicio sirve para comenzar a seguir a un usuario o dejarlo
de seguir en caso de que ya lo estuviéramos siguiendo, debemos de determinar
si ya lo seguimos primero (línea 19).
Segundo si lo seguimos o no, será la operación que realizaremos sobre los dos
perfiles. Si ya lo seguimos, quiere decir que la operación hará que dejemos de
seguirlo, por lo que hacemos un pull (eliminar) y un push en nuestra lista de
personas que seguimos (línea 24), en otro caso, debemos hacer lo contrario.
Página | 534
Finalmente, solo quedaría exportar la nueva función y agregar el router
correspondiente en el archivo api.js:
Para probar los cambios, solo tenemos que presionar el botón se “seguir” o
“siguiendo" del perfil de cualquier usuario.
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.
URL /secure/tweets
URL N/A
params
Método POST
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. }
535 | Página
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:
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.
Página | 536
13. tweetParent: req.body.tweetParent,
14. message: req.body.message,
15. image: req.body.image
16. })
17.
18. //Update the user profile to increment the tweet counter
19. let updateProfile = await Profile.updateOne(
20. { _id: user.id }, { $inc: { tweetCount: 1 } }, { session })
21.
22. // If the user profile dont exist, throw error
23. if ((!updateProfile.ok) || updateProfile.nModified == 0)
24. throw new Error("No existe el usuario")
25.
26. // Save the new Tweet
27. newTweet = await newTweet.save({ session })
28. res.send({
29. ok: true,
30. tweet: newTweet
31. })
32.
33. session.commitTransaction()
34. } catch (error) {
35. console.log("error => ", error.message)
36. session.abortTransaction()
37. res.send({
38. ok: false,
39. message: error.message || "Error al guardar el Tweet",
40. error: error.error || error
41. })
42. }
43. }
Podrás ver que nuevamente hemos creado una transacción (líneas 8 y 9). Y lo
que seguiría es crear el tweet, el cual se realiza en tres partes. La primera es
crear el tweet mediante el schema Tweet (línea 11). El segundo paso es
incrementar el contador de tweet del usuario (línea 19), para lo cual utilizamos
el operador $inc, 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 27).
Observa que para incrementar el contador de tweets y guardar el nuevo tweet
estamos enviando la referencia a la session, con lo que garantizamos que los dos
cambios se realicen en la misma transacción.
537 | Página
11. let newTweet = new Tweet({
12. _creator: user.id,
13. tweetParent: req.body.tweetParent,
14. message: req.body.message,
15. image: req.body.image
16. })
17.
18. // Increment replys in the parent tweet
19. let updatedTweet = await Tweet.updateOne(
20. { _id: req.body.tweetParent }, { $inc: { replys: 1 } })
21. .session(session)
22.
23. // Throw error if the parent tweet dont exist
24. if ((!updatedTweet.ok) || updatedTweet.nModified == 0)
25. throw new Error("No existe el Tweet padre")
26.
27. newTweet = await newTweet.save({ session })
28. res.send({
29. ok: true,
30. tweet: newTweet
31. })
32. session.commitTransaction();
33. } catch (error) {
34. session.abortTransaction()
35. console.log("error => ", error.message)
36. res.send({
37. ok: false,
38. message: error.message || "Error al guardar el Tweet",
39. error: err
40. })
41. }
42. }
43.
Ahora solo nos restaría agregar únicamente la función addTweet a los exports y
agregar el router correspondiente en el archivo api.js:
Página | 538
Para probar los cambios, solo basta con crear un nuevo tweet desde la pantalla
principal, aunque la respuesta será difícil probar, pues de momento no podremos
ver el detalle del Tweet hasta implementar el servicio para consultar el detalle de
un 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.
Nombre Like
URL /secure/like
URL N/A
params
Método POST
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
539 | Página
4. }
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. }
Página | 540
35. message: err.message || "Error al actualizar el Tweet",
36. error: error.error || error
37. })
38. }
39. }
40.
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.
Finalmente, solo restaría agregar esta nueva función a los exports y agregar el
router correspondiente al archivo api.js:
Para probar los cambios solo presionemos el botón del corazón en cualquier
Tweet:
541 | Página
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.
Nombre Like
URL /tweetDetails/:tweetID
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,
Página | 542
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,
30. "replys": 0,
31. "image": null
32. },
33. ...
34. ]
35. }
36. }
543 | Página
36. image: x.image,
37.
38. }
39. })
40.
41. res.send({
42. ok: true,
43. body: {
44. _id: tweet._id,
45. _creator: {
46. _id: tweet._creator._id,
47. name: tweet._creator.name,
48. userName: tweet._creator.userName,
49. avatar: tweet._creator.avatar
50. || '/public/resources/avatars/0.png'
51. },
52. date: tweet.date,
53. message: tweet.message,
54. liked: tweet.likeRef.find(
55. likeUser => likeUser.toString() === user.id || null),
56. likeCounter: tweet.likeCounter,
57. image: tweet.image,
58. replys: tweet.replys,
59. replysTweets: replys
60. }
61. })
62.
63. } catch (error) {
64. res.send({
65. ok: false,
66. message: error.message || "Error al cargar el Tweet",
67. e: error
68. })
69. }
70. }
71.
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 5) y luego realizar la
búsqueda del Tweet por medio del ID (línea 11), aprovechamos para realizar un
populate (join) con el perfil del usuario.
En este punto, solo tenemos que exportar la nueva función y agregar el router
correspondiente:
Página | 544
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:
En este punto podrás ver que los servicios funcionan adecuadamente si les
mandamos los parámetros adecuados, sin embargo, no hemos validado las
entradas para garantizar que todos los parámetros necesarios estén presentes
durante la ejecución del servicio.
Para agregar una validación, es necesario agregar un array con todas las reglas
de validación al momento de crear el route, tal y como lo podemos ver en el
siguiente fragmento de código:
545 | Página
2.
3. app.post('/login', [
4. body('username').isEmail(),
5. body('password').isLength({ min: 5 })
6. ], (req, res) => {
7. const errors = validationResult(req);
8. if (!errors.isEmpty()) {
9. return res.status(422).json({ errors: errors.array() });
10. }
11.
12. User.create({
13. username: req.body.username,
14. password: req.body.password
15. }).then(user => res.json(user));
16. });
Lo que hacemos aquí es usar la función body para recuperar los valores del
request, en este ejemplo recuperamos el username y password (líneas 4 y 5) para
después aplicarles ciertas validaciones que viene definidas en express-validator.
En este ejemplo, validamos que el username sea un correo electrónico y que el
password tenga una longitud mínima de 5 caracteres.
Página | 546
26. validationRules,
27. validate,
28. }
Por otro lado, es necesario agregar estas validaciones cuando creamos el route:
547 | Página
Puedes ver la lista completa de validaciones disponibles en la página web de
express-validator: https://express-validator.github.io/docs/validation-chain-
api.html
Página | 548
48. }
Por otra parte, el método userValidate es el que recupera los errores y corta la
ejecución si detecta un error.
El último paso sería agregar las validaciones al Route, por lo que vamos a ir al
archivo api.js y agregar lo siguiente:
Para comprobar que esto se está cumpliendo, vamos a ejecutar una prueba desde
la herramienta Postman, veamos el resultado:
549 | Página
Si observas el request (parte superior), podrás ver que no hemos enviado
ninguno de los parámetros requeridos, por lo que en la respuesta (parte inferior),
podrás ver que nos lanza todos los errores.
Página | 550
50. }
Como podrás ver, este archivo es exactamente igual al anterior, con la única
diferencia de que los campos que validamos son otro.
551 | Página
38. }
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.
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 específica del tema.
Página | 552
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.
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.
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.
553 | Página
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
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
1. .myclass.myclass2#myID
Página | 554
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:
El resultado:
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')
1. link(
2. rel='stylesheet'
3. href='/styles.css'
4. )
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.
Unbuffered Code
555 | Página
guion alto (-). Son utilizados para agregar estructuras de control o definir
variables. Ejemplo:
Buffered code
Unescaped Buffered
Iteraciones
Pug soporta dos tipos de iteraciones, each y while, las cuales funcionan
exactamente igual que en cualquier lenguaje de programación.
Each
Página | 556
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
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.
557 | Página
Si regresamos al archivo api.js, podremos ver que hemos definido un router
para escuchar en la raíz del subdominio:
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
Página | 558
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
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')
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.
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.
559 | Página
Fig. 161 - API Catalog.
Como podemos observar, el catálogo es simplemente una lista con los servicios
disponibles con la información básica:
Página | 560
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:
561 | Página
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:
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.
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",
Página | 562
5. secure: true,
6. url: "/secure/tweets",
7. method:"POST",
8. urlParams: [],
9. requestFormat:"{}",
10. dataParams: "{}",
11. successResponse: "{}",
12. errorResponse:"{}"
13. }
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.
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
563 | Página
\"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
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. }
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:
Página | 564
Fig. 164 - Documentación de un servicio.
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')
565 | Página
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 🔒
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 🔒 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.
Página | 566
con un atributo correspondiente al tipo de documento (class="json"). Cuando
la librería se active (línea 10), reconocerá las clases de estilo y dará formato
automáticamente.
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
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:
567 | Página
Fig. 165 - Foto de perfil en base64
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.
Página | 568
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.
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.
569 | Página
Resumen
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.
Página | 570
Producción
Capítulo 18
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
571 | Página
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)
y Stress (pruebas de carga) y quizás algunos ambientes más. Sin embargo,
nosotros abordaremos solo desarrollo y producción.
Alta disponibilidad
Cluster
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.
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.
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.
Página | 572
Lo primero que tenemos que hacer es instalar la librería cluster, mediante el
siguiente comando:
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:
573 | Página
52.
53. app.listen(configuration.server.port, function () {
54. console.log('Example app listening on port 8080!')
55. });
56. }
57.
58. if (require.main === module) {
59. startServer();
60. } else {
61. module.exports = startServer;
62. }
De las líneas 13 a 56 hemos agregado todo el inicio del server dentro de la función
startServer, después en las líneas 58 a 62 definimos la forma en que se debe de
ejecutar el server. Cuando un archivo es ejecutado directamente mediante el
comando node, se establece el valor 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
archivo, entonces solamente exportamos la función startServer para ser
utilizada por fuera.
Página | 574
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 del
archivo 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
(línea 7) por cada CPU que tenga el equipo, es decir, si nuestra máquina es de 8
núcleos, se levantarán 8 instancias del servidor, si nuestro procesador es de 6
núcleos, entonces 6 instancias se levantarán y así sucesivamente.
Por otra parte, registramos el evento “exit” (línea 15), 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.
Una vez que hemos creado el cluster, solo restaría ejecutarlo para comprobar su
funcionamiento, por lo tanto, comenzaremos con apagar el server y volvemos a
iniciar el server mediante node cluster.js:
575 | Página
Fig. 166 - 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:
Una vez en los procesos, procedemos con terminar alguno, tiendo cuidado de no
matar el proceso del cluster, el cual podemos distinguir porque es el que menos
memoria consume:
Página | 576
Fig. 168 - Cluster recovery.
Puertos
Internet trabaja exclusivamente con los puertos 80 para HTTP y 443 para HTTPS,
por lo que configurar nuestra aplicación para trabajar en estos puertos 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.
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 serverConfig.js y modificar el
puerto:
1. module.exports = {
577 | Página
2. server: {
3. port: 80
4. },
5. mongodb: {
6. connectionString: "<CONNECTION_STRING>"
7. },
8. jwt: {
9. secret: "<JTW_SECRET>"
10. }
11. }
Comunicación segura
Página | 578
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.
Estos certificados pueden ser validados por los navegadores, lo que habilita el
candado que podemos ver en la barra de navegación:
579 | Página
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 Letsencript, pues pueden ser
emitidos de forma gratuita, con la única condición de que debes de tener un
dominio comprados.
Por otro lado, tenemos los certificados auto firmados, los cuales pueden ser
emitidos por quien sea, incluso, podemos crear nuestros propios certificados
sin ningún costo.
Página | 580
Esta pantalla provocará que los usuarios salgan 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:
En este punto, te estarás preguntando, entonces para que pueden servir este
tipo de certificados, y 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.
581 | Página
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.
http://gnuwin32.sourceforge.net/packages/openssl.htm
https://www.openssl.org
Página | 582
Fig. 173 - Creando el certificado con OpenSSL.
Una vez creados los certificados, los podremos ver desde nuestro editor:
583 | Página
25. console.log(`Example app listening on port
26. ${configuration.server.securePort}!`)
27. });
28.
29. http.createServer(app).listen(configuration.server.port);
30. }
31.
32. if (require.main === module) {
33. startServer();
34. } else {
35. module.exports = startServer;
36. }
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 que sigue es levantar el servidor utilizando los certificados, por ello creamos
el servidor mediante el método createServer de la línea 20, el cual permite
agregar los parámetros key, cert, passphrase, los cuales corresponden a los
archivos del certificado. El key corresponde a la llave privada y cert a la llave
pública.
1. module.exports = {
2. server: {
3. port: 80,
4. securePort: 443
5. }
6. }
Página | 584
Fig. 175 - Probando el canal seguro HTTPS.
Error común
1. "scripts": {
2. "start": "npm run build && npm run start_windows",
3. "dev": "nodemon server.js",
585 | Página
4. "build": "webpack -p",
5. "start_linux": "NODE_ENV=production&&node cluster.js",
6. "start_windows": "set NODE_ENV=production&&node cluster.js"
7. }
Al agregar estos nuevos scripts, nos permite ejecutar la aplicación de una forma
más simple, ya que podemos ejecutar la aplicación en modo desarrollo mediante
el comando npm run dev o en producción mediante npm start.
Cuando ejecutamos npm run dev, lo que hacemos es que en realidad ejecutamos
el comando node server.js. y cuando ejecutamos npm start hacemos dos cosas,
lo primero es que se ejecuta el comando build, el cual transpila la aplicación para
producción mediante el comando webpack -p y luego, ejecutamos el comando
marcado en amarillo en la línea 2. Este comando podría variar según el sistema
operativo del servidor. En este caso, estamos ejecutando en Windows, por lo
tanto ejecutamos el script start_windows, el cual corresponde con el script de la
línea 6, pero si estamos en Linux, debemos de cambiar ese comando por
start_linux. La diferencia entre start_windows y start_linux es la forma en que
establecemos la variable de entorno NODE_ENV.
Sin embargo, esto no evita que webpack-dev-server inicie, por lo que tendremos
que hacer un pequeño cambie en el archivo server.js, el cual podemos ver a
continuación:
Página | 586
1. function startServer() {
2.
3. . . .
4.
5. if (process.env.NODE_ENV !== 'production') {
6. app.use(require('webpack-dev-middleware')(compiler, {
7. noInfo: true,
8. publicPath: config.output.publicPath
9. }))
10. }
11.
12. . . .
13. }
1. module.exports = {
2. mode: "production",
3. ...
4. }
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.
587 | Página
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
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.
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));
Página | 588
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.
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.
npm install
npm start
589 | Página
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.
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.
Página | 590
CONCLUSIONES FINALES
¡Felicidades! Si estas leyendo estas líneas, es porque seguramente has concluido
de leer este libro, lo que te convierte en un programador Full Stack capaz de
desarrollar una aplicación completa utilizando React, NodeJS, Express y MongoDB
(Stack MERN).
Como ya te habrás 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.
GRACIAS.
591 | Página