React Workshop

Workshop - Paso 1

Buenísimo, ya tenemos una idea de cómo hacer los componentes de nuestra app del clima. Vamos al archivo templates/template.css donde va a estar el css de la app ya preparado, lo abrimos y copiamos todo el contenido y lo pegamos en nuestro src/App.css ya existente. Hagamos lo mismo con el archivo templates/template.js y src/App.js donde ya van a estar preparadas las clases (className en React, recuerdan? 😜) con los estilos recién copiados. Una vez que reemplazamos el código en nuestros archivos App.js y App.css vamos a generar una API key para poder utilizar el servicio de la API del clima de Weatherbit.io, la página para registrarse es esta. Elegí Weatherbit porque Apixu se puso bastante restrictiva y Weatherbit fue una de las más simples de usar que encontré. Warning: Van a tener que loguearse a cada rato, tienen medio roto el login parece, pero es lo mejor que encontré a cuanto info que brinda la API.

Bueno, repasamos antes de comenzar, deberíamos tener:

Una vez que hagamos eso, si todavía tenemos corriendo el servidor, deberíamos ver cómo se debería ver nuestra app terminada (falta componentizar todo, pequeño detalle). Si no lo vemos, es posible que haya que refrescar la página, sino es posible que no estemos corriendo el servidor, en cuyo caso debemos ir a la línea de comando y volver a ejecutar npm start.

Empecemos - Consiguiendo nuestra data

Vamos a encarar esto primero consiguiendo los datos y teniéndolos a mano en el estado de nuestro componente para luego vamos a separar en componentes, pasando todos los datos (mediante props) que necesitemos pasar. Hacer API calls, llamadas a las APIs, en React es quizás algo no tan simple como uno cree que debería ser, pero hay un post que lo explica bastante bien. Acá vamos a explicar lo necesario, pero si quieren ampliar pueden leer el post acá (en inglés).

Antes de hacer el fetch para traer la data, tenemos que saber dónde pedirla 😝 Según la docu de Weatherbit, si le 'pegamos' a esta dirección, vamos a tener el pronóstico para el día de hoy + 6 días (sí, el día de hoy está incluido cuando hacemos days=7, por suerte 😀)

https://api.weatherbit.io/v2.0/forecast/daily?city=Buenos+Aires,Argentina&key=<YOUR_API_KEY>&days=7

Donde dice <YOUR_API_KEY> ponemos nuestra key generada cuando nos registramos, y si vamos a esa dirección en el navegador (con nuestra key pegada) vamos a ver un JSON con toda la data que necesitamos (si se veo medio feo podemos copiar y pegar el texto del JSON en JSON Formatter y va a quedar más claro cómo están compuestos los objetos, o buscar alguna extensión del navegador para formatear JSON, y en la docu de Weatherbit (en inglés) explica qué es cada parte del objeto, aunque vamos a ir viendo qué necesitamos para cada parte de nuestra app) Como podemos ver, estamos buscando el clima en la ciudad de Buenos Aires, Argentina, pero cambien el texto por su ciudad o por la que quieran buscar en el mundo! 😁

Y ahora la pregunta del millón: dónde hacemos la llamada a la API en nuestro componente?

Existe lo que se llama "Métodos de los ciclos de vida" (Lifecycle methods) de nuestros componentes, son parte de React y son para que podamos hacer distintas cosas en distintos momentos de la vida de nuestro componente. En este diagrama podemos verlos bien y cuándo se ejecuta cada uno:

Ciclos de vida de un método

Diagrama de Ciclos de vida de componentes

El método donde vamos a hacer el fetch hacia la API de Weatherbit es componentDidMount(), con la idea de que primero nuestro componente se monte (y renderice) correctamente y en ese momento buscar los datos para que se actualice el estado de nuestro componente causando que React lo re-renderice (no es algo que afecte a la performance, como dijimos, renderiza lo justo y necesario nomás). Antes de hacer el fetch, tenemos que saber algo sobre usar keys en el frontend de nuestras apps.

Seguridad con APIs y variables de entorno

Cuando trabajamos en equipo y también cuando utilizamos datos sensibles que no queremos compartir con el mundo para que no se abusen de ellos (por ejemplo, nuestra API key, que pertenece a nuestro usuario), se utilizan variables de entorno en un archivo .env, el cual puede tener una extensión distinta si es que son variables del entorno de desarrollo local (.env.development.local) que utilizamos cuando estamos desarrollando en nuestra PC o variables de producción (.env.production) que son para cuando nuestra aplicación ya está funcionando en el mundo exterior. Entonces vamos a aprovechar una de las bondades que nos da create-react-app y vamos a generar un archivo de desarrollo local para usar nuestra API key sin tener que luego preocuparnos por la seguridad de la misma si subimos nuestro código a github por ejemplo. Según la documentación (en inglés), debemos crear un archivo en la raíz de nuestro proyecto (sería junto al package.json, el .gitignore, etc.) y lo vamos a llamar .env.development.local

