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 😉
<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:
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í:
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)
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! 😊