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)

Mauricio Garcia
16 min readNov 24, 2020

--

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.
  • HeaderComponente 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 de Form, Card y SearchList, así como el control del estado principal.
  • FormComponente que va tener un input, donde vamos a poder escribir, y al momento de dar enter, va a mandar lo capturado.
  • CardComponente que va a mostrar un título y una imagen.
  • SearchListComponente 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 agregamos ThemeProvider a la App
  • En la constante theme agregamos dos paletas de colores light y dark :

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 de setPokemonList
  • (B) — Hacemos la petición con fetch usando async/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 constantes loading y response .
  • (C) — Cuando loading = false y response = 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 componente Loading, 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 :

Para ver otros iconos[ref]

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ón createUseStyles, donde, por medio del argumento le pasamos el theme, y agregamos los colores theme.* .
  • (C) —En la constante classes, invocamos useStyle .
  • (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:

  • Código completo de App[ref]
  • Código completo de Components/Header[ref]

:: 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.
  • reducerListFunción que ejecuta dispatchList, 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).
  • searchArreglo, donde vamos a ir agregando los nombres que buscó.
  • itemCurrent — Los datos del ítem buscado o seleccionado.
  • indexCurrent — El index del item seleccionado de search.

— 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 variable data :

Sí hemos hecho todo bien, debemos de ver lo siguiente:

  • Código completo de App[ref]
  • Código completo de Components/Container[ref]

:: 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 constante inputNameRef

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ón createUseStyles, donde, por medio del argumento le pasamos el theme, y agregamos los colores theme.* .
  • (C)— En la constante classes, invocamos useStyle .
  • (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 de items el pokémon.
  • (C) — Agregamos el componente Form, y le pasamos por el atributo handleEvent la función dispatchList

Sí hemos hecho todo bien, debemos de ver lo siguiente:

  • Código completo de Components/Container[ref]
  • Código completo de Components/Form[ref]

:: 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/ :

error.gif
loading.gif

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ón createUseStyles, donde, por medio del argumento le pasamos el theme, y agregamos los colores theme.* .
  • (C) — En la constante classes, invocamos useStyle .
  • (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 buscado list.itemCurrent

Sí hemos hecho todo bien, debemos de ver lo siguiente:

  • Código completo de Components/Container[ref]
  • Código completo de Components/Card[ref]

:: 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 retornar null en el componente.
  • handleEvent — Va a ser la función encargada de actualizar el estado del padre.
  • indexCurrentPropiedad 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 clase active .
  • Es importante observar que tenemos 3 acciones: PREV, NEXT y GOTO, por lo que debemos de agregarlas en la función de reducerList del componente Container (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ón createUseStyles, donde, por medio del argumento le pasamos el theme, y agregamos los colores theme.* .
  • (C)— En la constante classes, invocamos useStyle .
  • (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 de items el pokémon.
  • (C) — Agregamos el componente SearchList, y le pasamos por el atributo list la lista de las palabras buscadas, indexCurrent el item seleccionado y handleEvent la función dispatchList

Sí hemos hecho todo bien, debemos de ver lo siguiente:

  • Código completo de Components/Container[ref]
  • Código completo de Components/SearchList[ref]

:: 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 de search la palabra seleccionada y buscarla dentro de items
  • 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 de search la palabra seleccionada y buscarla dentro de items
  • 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 de search la palabra seleccionada y buscarla dentro de items
  • 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 :

  • Código completo de Helpers/helper[ref]
  • Código completo de Components/Container[ref]

:: 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:

https://mauriciogc.github.io/react-search-pokemon/

--

--

No responses yet