(sí, comienza con un punto, al igual que el .gitignore). Dentro debemos definir nuestra variable de entorno, que por convención se escriben en mayúsculas y según la documentación de create-react-app, DEBE comenzar con "REACT_APP". Podemos llamarla REACT_APP_API_KEY para saber a qué nos referimos cuando la querramos usar. Entonces dentro del archivo debemos tener una línea como la siguiente:

REACT_APP_API_KEY = 'c4321dd709e94986a41d00XXXXXXXXXX'

Ya con tener esa línea podemos usar esta variable para hacer el fetch, para utilizarla debemos anteponer process.env. + nuestra variable a utilizar. En nuestro caso podemos usarla sumando strings "a la vieja usanza", o en mi opinión de forma más prolija, utilizando template strings de ES6, lo cual quedaría de la siguiente manera:

`https://api.weatherbit.io/v2.0/forecast/daily?city=Buenos+Aires,Argentina&key=${process.env.REACT_APP_API_KEY}&days=7`

Donde podemos observar que el string en su totalidad va a estar envuelto entre dos backticks ` y al momento de utilizar la variable, debe estar entre los caracteres ${}, queda más limpio que "string" + variable + "string", no? También se pueden utilizar expresiones entre esos caracteres, es súper útil!

IMPORTANTE: En este momento ya teniendo nuestro archivo generado con la data de nuestra API key, hay que reiniciar el servidor local de desarrollo para que nuestra app pueda ver el .env.development.local, sino vamos a tener problemas después y no vamos a poder usar la variable de entorno. Así que vamos a nuestra terminal donde esté corriendo el servidor y hacemos Ctrl + C y luego corremos el comando npm start. Ahora podemos seguir sin problemas 😁

Bueno, ya tenemos nuestra API key y la vamos a tener resguardada a la hora de subir nuestro código a algún lado (no se va a subir ese archivo porque figura en el .gitignore), ya no nos queda otra que arrancar con el fetch, manos a la obra!

Esto es lo que vamos a hacer:

  • Crear un estado en nuestro componente donde vamos a guardar los datos que vayamos a buscar a la API
  • "Pegarle" a la API usando fetch() y guardando la respuesta en formato JSON en el estado
  • Empezar a utilizar los datos de ese estado en el componente App que tenemos

Vamos a crear el estado (tal como vimos antes, se hace en el constructor del componente):

/* [...] */
class App extends Component {
  // Agregamos el constructor y definimos el estado
  constructor(props) {
    super(props);
    this.state = {
      current: {},
      forecast: [],
      location: {}
    };
  }

  render() {
/* [...] */

Ya vimos más arriba qué recibimos en el JSON y un link a la docu de Weatherbit para ver para qué nos sirve cada cosa, pero pasemos en limpio:

En nuestro objeto de respuesta de la API, vamos a tener algunos datos sueltos como ser el nombre de la ciudad, el código del país, latitud y longitud... Pero lo que más nos interesa estudiar es el Array (colección de cosas) de nombre data, el cual tiene objetos con el clima de los 7 días que pedimos incluyendo hoy (el día actual es el primer objeto en el array)

Por lo que podemos ver, en la parte de arriba (el div con la clase 'top') vamos a usar un objeto con algunos datos de los que están sueltos y el primer objeto de data.

Vamos a manipular un poco los datos que nos lleguen de la API para intentar hacer más amena la transición a esta versión del workshop (si hiciste la versión de Apixu, este refactor espero que no te complique la existencia). En current vamos a guardar el primer valor del array data, que como dijimos antes, va a ser el clima del día corriente. Luego en forecast guardamos el resto de los días para usarlos en la parte inferior y location va a ser un objeto que vamos a armar a mano con una estructura parecida a un objeto que venía de la anterior API (Apixu te odio 😑). Vayamos entonces a hacer el fetch() a la dirección que obtuvimos antes, con nuestra API_KEY y como dijimos, va a ser adentro del método componentDidMount():

/* [...] */
class App extends Component {
  constructor(props) {
      /* [...] */
  }

  // Definimos el método componentDidMount con el fetch
  componentDidMount() {
    fetch(
      `https://api.weatherbit.io/v2.0/forecast/daily?city=Buenos+Aires,Argentina&key=${process.env.REACT_APP_API_KEY}&days=7`
    )
      .then(response => response.json())
      .then(jsonData => {
        const current = jsonData.data.shift();
        const forecast = jsonData.data;
        const location = {
          city_name: jsonData.city_name,
          country_code: jsonData.country_code,
          state_code: jsonData.state_code
        };
        this.setState({ current: current, forecast: forecast, location: location });
      });
  }

  render() {
/* [...] */

Vamos a analizar un poco lo que hicimos en el método, primero hicimos el fetch (el link es a la documentación para ampliar) y esto nos retorna un objeto Promise, es una promesa de la que vamos a obtener nuestros datos. Utilizando programación funcional con function chaining, podemos decir que then (entonces), una vez obtenida la response (respuesta), la retorne en formato JSON, usando el método json(). Luego siguiendo con la cadena de funciones, ese JSON va a caer en el siguiente then y lo recibimos en jsonData, donde está nuestra data del clima que necesitamos manipular. Como dijimos, en current guardamos el primer objeto usando el método shift() de arrays, lo cual ya nos deja a data con los 6 días restantes, esos los metemos en forecast y luego nos armamos location con un objeto con los datos que queremos mostrar, actualizando luego el estado y guardando todo lo que generamos recién en cada parte del estado de nuestro componente. Algo copado que podemos hacer es, que si nuestra variable (o constante) tiene el mismo nombre que lo que queremos modificar en el estado, se puede poner de la siguiente manera:

this.setState({ current, forecast, location })

Queda más compacto y a gusto de cada uno, yo lo prefiero así, más cortito 😁

Buenísimo, ya tenemos los datos en el estado de nuestro componente, ahora nos falta empezar a usarlos!

Un truquito (ponele) para que no nos quede taaan largo nuestro código al usar los datos de nuestro estado (this.state.clima.blablabla cada vez), podemos usar destructuring de ES6:

const { location, current, forecast } = this.state

Esto debemos insertarlo en el render() de nuestro componente, antes del return. Luego podemos acceder a nuestra data por ejemplo haciendo location.city_name, current.temp, etc. Ya vimos cómo acceder a los datos que recibimos en el estado, así que les dejo un rato para ir acomodando toda la data en la parte de arriba de nuestro componente... ... ... Listo? Ahhh te tira errores? undefined por todos lados? Bienvenido/a a Javascript 🤣🤣 Nah, para evitar estas cosas existe algo súper útil que es el conditional rendering (link a la docu), donde le decimos que renderice cuando ya nuestra data esté "fetcheada" y disponible. Esto lo logramos con un valor booleano (true o false) en el estado del componente que debemos alterar una vez que terminemos con el fetch.

Entonces agregamos en el estado isLoaded: false para indicar que todavía no está cargada nuestra data. Luego en el fetch() actualizamos nuestro estado e indicamos que ya junto con nuestra data cargada, cambiamos isLoaded a true: this.setState({ current, forecast, location, isLoaded: true }). Y luego al renderizar, retornamos según esté cargada o no nuestra data, verificando con el operador ternario de la siguiente forma:

render() {
  const { location, current, forecast, isLoaded } = this.state;
    return isLoaded ? (
      {/* Nuestro componente */}
    ) : (
      <div className="App">
          Cargando...
      </div>
    )
}

Bueno, el tema de la imagen del clima de arriba es un poco complejo porque Weatherbit adentro de nuestro objeto del clima actual nos provee un objeto weather con una propiedad icon, lo cual es un string con unos caracteres raros como "c02d" 🤔🤔🤔 Pero si escarban en la documentación de Weatherbit (o les paso mejor los links directo 😜) van a ver que cada código responde a un iconito distinto del clima en este link Entonces vamos a tener que bajar los iconitos uno por uno? 🤦‍♂️🤦‍♀️ Nooooo, tranquis, que Weatherbit es copado y nos provee un pack de íconos que ya tiene cada uno el nombre correspondiente al código que nos llega desde la API ;D Así que vayamos a este link y bajemos el último pack con todas las imágenes, Weatherbit weather Icons, y podemos descomprimirlo en /src para que nos quede /src/icons y nuestras imágenes png adentro.

(Gracias Pablo (@wbkpablo) por pasarme el link del pack y coparte a avisarme todos los problemas del workshop)

Ahora debemos indicarle al elemento img dónde tiene que ir a buscar las imágenes para cada iconito que nos llega, y vamos a hacer que los busque de forma dinámica.

Esto lo podemos hacer con los template strings de los que hablamos antes pero con una salvedad: necesitamos que nuestra app de react se entere de las imágenes que están en esa carpeta, usando require(). Sino tendríamos que copiar nuestra carpeta directo en /public y no es algo recomendable... Así que les dejo intentarlo y si no les sale les dejo cómo sería acá abajo:

Para ver la solución de cada archivo hacé click en las pestañas 👆
<img
  src={require(`./icons/${current.weather.icon}.png`)}
  alt="Clima principal"
  className="image"
/>

Una vez que terminamos con la parte de arriba, debería quedarnos algo así nuestro App.js:

Para ver la solución de cada archivo hacé click en las pestañas 👆
import React, { Component } from 'react'
import logo from './logo.svg'
import './App.css'

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      current: {},
      forecast: [],
      location: {}
      isLoaded: false
    }
  }

  componentDidMount() {
    fetch(
      `https://api.weatherbit.io/v2.0/forecast/daily?city=Buenos+Aires,Argentina&key=${process.env.REACT_APP_API_KEY}&days=7`
    )
      .then(response => response.json())
      .then(jsonData => {
        const current = jsonData.data.shift();
        const forecast = jsonData.data;
        const location = {
          city_name: jsonData.city_name,
          country_code: jsonData.country_code,
          state_code: jsonData.state_code
        };
        this.setState({ current, forecast, location, isLoaded: true });
      });
  }

  render() {
    const { location, current, forecast, isLoaded } = this.state
    return isLoaded ? (
      <div className="App">
        <div className="container">
          <div className="top">
            <img
              src={require(`./icons/${current.weather.icon}.png`)}
              alt="Clima principal"
              className="image"
            />
            <p className="temp">{current.temp} °C</p>
            <h4 className="city">
              {location.city_name}, {location.state_code}, {location.country_code}
            </h4>
            <div className="temphr">
              Max: {current.max_temp} °C, Min: {current.min_temp} °C, H: {current.rh} %
            </div>
          </div>
          <div className="bottom">
            <div className="card">
              <h5>Lunes</h5>
              <h6>2018-09-26</h6>
              <img src={logo} alt="Clima diario" className="imagebottom" />
              <div className="minmax">
                <p>
                  <span className="tempmax" />
                  26
                </p>
                <p>
                  <span className="tempmin" />
                  15
                </p>
              </div>
            </div>
            <div className="card">
              <h5>Martes</h5>
              <h6>2018-09-27</h6>
              <img src={logo} alt="Clima diario" className="imagebottom" />
              <div className="minmax">
                <p>
                  <span className="tempmax" />
                  26
                </p>
                <p>
                  <span className="tempmin" />
                  15
                </p>
              </div>
            </div>
            <div className="card">
              <h5>Miércoles</h5>
              <h6>2018-09-28</h6>
              <img src={logo} alt="Clima diario" className="imagebottom" />
              <div className="minmax">
                <p>
                  <span className="tempmax" />
                  26
                </p>
                <p>
                  <span className="tempmin" />
                  15
                </p>
              </div>
            </div>
            <div className="card">
              <h5>Jueves</h5>
              <h6>2018-09-29</h6>
              <img src={logo} alt="Clima diario" className="imagebottom" />
              <div className="minmax">
                <p>
                  <span className="tempmax" />
                  26
                </p>
                <p>
                  <span className="tempmin" />
                  15
                </p>
              </div>
            </div>
            <div className="card">
              <h5>Viernes</h5>
              <h6>2018-09-30</h6>
              <img src={logo} alt="Clima diario" className="imagebottom" />
              <div className="minmax">
                <p>
                  <span className="tempmax" />
                  26
                </p>
                <p>
                  <span className="tempmin" />
                  15
                </p>
              </div>
            </div>
            <div className="card">
              <h5>Sábado</h5>
              <h6>2018-10-01</h6>
              <img src={logo} alt="Clima diario" className="imagebottom" />
              <div className="minmax">
                <p>
                  <span className="tempmax" />
                  26
                </p>
                <p>
                  <span className="tempmin" />
                  15
                </p>
              </div>
            </div>
          </div>
        </div>
      </div>
    ) : (
      <div className="App">Cargando...</div>
    )
  }
}

export default App

Lo tenés igual? Parecido? Bueno, si ves que te trabaste con algo o no te sale, podés copiar y pegar o leer el código por algún gotcha de algo que no te esté saliendo 😁

Listo, ya tenemos la parte de arriba andando perfectamente y ahora vamos a atacar la parte de abajo. Analicémosla un poco antes...

Parte de arriba terminada

La idea de React y los componentes es tener partes reutilizables de código, entonces, qué vemos que se repite y pueda reutilizarse en nuestro componente? Eso que se repite podemos separarlo en un componente aparte. Ya lo identificaste?

© 2021 - MIT License