React Workshop

Workshop - Paso 2

Dividamos en componentes

Sigamos con la segunda parte, ya pensamos qué parte de nuestro componente App podemos aislar y reutilizar convirtiéndola en componente, me imagino que pensamos todos lo mismo, lo decimos a la vez? 1, 2... El contai... La card!! Por como vemos, necesitamos mostrar 6 veces los mismos datos, entonces es nuestro candidato perfecto para aislar, luego se puede pasar otras cosas a componentes, la especificación ya queda en cada uno según la modularización que quiera realizar (como por ejemplo, el div con clase "minmax" también se repite bastante, no? 😏) Antes de hacer el componente, tenemos que analizar lo que tenemos y cómo lo queremos mostrar. Qué tenemos? Nuestra card:

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

Y un array forecast con los 6 días restantes que le pedimos a la API:

{
  "moonrise_ts": 1573778575,
  "wind_cdir": "SSE",
  "rh": 65,
  "pres": 1008.87,
  "high_temp": 26.7,
  "sunset_ts": 1573770827,
  "ozone": 279.889,
  "moon_phase": 0.841582,
  "wind_gust_spd": 12.1112,
  "snow_depth": 0,
  "clouds": 48,
  "ts": 1573700460,
  "sunrise_ts": 1573720547,
  "app_min_temp": 19.6,
  "wind_spd": 3.9362,
  "pop": 70,
  "wind_cdir_full": "south-southeast",
  "slp": 1010.46,
  "valid_date": "2019-11-14",
  "app_max_temp": 27.1,
  "vis": 21.7656,
  "dewpt": 15.2,
  "snow": 0,
  "uv": 8.43664,
  "weather": {
    "icon": "t02d",
    "code": 201,
    "description": "Thunderstorm with rain"
  },
  "wind_dir": 168,
  "max_dhi": null,
  "clouds_hi": 46,
  "precip": 7.0625,
  "low_temp": 19.4,
  "max_temp": 27.1,
  "moonset_ts": 1573731951,
  "datetime": "2019-11-14",
  "temp": 22.6,
  "min_temp": 19.2,
  "clouds_mid": 19,
  "clouds_low": 5
}

Viendo los nombres de las cosas podemos ir pensando dónde poner cada cosa, no? Lo que nos falta ver es cómo iterar, recorrer el array usando los datos de ese array en un componente que reutilicemos en cada iteración.

Para eso tenemos el método map() de Array, que en cada iteración vamos a tener disponible los días tal como vemos en el JSON que está acá arriba ☝️

El uso de map() en nuestro JSX sería:

import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";

class App extends Component {

  /* [...] */

  render() {
    const { location, current, forecast, isLoaded } = this.state;
    return isLoaded ? (
      <div className="App">
        <div className="container">
          <div className="top">

            {/* [...] */}

          </div>
          <div className="bottom">
            {/*Usamos map() donde va el código que vamos a repetir*/}
            {forecast.map(dia => {
              /* en 'dia' vamos a tener cada objeto del array */
              return(
                /*
                  Acá iteramos nuestra card con la data del 'dia'
                  y la retornamos
                */
              )}
            )}
          </div>
        </div>
      </div>
    ) : (
      <div className="App">Cargando...</div>
    );
  }
}

export default App;

Ya tenemos entonces nuestro código a repetir, la data en un array y nuestra herramienta para iterar, prueben hacerlo y cualquier cosa dejo la solución en el cuadrito de acá abajo. Pista: Tiene mucha similitud con lo que hicimos en el Paso 01, presten atención a la parte de la imagen 😉

Para ver la solución de cada archivo hacé click en las pestañas 👆
<div className="bottom">
  {forecast.map(dia => {
    return (
      <div className="card">
        <h5>{this.getWeekDay(dia.datetime)}</h5>
        <h6>{dia.valid_date}</h6>
        <img
          src={require(`./icons/${dia.weather.icon}.png`)}
          alt="Clima diario"
          className="imagebottom"
        />
        <div className="minmax">
          <p>
            <span className="tempmax" />
            {parseInt(dia.max_temp)}
          </p>
          <p>
            <span className="tempmin" />
            {parseInt(dia.min_temp)}
          </p>
        </div>
      </div>
    )
  })}
</div>

Esa es mi solución, ah, qué? Que qué es getWeekDay()? Es una función que hice yo 😛 me pasó Goncy para mostrar el día de la semana calculando desde la fecha en string que nos proveen en datetime. Para hacer la función es como hicimos antes cuando vimos handleClick en Props y State, pero debemos definir antes un array con los días de la semana, el cual va por fuera de la definición de nuestro componente.

import React, { Component } from "react";
import "./App.css";

/* Generamos un array con los días de la semana */
const semana = ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado",
"Domingo"]

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      current: {},
      forecast: [],
      location: {},
      isLoaded: false
    };
    //No hay que olvidarse del binding
    this.getWeekDay = this.getWeekDay.bind(this);
  }

  getWeekDay(fecha) {
    return semana[new Date(fecha).getDay()];
  }
  {/* [...] */}
}

Algo para tener en cuenta cuando iteramos: Si vemos la consola, React nos dice que cada hijo en un array o iterador debe tener una prop única que sea key (clave), tiene que ser un valor único que no se repita dentro del array. Pasar el índice como key se considera malas prácticas ya que si eliminamos o movemos datos dentro del array, ese índice va cambiando, algo similar puede pasar utilizando el id como key si no lo manejamos bien. En nuestro caso la key puede ser el día de la semana que sabemos que va a ser único, entonces pasamos eso como prop a lo que se itera, nos quedaría:

<div className="card" key={dia.valid_date}>

También vemos que las fechas máxima y mínima desde la API nos la proveen como un número con decimal, al utilizar parseInt() obtenemos la parte entera del número únicamente (en lo personal me gusta más un número entero).

Ya está, tenemos todo funcional y todo lindo, ahora nos falta algo clave, componentizar. Dejo el código hasta ahora y pensamos las divisiones:

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

const semana = [
  'Lunes',
  'Martes',
  'Miércoles',
  'Jueves',
  'Viernes',
  'Sábado',
  'Domingo',
]

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

  getWeekDay(fecha) {
    return semana[new Date(fecha).getDay()]
  }

  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">
            {forecast.map(dia => {
              return (
                <div className="card" key={dia.valid_date}>
                  <h5>{this.getWeekDay(dia.datetime)}</h5>
                  <h6>{dia.valid_date}</h6>
                  <img
                    src={require(`./icons/${dia.weather.icon}.png`)}
                    alt="Clima diario"
                    className="imagebottom"
                  />
                  <div className="minmax">
                    <p>
                      <span className="tempmax" />
                      {parseInt(dia.max_temp)}
                    </p>
                    <p>
                      <span className="tempmin" />
                      {parseInt(dia.min_temp)}
                    </p>
                  </div>
                </div>
              )
            })}
          </div>
        </div>
      </div>
    ) : (
      <div className="App">Cargando...</div>
    )
  }
}

export default App

Ahora es cuando uno decide qué tanto componentizar, qué tan atómico ser. Podemos hacer que la parte de arriba sea un componente y la parte de abajo otro, o la parte de arriba y la card otro, o dejar la parte de arriba como parte del componente App y que la parte de abajo sea otro componente... En fin, hay varias posibilidades, para este workshop lo que vamos a hacer es que lo que está en la parte de arriba sea un componente, lo que está abajo sea otro y lo que llamamos card sea otro, que es el que más nos hace ver el tema de la reutilización de código.

Empecemos entonces por la parte de arriba, creamos un archivo nuevo llamado Top.js y creamos nuestro componente funcional con lo que corresponde a la parte de arriba. No olvidemos pasar por props lo que necesitemos en el componente (current y location), que lo vamos a tener disponible en nuestro nuevo componente en props (para lo cual si usamos ES6 destructuring de nuevo, no tenemos que modificar nada en nuestro JSX 😉). Nos debería quedar así:

Para ver la solución de cada archivo hacé click en las pestañas 👆
import React, { Component } from "react";
// Importamos nuestro componente nuevo,
// presten atención a la carpeta donde lo crean
import Top from "./Top";

import "./App.css";

class App extends Component {

  /* [...] */

  render() {
    const { location, current, forecast, isLoaded } = this.state;
    return isLoaded ? (
      <div className="App">
        <div className="container">
          {/* Utilizamos nuestro componente y le pasamos props */}
          <Top current={current} location={location}/>

          <div className="bottom">
            {/* [...] */}
          </div>

        </div>
      </div>

    /* [...] */
}
import React, { Component } from "react";
// El archivo css no es necesario cargarlo ya que se carga en el componente padre

// Indicamos que la función recibe props
const Top = (props) => {
  // Usamos ES6 destructuring
  const { current, location } = props;
  return(
    <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>;
  );
};

export default Top;

Ahora les queda como tarea hacer lo mismo con el componente de abajo, al que podemos llamar Bottom, recuerden también crear uno Card que va a ser utilizado adentro de las iteraciones del map() y va a haber que pasar las props dos niveles para abajo en el árbol de jerarquía. También presten atención a lo que se usa en Card, qué es lo que se tienen que llevar de App 😉

Una vez que finalicen, les deberían quedar estos archivos: (no espíen antes de probar ehhhhh)

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

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">
          <Top current={current} location={location} />

          <Bottom forecast={forecast} />
        </div>
      </div>
    ) : (
      <div className="App">Loading...</div>
    )
  }
}

export default App
import React from 'react'

const Top = props => {
  const { current, location } = props
  return (
    <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>
  )
}

export default Top
import React from 'react'
import Card from './Card'

const Bottom = props => {
  const { forecast } = props
  return (
    <div className="bottom">
      {forecast.map(dia => {
        return <Card dia={dia} key={dia.valid_date} />
      })}
    </div>
  )
}

export default Bottom
import React from 'react'

const semana = [
  'Lunes',
  'Martes',
  'Miércoles',
  'Jueves',
  'Viernes',
  'Sábado',
  'Domingo',
]

const getWeekDay = fecha => {
  return semana[new Date(fecha).getDay()]
}

const Card = props => {
  const { dia } = props
  return (
    <div className="card">
      <h5>{getWeekDay(dia.datetime)}</h5>
      <h6>{dia.valid_date}</h6>
      <img
        src={require(`./icons/${dia.weather.icon}.png`)}
        alt="Clima diario"
        className="imagebottom"
      />
      <div className="minmax">
        <p>
          <span className="tempmax" />
          {parseInt(dia.max_temp)}
        </p>
        <p>
          <span className="tempmin" />
          {parseInt(dia.min_temp)}
        </p>
      </div>
    </div>
  )
}

export default Card

Listo!! Phew~ ya deberíamos tener nuestra app del clima funcionando a la perfección! En la próxima parte vamos a ver cómo usar Netlify que es un servicio que me gusta MUCHISIMO, no solamente proveen hosting sino que también proveen Netlify Functions, que son lambda functions y funcionan como una especie de backend para nuestra app! 😊

© 2021 - MIT License