React — Creando la app Search Pokémon con create-react-app, estilos con react-jss y Hooks (useState, useEffect, useRef, useReducer, useCallback y custom Hooks)
Temario
- Introducción
- Creando una app con create-react-app
- Definiendo los componentes
- Creando los componentes funcionales (App, Loading, Header, Container, Form, Card, SearchList)
- Optimizando componentes
- Validando las propiedades
- Construyendo la aplicación para subir a producción
- Subiendo el proyecto a un servidor (github.io)
i. Introducción
En esta entrega vamos a crear una búsqueda de pokémon con React, donde haremos la aplicación con create-react-app
, componentes funcionales, propTypes para validar las propiedades, los estilos con react-jss
y utilizaremos los Hooks useState, useEffect, useReducer, useRef, useCallback, y custom Hooks
, prácticamente… todo lo que hemos aprendido 🤓…
Si llegaste aquí directamente y quieres aprender React desde cero, te recomiendo que inicies por acá (React — Primeros pasos…[ref])
La idea es tener algo así:
Ver proyecto en línea[ref]
ii. Creando una app con create-react-app
Lo primero que vamos a hacer es ir a la carpeta donde tenemos los proyectos, y desde ahí ejecutamos el siguiente comando:
Si tienes dudas por que npx o llegaste aquí directo, te recomiendo que inicies primero por acá (Mi primera App con create-react-app [ref])
Vamos a la carpeta del proyecto, e instalamos:
- Para los estilos:
react-jss
Puedes utilizar el que más te guste, revisa esta story para conocer más (React — Formas de diseñar componentes de React, desde estilos en línea hasta CSS in JS[ref])
- Instalamos
react-image
[ref], para la carga de la imagen del pokémon seleccionado, este paquete nos da la oportunidad de poner una imagen de “cargando”, y una imagen de “error” (en caso de que no cargue la imagen).
Una vez que se ha descargado todo, vamos a levantar el servidor, y ver la aplicación :
Se debe abrir una ventana en el navegador, mostrando la aplicación.
iii. Definiendo los componentes
El siguiente paso, es definir qué componentes son los que vamos a construir, entonces:
App
— Es el componente principal, y va a ser el encargado de cargar el tema y de invocar la API para obtener la lista de Pokémon. Va a contener los componentes:Loading, Header y Container
.Header
— Componente que va a tener un botón que nos va a permitir cambiar el tema de la aplicación.Container
— Es el componente que se va a encargar de la comunicación deForm, Card y SearchList
, así como el control del estado principal.Form
— Componente que va tener uninput
, donde vamos a poder escribir, y al momento de dar enter, va a mandar lo capturado.Card
— Componente que va a mostrar un título y una imagen.SearchList
—Componente encargado de mostrar la lista de búsqueda que se han hecho, con la oportunidad de poder navegar sobre ella (ya sea dando click directo, o con los botones de siguiente y atrás).Loading
— (No aparece en la imagen de arriba), es un cargador animado[ref]
iv. Creando los componentes funcionales
Antes de generar los componentes funcionales, primero, vamos a limpiar el proyecto:
El componente App.js
debe quedar así:
Los estilos App.css
Eliminar : logo.svg
Crear: src/assets, src/Components, src/Helpers, src/Hooks
Nuestra carpeta debe verse así:
— App.js : Tema
Lo primero que vamos a hacer es crear el tema de la aplicación:
- Importamos
react-jss
, y agregamosThemeProvider
a laApp
- En la constante
theme
agregamos dos paletas de coloreslight
ydark
:
Creamos el estado typeTheme
(No olvides importar useState
), donde le vamos a indicar el tipo de tema que vamos a utilizar (por default light
):
Y por último, agregamos una función, que nos permita cambiar el tema y la añadimos al atributo theme
de ThemeProvider
:
Código completo de App
:
— App.js : API Pokémon
Ocuparemos la API pokeapi[ref], para obtener la lista de los pokémon. Vamos a crear la petición, donde ocuparemos useEffect
(no olviden importarla) y fetch
, así como el estado pokemonList
que va a guardar la lista.
Donde:
- (A) —Para que se ejecute
useEffect
una sola vez, hacemos que dependa desetPokemonList
- (B) — Hacemos la petición con
fetch
usandoasync/await
- (C) — Guardamos la petición
Si hemos hecho todo correctamente el response
de la API, debe mostrarse en consola:
Aprovechemos nuestros conocimientos, el fetch
vamos a convertirlo en un Hook personalizado (con el objetivo de usarlo en otros proyectos)
— App.js : Convertir el fetch en un Hook
Creamos el Hook Hooks/useFetch
, donde vamos pasar el código que anteriormente hicimos (eliminarlo de App
) :
Vamos a volverlo un poco más genérico. La idea es poder pasarle la url
y que además tenga un estado loading
para saber si ya termino de hacer la petición. Entonces, cuando se hace la petición, loading=true
, cuando tiene los datos loading=false
.
Vamos a regresar response
(son los datos de la petición que hicimos), loading
(saber el status de la petición).
Ya tenemos el Hook personalizado, lo siguiente, es ocuparlo en la App
:
Donde:
- (A) — Importamos el Hook personalizado
useFetch
- (B) — Simplemente lo invocamos, con la
url
, el Hook, nos va a devolver dos constantesloading
yresponse
. - (C) — Cuando
loading = false
yresponse = datos
, vamos a imprimir en consola el resultado.
Si hemos hecho todo correctamente el response
de la API, debe mostrarse en consola:
— App.js : Formato a los datos
Necesitamos darle el formato necesario a la lista, la idea es tener:
id
— Identificador único por cada pokémon.name
— Nombre del pokémon.img
— URL de imagen para mostrar.fullImg
— URL de imagen de mayor resolución.
Entonces:
Lo que hicimos fue crear un nuevo arreglo llamado data
, con el formato que necesitamos.
— Loading.js : Creando el componente
Vamos a crear el componente Components/Loading
, para este caso vamos aprovechar el componente de la story pasada (React — Hooks [useReducer, useCallback, useMemo, useRef, and customs Hooks](Parte II)[***]):
— App.js : Agregando el componente Loading
Donde:
- (A) — Importamos
Fragment
, para no agregar elementos extras. - (B) — Importamos el componente
Loading
. - (C) — Hacemos la validación, si
loading=true
vamos a mostrar el componenteLoading
, de lo contrario, mostramos “Se han cargado los datos!”
- Código completo de
App
[ref] - Código completo de
Hooks/useFetch
[ref] - Código completo de
Components/Loading
[ref]
:: Descargar proyecto (Parte I.I)
Puedes descargar el proyecto de GitHub[ref] con el tag 0.1.0
Continuemos….
— Header.jsx : Creando el componente
Vamos a crear el componente Components/Header
:
Descargar la siguiente imagen, y agregarla en assets/roll.png
:
Importamos la imagen, y la agregamos al componente:
Agregamos estilos al componente:
Donde:
- (A) — Importamos el style componente
react-jss
. - (B) — En la constante
useStyle
invocamos la funcióncreateUseStyles
, donde, por medio del argumento le pasamos eltheme
, y agregamos los colorestheme.*
. - (C) —En la constante
classes
, invocamosuseStyle
. - (D) — Cambiamos
className="any" => className={classes.any}
.
Vamos agregar la función toggleTheme
, para que se ejecute cada vez que se le da click a la imagen:
— App.js : Agregando el componente Header
Donde:
- (A) — Importamos el componente
Header
- (B) — Y lo agregamos cuando se haya obtenido la lista de pokémon.
Sí hemos hecho todo bien, debemos de ver lo siguiente:
:: Descargar proyecto (Parte I.II)
Puedes descargar el proyecto de GitHub[ref] con el tag 0.1.1
— Container.jsx : Creando el componente
Vamos a crear el componente Components/Container
Agregamos los estilos, para darle el diseño adecuado:
El siguiente paso es crear un estado complejo, por lo que usaremos el Hook useReducer
:
Donde:
list
— Es el objeto del estado actual.dispatchList
— La función que tenemos que ejecutar para actualizar el estado.reducerList
— Función que ejecutadispatchList
, recibe dos argumentos:state, action
.{...}
— Es el objeto con los valores iniciales.
Donde {...}
:
items
— La lista de pokémon que nos mandan desde el padre (App
).search
— Arreglo, donde vamos a ir agregando los nombres que buscó.itemCurrent
— Los datos del ítem buscado o seleccionado.indexCurrent
— El index del item seleccionado desearch
.
— App.js : Agregando el componente Container
Donde:
- (A) — Importamos el componente
Container
- (B) — Y lo agregamos cuando se haya obtenido la lista de pokémon, y por medio del atributo
items
le pasamos la variabledata
:
Sí hemos hecho todo bien, debemos de ver lo siguiente:
:: Descargar proyecto (Parte I.III)
Puedes descargar el proyecto de GitHub[ref] con el tag 0.1.2
— Form.jsx : Creando el componente
Vamos a crear el componente Components/Form
:
Donde:
- (A) — Vamos a utilizar
useRef
para obtener el valor del input, lo he decidido así, ya que no es necesario que se este renderizando el componente cada vez que se escribe, además el valor se va a mandar hasta que se le de enter. - (B) — Función que vamos a ocupar cuando se le dé enter, donde obtendremos el valor, con la finalidad de mandarlo al padre.
- (C) — En el atributo
onSubmit
agregamos la función de (B)handleSubmit
- (D) — Pasamos la referencia al
input
por medio de la constanteinputNameRef
En el componente vamos a recibir la propiedad handleEvent
, que va a ser la función encargada de actualizar el estado del padre; donde, se ejecutará dentro de la función handleSubmit
, con el objeto { type:"SEARCH", name }
Agregamos estilos al componente:
Donde:
- (A) — Importamos el style componente
react-jss
. - (B) — En la constante
useStyle
invocamos la funcióncreateUseStyles
, donde, por medio del argumento le pasamos eltheme
, y agregamos los colorestheme.*
. - (C)— En la constante
classes
, invocamosuseStyle
. - (D) — Cambiamos
className="any" => className={classes.any}
.
— Container.jsx : Agregando el componente Form
Donde:
- (A) — Importamos el componente
Form
- (B) — En la función
reducerList
, agregamos el tipo"SEARCH"
, con la finalidad de buscar dentro deitems
el pokémon. - (C) — Agregamos el componente
Form
, y le pasamos por el atributohandleEvent
la funcióndispatchList
Sí hemos hecho todo bien, debemos de ver lo siguiente:
:: Descargar proyecto (Parte I.IV)
Puedes descargar el proyecto de GitHub[ref] con el tag 0.1.3
— Container.jsx : Buscando el pokémon
Actualmente la función reducerList
está así:
Tenemos que hacer lo siguiente:
- Guardar dentro de
search
la nueva palabra buscada. - Buscar dentro de
items
si se encuentra el pokémon buscado. - En caso de que lo encontremos debemos actualizar
itemCurrent
con el objeto encontrado. - En caso de que no sea encontrado, regresar en
itemCurrent = {name:"no matches"}
Si encuentra el pokémon, nos regresa el estado list
de la siguiente manera:
Y cuando no encuentra el pokémon, nos regresa el estado list
de la siguiente manera:
- Código completo de
Components/Container
[ref]
:: Descargar proyecto (Parte I.V)
Puedes descargar el proyecto de GitHub[ref] con el tag 0.1.4
— Card.jsx : Creando el componente
Vamos a crear el componente Components/Card
:
En el componente vamos a recibir la propiedad item
, que va a contener name
— nombre del pokémon, img
— URL de la imagen a mostrar
En caso de que no tenga item
, no vamos a mostrar el contenido del componente, por lo que retornamos null
:
— Card.jsx : Utilizando react-image
Importamos el paquete react-image
[ref]
Vamos a utilizar el componente Img
para cargar la imagen, este componente nos permite agregar una imagen de cargador y otra en caso de que falle la descarga de la imagen.
::Sintaxis
Descargar las siguientes imágenes, y agregarlas en assets/
:
Importamos las imágenes, y las agregamos al componente:
Agregamos estilos al componente:
Donde:
- (A) — Importamos el style componente
react-jss
. - (B) — En la constante
useStyle
invocamos la funcióncreateUseStyles
, donde, por medio del argumento le pasamos eltheme
, y agregamos los colorestheme.*
. - (C) — En la constante
classes
, invocamosuseStyle
. - (D) — Cambiamos
className="any" => className={classes.any}
.
— Container.jsx : Agregando el componente Card
Donde:
- (A) — Importamos el componente
Card
- (B) — Agregamos el componente, y por medio del atributo
item
le pasamos los datos de pokémon buscadolist.itemCurrent
Sí hemos hecho todo bien, debemos de ver lo siguiente:
:: Descargar proyecto (Parte I.VI)
Puedes descargar el proyecto de GitHub[ref] con el tag 0.1.5
— SearchList.jsx : Creando el componente
La idea es tener un título que diga “List of already search pokémon”, dos botones: prev y next, además de la lista de las palabras que ha buscado.
Vamos a crear el componente Components/SearchList
:
Vamos a recibir 3 propiedades:
list
— La lista de palabras buscadas, en caso de que no tenga vamos a retornarnull
en el componente.handleEvent
— Va a ser la función encargada de actualizar el estado del padre.indexCurrent
— Propiedad para saber cuál de la lista está seleccionado y para bloquear/desbloquear los botones.
- Para el botón
PREV
estamos validando que si el seleccionado es<= 0
debe deshabilitar el botón, de lo contrario habilitarlo. - Para el botón
NEXT
estamos validando que si el seleccionado es>= tamaño de la lista
debe deshabilitar el botón, de lo contrario habilitarlo. - En la lista, en caso de que el
index
de la lista sea igual al seleccionado, agregamos la claseactive
. - Es importante observar que tenemos 3 acciones:
PREV, NEXT y GOTO
, por lo que debemos de agregarlas en la función dereducerList
del componenteContainer
(este paso lo haremos más adelante)
Agregamos estilos al componente:
Donde:
- (A) — Importamos el style componente
react-jss
. - (B) — En la constante
useStyle
invocamos la funcióncreateUseStyles
, donde, por medio del argumento le pasamos eltheme
, y agregamos los colorestheme.*
. - (C)— En la constante
classes
, invocamosuseStyle
. - (D) — Cambiamos
className="any" => className={classes.any}
.
— Container.jsx : Agregando el componente SearchList y las acciones
Donde:
- (A) — Importamos el componente
SearchList
- (B) — En la función
reducerList
, agregamos el tipo"PREV","NEXT","GOTO"
, con la finalidad de buscar dentro deitems
el pokémon. - (C) — Agregamos el componente
SearchList
, y le pasamos por el atributolist
la lista de las palabras buscadas,indexCurrent
el item seleccionado yhandleEvent
la funcióndispatchList
Sí hemos hecho todo bien, debemos de ver lo siguiente:
:: Descargar proyecto (Parte I.VII)
Puedes descargar el proyecto de GitHub[ref] con el tag 0.1.6
— Container.jsx : Acciones — Anterior, Siguiente e Ir
Actualmente la función reducerList
está así:
En PREV
, tenemos que hacer lo siguiente:
- Si
indexCurrent > 0
, debemos obtener desearch
la palabra seleccionada y buscarla dentro deitems
- En caso de que lo encontremos debemos actualizar
itemCurrent
con el objeto encontrado.
En NEXT
, tenemos que hacer lo siguiente:
- Si
indexCurrent < al tamaño de la lista
, debemos obtener desearch
la palabra seleccionada y buscarla dentro deitems
- En caso de que lo encontremos debemos actualizar
itemCurrent
con el objeto encontrado.
En GOTO
, tenemos que hacer lo siguiente:
- Vamos a recibir
action.index
, que es el seleccionado, entonces, debemos obtener desearch
la palabra seleccionada y buscarla dentro deitems
- En caso de que lo encontremos debemos actualizar
itemCurrent
con el objeto encontrado.
Cuando probamos la aplicación, vemos lo siguiente:
- Código completo de
Components/Container
[ref]
:: Descargar proyecto (Parte I.VIII)
Puedes descargar el proyecto de GitHub[ref] con el tag 0.2.0
v. Optimizando componentes
Hasta aquí, ya tenemos la aplicación completa, pero… creo que todavía podemos hacerle algunas mejoras.
— Reutilizando código
En el componente Container
, en la función reducerList
, tenemos mucho código repetido:
La idea es crear una función genérica donde pongamos toda esa lógica.
Vamos a crear un helper Helpers/helper.js
, donde agregamos la función findWord
(aquí es donde vamos a poner la lógica):
Podemos observar que he agregado una validación extra, en caso de que no se encuentre la palabra exacta, buscará la primera coincidencia.
El siguiente paso es importar helper
en el componente Container
, y hacer los cambios en la función reducerList
:
:: Descargar proyecto (Parte II.I)
Puedes descargar el proyecto de GitHub[ref] con el tag 0.2.1
— Form.jsx: Reduciendo instancias de las funciones
Vamos abrir el componente Form
, para validar cuantas instancias de handleSubmit
se crean cada vez que se renderiza el componente:
Podemos observar que con cualquier interacción que se hace con la aplicación, genera una nueva instancia, entonces, vamos a utilizar useCallback
para persistir en caché la función:
Ahora sí, sin importar la acción que estemos realizando dentro de la aplicación, va a conservar la instancia de la función.
— SearchList.jsx: Reduciendo instancias de las funciones
Hagamos la misma prueba ahora con el componente SearchList
, para validar cuantas instancias de handlerPrev, handlerNext
se crean cada vez que se renderiza el componente:
Podemos observar que con cualquier interacción que se hace con la aplicación, genera una nueva instancia para cada función (en este caso 2), entonces, vamos a utilizar useCallback
para persistir en caché las funciones:
Cuando ejecutamos la aplicación, vemos lo siguiente:
¿Puedes detectar la falla?…recordemos las reglas para poder usar un Hook:
- Los Hooks no funcionan con componentes de clase.
- Nunca llamar un Hook dentro de un loop, condición, función anidada o funciones fuera del componente, con esta regla nos aseguramos de que los Hooks siempre se llamen en el mismo orden cada vez que un componente se renderiza.
- Los Hooks, deben estar ubicados dentro del componente en el nivel superior.
- Solo usar cuando sea un componente funcional.
- Un Hook puede mandar a llamar a otro Hook.
El primer problema, es que no están dentro del componente a nivel superior, y el segundo problema es que se está usando después de un if
. Hagamos los cambios (solo es mover una línea de código):
Ahora sí, sin importar la acción que estemos realizando dentro de la aplicación, va a conservar las instancias de las funciones.
:: Descargar proyecto (Parte II.II)
Puedes descargar el proyecto de GitHub[ref] con el tag 1.0.0
:: Ejemplo completo
vi. Validando las propiedades
Ahora por último, pero no el menos importante… vamos a validar las propiedades de cada uno de los componentes (si aplica) con prop-types
.
En todos los componentes que aceptamos propiedades de entrada, vamos a importar:
El componente Card
:
El componente Container
:
El componente Form
:
El componente SearchList
:
:: Descargar proyecto (Parte II.III)
Puedes descargar el proyecto de GitHub[ref] con el tag 1.0.1
vii. Construyendo la aplicación para subir a producción
Una vez que tenemos el proyecto, el siguiente paso es construir el proyecto para producción, entonces, en consola debemos ejecutar el siguiente comando (no olvides estar en la carpeta del proyecto)
Si lo hemos hecho bien, la consola debe verse algo así:
Con este comando, lo que hace es crear una carpeta dentro de la aplicación, llamada build
, que es el proyecto listo para mandar a producción.
viii. Subiendo el proyecto a un servidor (github.io)
Debemos de tener una cuenta en Github para poder subir páginas, si no tienes una cuenta [aquí crea una].
Vamos a crear un nuevo repositorio, dirígete a GitHub y crea un nuevo repositorio[ref] llamado username.github.io, donde username es tu nombre de usuario en GitHub.
Si la primera parte del repositorio no coincide exactamente con tú nombre de usuario, no funcionará, así que asegurate de hacerlo bien.
El siguiente paso es clonar el repositorio:
En mi caso:
Vamos al proyecto react-search-pokemon
(donde generamos el build
) y copiamos el contenido que está dentro de la carpeta build
, y la pegamos dentro del repositorio clonado.
Agregamos, confirmamos y enviamos los cambios:
Si haz hecho bien todos los pasos puedes acceder a https://username.github.io/ y ver la aplicación (recuerda cambiar username
por tu usuario).
Y eso es todo!, te comparto la liga que he generado:
En la siguiente entrega vamos a ver React — Haciendo peticiones con Axios y Hooks
La entrega pasada vimos React — Hooks [useReducer, useCallback, useMemo, useRef, and customs Hooks](Parte II)
Bibliografía y links que te puede interesar…