hand Inicio
hand JSBloqs
hand GutenBloqs
Qué?
nochedía

DESARROLLO WEB con
REACT y WORDPRESS

Apúntate a la newsletter (escribo algo de tanto en cuanto)
Wallpaper)

WebSite de wallpapers con React

800 palabras
3 minutos
July 15, 2020
cursosgatsbybasico

Vamos a preparar una aplicación parecida a la extensión de chrome que exploramos otro curso con vanilla JS, pero esta vez en React

Prólogo

Se trata de conseguir una herramienta que nos muestre fondos de pantalla para inspirarnos, parecida a la que exploramos en este otro curso, pero esta vez en un entorno React

En mi caso esta herramienta la utilizo como fondo de una segunda pantalla que bastantes veces está vacía y me sirve como marco

La herramienta final la puedes encontrar kuworking.com/wallpapers, hecha en Gatsby

Aquí lo vamos a explorar en React, y es que al final integrar cualquier aplicación React en Gatsby es bastante trivial

Vamos allá

  1. Prólogo
  2. Código
  3. Comentario

Código

Básicamente, lo que quiero es lo siguiente:

  • Pedir una imagen
  • Después rellenar un pool de imágenes
  • Mostrar las imágenes
  • Que al clicar la imagen esta se cambie y se utilicen los keywords del <input>

La mejor manera es visualizar un custom hook que nos proporcione lo que queremos

js
import React from 'react'
import styled from '@emotion/styled'
export const Wallpaper = () => {
const [background, change_background, inputRef] = useChangeBackground()
return (
<>
<Text>
<input type="text" ref={inputRef} />
</Text>
<WallpaperDiv background={background} onClick={change_background} />
</>
)
}

Con esto lo tendríamos todo

  • Un <input> para gestionar las keywords
  • Un <Wallpaper> que nos muestre el fondo de pantalla
  • Y que ese mismo wallpaper nos sirva de botón para cambiar el fondo de pantalla

Ahora vamos a definir el hook, que el elefante de la habitación

Con una salvedad, y es que cuando pedimos la imagen random a unsplash ésta no nos da la imagen sino que nos devuelve la url definitiva y luego tenemos que volver a pedir esa url

js
const useChangeBackground = () => {
// el background actual
const [background, setBackground] = useState([])
// la lista de backgrounds
const [backgroundPool, setBackgroundPool] = useState([])
// la lista de backgrounds segunda parte (para evitar un race condition)
const [backgroundPoolTemp, setBackgroundPoolTemp] = useState([])
// los keywords utilizados
const [keywords, setKeywords] = useState('coworking')
// La referencia para el input
const inputRef = useRef()
// función para forzar un cambio de imagen
const change_background = async () => {
// creo una copia del array y si está vacío volco el contenido del array segunda parte
// .. justo en este momento si el useEffect que rellena este array segunda parte está activo
// .. puede darse un race condition que me dé imágenes repetidas
const pool = backgroundPool.length === 0 ? [...backgroundPoolTemp] : [...backgroundPool]
if (backgroundPool.length === 0) {
setBackgroundPool(backgroundPoolTemp);
setBackgroundPoolTemp([]);
}
// para saber si las keywords son nuevas o no, si lo son necesito volver a precargar las listas de imágenes
const [changed] = get_keywords()
// función para encontrar la primera imagen que no sea la actual
// siempre debería devolver la primera, pero un race condition puede evitarlo
const find_new_image = (img, arr) =>
(img === arr[0] ? find_new_image(img,[...arr.slice(1)]) : arr[0])
// si no ha cambiado cojo la primera imagen de la lista de imágenes percargadas
const image = changed ? await get_images(true) : find_new_image(background, pool)
if (changed) {
// si ha cambiado vacío las dos listas
setBackgroundPool([])
setBackgroundPoolTemp([])
} else {
// si no ha cambiado quito de la lista la imagen que he cargado
const num = pool.findIndex(el => el === image)
setBackgroundPool(pool.slice(num + 1))
}
// pongo la imagen actual en su estado
setBackground(image)
}
// Al principio necesito cargar una imagen
useEffect(() => {
;(async () => {
const image = await get_images()
setBackground(image)
})()
}, [])
// función para recuperar los keywords del <input> i de paso ver si han cambiado o no
const get_keywords = () => {
const newKeywords = inputRef.current.value.replace(/\s/g, '') || keywords
const changed = keywords !== newKeywords
if (changed) setKeywords(newKeywords)
return [changed, newKeywords]
}
// funcion para bajar una imagen, si no es forzada mira si es el mismo día para mostrar la misma durante ese día)
const get_images = async (force = false) => {
const update = force ? true : is_this_a_new_day()
const image = update ? await get_image() : window.localStorage.getItem('kw-wallpaper') || (await get_image())
return image
}
// función para conseguir la url de unsplash y ver si ya la tenemos en el cache o no
// ..por qué no utilizo la keywords del estado? Porque aquí aún no se me habrá actualizado
const get_image = async () => {
const [, keywords] = get_keywords()
const different_number = new Date().getMilliseconds()
const random_url = `https://source.unsplash.com/random/?sig=${different_number}&${keywords}`
const { url } = await get_response([fetch(random_url), wait(5000)], 5)
const request = new Request(url)
const cache = await caches.open('kw-cache')
const cached_image = await cache.match(request)
if (!cached_image) cache.add(request)
return cached_image ? cached_image.url : url
}
// rellenar el pool de imágenes
useEffect(()=>{
if (backgroundPoolTemp.length >= 10) return
(async () => {
// rellenamos una imagen cada medio segundo
await wait(500)
let temp_pool = [...backgroundPoolTemp]
const response = await get_image()
// nos aseguramos que la imagen no esté repetida dentro del pool
if (!temp_pool.includes(response) && !response.endsWith('undefined')) {
temp_pool.push(response)
temp_pool = [...new Set(temp_pool)] // eliminamos duplicados
}
setBackgroundPoolTemp([...temp_pool])
})()
},[background, backgroundPoolTemp])
// retornamos lo dicho antes
return [background, change_background, inputRef]
}

Y un par de funciones (helpers) que nos dan funcionalidades concretas

js
// función para esperar un tiempo con promesas
const wait = ms => new Promise((res, rej) => setTimeout(() => res('timed'), ms))
// función para determinar si el día ha cambiado o qué
const is_this_a_new_day = () => {
const last_date = Number(window.localStorage.getItem('kw-date')) || ''
const today = Number(new Date().getDay())
window.localStorage.setItem('kw-date', today)
return today === Number(last_date) ? false : true
}
// función para pedir la imagen durante un tiempo limitado
// ..para no quedarnos eternamente esperando
const get_response = async (functions, times) => {
// to state a time limit for an await function
let response = await Promise.race(functions)
let counter = 0
while (response === 'timed') {
response = await Promise.race(functions)
counter++
if (counter > times) break
}
if (response !== 'timed') return response
return false
}

En total, una estructura muy simple

Vamos a comentarlo

El componente principal <Wallpaper>

  • Simplemente nos devuelve un <input> y un <div> que tendrá la variable de background como fondo

Las funciones "extra" o helpers nos encapsulan la lógica para

  • wait nos permite esperar un tiempo bloqueando la ejecución
  • is_this_a_new_day nos devuelve un booleano que nos indica si estamos en un nuevo día o no (y si es que no, repetimos la imagen)
  • get_response nos hace un fetch controlado en el tiempo, porque a veces una petición se encalla y vale la pena terminarla y volver a repetir el tema

El hook useChangeBackground se encarga de toda la lógica restante

  • Devuelve [background, change_background, inputRef], es decir el estado donde se guarda el wallpaper, el método para cambiar el wallpaper, y la referencia del input

  • Trabaja con tres estados principales: el background en sí, una lista para almacenar los backgrounds pre-cargados, y otra lista para realmente agregar los backgrounds pre-cargados

La razón de tener dos listas aparentemente redundantes es porque la del medio me sirve como lista neutral

Es decir, cuando cambio el background con un click, este mueve un background de la lista neutral al background en sí

Mientras tanto, la lista donde se van pre-cargando backgrounds está contínuamente manteniéndose "llena" con 10 imágenes

El problema lo tengo cuando muevo ese background de la lista, ya que si en lugar de ser una lista independiente fuera la misma lista con los backgrounds pre-cargados entonces entraríamos en race conditions con lo que la misma lista se editaría desde dos sitios al mismo tiempo

En la práctica, esto quiere decir que repetiríamos imágenes básicamente porque estaríamos viendo listas previas

Total, con un array intermedio esto lo suavizo ya que sólo hago el volcado cuando ésta está vacía, y sólo es en ese momento donde hay riesgo de race conditions (lo tienes en los comentarios del código)

  • La función change_background se ocupa precisamente de eso, de gestionar el que se carge una nueva imagen ya sea nueva porque los keywords han cambiado, o una de las imágenes pre-cargadas

  • Luego tenemos un useEffect con [] que simplemente se encarga de cargar la primera imagen de todas

  • Y luego tenemos el segundo useEffect con [background, backgroundPoolTemp]

Es decir, este useEffect se ejecuta cada vez que background cambia, lo cual pasa a cada click para cambiar el background

Y también cada vez que backgroundPoolTemp cambia, que pasa siempre en ese mismo bloque excepto cuando tiene más de 10 elementos

En la práctica, esta función se ocupa de mantener siempre la lista de imágenes precargadas llena hasta 10 imágenes, y cuando ésta se vuelva a la lista neutral entonces tendríamos 20 imágenes precargadas

Listos

Lo puedes probar en el codesandbox con el código final aquí abajo

Recuerda de añadir las dependencias de @emotion/core y @emotion/styled

jsx
import React, { useState, useEffect, useRef } from 'react'
import styled from '@emotion/styled'
export const Wallpaper = () => {
const [background, change_background, inputRef] = useChangeBackground()
return (
<>
<Text>
<input type="text" ref={inputRef} />
</Text>
<WallpaperDiv background={background} onClick={change_background} />
</>
)
}
export default Wallpaper
const WallpaperDiv = styled.div`
position: absolute;
cursor: pointer;
width: 100%;
height: 100vh;
background-size: cover;
background-repeat: no-repeat;
background-position-x: center;
background-position-y: center;
background-attachment: fixed;
background-image: url('${props => props.background}');
`
const Text = styled.div`
padding-bottom: 5px;
z-index: 1;
& > input {
font-size: 1.2em;
font-weight: 700;
width: 200px;
padding: 5px;
background-color: #eed0ff;
border-radius: 3px;
border: unset;
color: #fff;
&::placeholder {
color: #fff;
}
}
`
const wait = ms => new Promise((res, rej) => setTimeout(() => res('timed'), ms))
const is_this_a_new_day = () => {
const last_date = Number(window.localStorage.getItem('kw-date')) || ''
const today = Number(new Date().getDay())
window.localStorage.setItem('kw-date', today)
return today === Number(last_date) ? false : true
}
const get_response = async (functions, times) => {
// to state a time limit for an await function
let response = await Promise.race(functions)
let counter = 0
while (response === 'timed') {
response = await Promise.race(functions)
counter++
if (counter > times) break
}
if (response !== 'timed') return response
return false
}
const useChangeBackground = () => {
// el background actual
const [background, setBackground] = useState([])
// la lista de backgrounds
const [backgroundPool, setBackgroundPool] = useState([])
// la lista de backgrounds segunda parte (para evitar un race condition)
const [backgroundPoolTemp, setBackgroundPoolTemp] = useState([])
// los keywords utilizados
const [keywords, setKeywords] = useState('coworking')
// La referencia para el input
const inputRef = useRef()
// función para forzar un cambio de imagen
const change_background = async () => {
// creo una copia del array y si está vacío volco el contenido del array segunda parte
// .. justo en este momento si el useEffect que rellena este array segunda parte está activo
// .. puede darse un race condition que me dé imágenes repetidas
const pool = backgroundPool.length === 0 ? [...backgroundPoolTemp] : [...backgroundPool]
if (backgroundPool.length === 0) {
setBackgroundPool(backgroundPoolTemp);
setBackgroundPoolTemp([]);
}
// para saber si las keywords son nuevas o no, si lo son necesito volver a precargar las listas de imágenes
const [changed] = get_keywords()
// función para encontrar la primera imagen que no sea la actual
// siempre debería devolver la primera, pero un race condition puede evitarlo
const find_new_image = (img, arr) =>
(img === arr[0] ? find_new_image(img,[...arr.slice(1)]) : arr[0])
// si no ha cambiado cojo la primera imagen de la lista de imágenes percargadas
const image = changed ? await get_images(true) : find_new_image(background, pool)
if (changed) {
// si ha cambiado vacío las dos listas
setBackgroundPool([])
setBackgroundPoolTemp([])
} else {
// si no ha cambiado quito de la lista la imagen que he cargado
const num = pool.findIndex(el => el === image)
setBackgroundPool(pool.slice(num + 1))
}
// pongo la imagen actual en su estado
setBackground(image)
}
// Al principio necesito cargar una imagen
useEffect(() => {
;(async () => {
const image = await get_images()
setBackground(image)
})()
}, [])
// función para recuperar los keywords del <input> i de paso ver si han cambiado o no
const get_keywords = () => {
const newKeywords = inputRef.current.value.replace(/\s/g, '') || keywords
const changed = keywords !== newKeywords
if (changed) setKeywords(newKeywords)
return [changed, newKeywords]
}
// funcion para bajar una imagen, si no es forzada mira si es el mismo día para mostrar la misma durante ese día)
const get_images = async (force = false) => {
const update = force ? true : is_this_a_new_day()
const image = update ? await get_image() : window.localStorage.getItem('kw-wallpaper') || (await get_image())
return image
}
// función para conseguir la url de unsplash y ver si ya la tenemos en el cache o no
// ..por qué no utilizo la keywords del estado? Porque aquí aún no se me habrá actualizado
const get_image = async () => {
const [, keywords] = get_keywords()
const different_number = new Date().getMilliseconds()
const random_url = `https://source.unsplash.com/random/?sig=${different_number}&${keywords}`
const { url } = await get_response([fetch(random_url), wait(5000)], 5)
const request = new Request(url)
const cache = await caches.open('kw-cache')
const cached_image = await cache.match(request)
if (!cached_image) cache.add(request)
return cached_image ? cached_image.url : url
}
// rellenar el pool de imágenes
useEffect(()=>{
if (backgroundPoolTemp.length >= 10) return
(async () => {
// rellenamos una imagen cada medio segundo
await wait(500)
let temp_pool = [...backgroundPoolTemp]
const response = await get_image()
// nos aseguramos que la imagen no esté repetida dentro del pool
if (!temp_pool.includes(response) && !response.endsWith('undefined')) {
temp_pool.push(response)
temp_pool = [...new Set(temp_pool)] // eliminamos duplicados
}
setBackgroundPoolTemp([...temp_pool])
})()
},[background, backgroundPoolTemp])
// retornamos lo dicho antes
return [background, change_background, inputRef]
}
Ver en CodeSandBox

Comentario

Qué le falta al código?

Hay un par de warnings avisándonos que a los useEffect les falta la dependencia get_images y get_image

Estas funciones no van a cambiar nunca, por lo que puedes ignorar el warning, añadirlas como dependencias, o añadir la regla eslint para ignorar este tipo de warnings

Qué más?

Estamos utilizando useEffect con funciones asíncronas

Esto qué quiere decir?

Que si estamos en medio de la ejecución y el componente se cancela (cuando el usuario cambia de página por ejemplo), si el código intenta cambiar un estado esto dará una fuga de memoria

Solución? Cancelar ese código

useEffect nos da la posibilidad de ejecutar código cuando el componente se desmonte

Pues se trata simplemente de utilizar una variable para comprobar si el componente sigue activo o no

Por ejemplo

js
useEffect(() => {
let alive = true
;(async () => {
const image = await get_images()
if (!alive) return
setBackground(image)
})()
return () => (alive = false)
}, [])

Así evitamos el setBackground si ya no hay componente

🙋‍♂️

Qué tal el curso?

👌 Bien 🙌🙌
👍 Bien, pero algunas cosas podrían explicarse mejor 😬
🤷‍♂️ Da por sentadas demasiadas cosas 😒
🤷‍♂️ A ver, hay poca chicha 😬
🤷‍♂️ Los ejemplos no son muy claros 🙇‍♂️
🤷‍♂️ No se entiende, está mal escrito 👎
✍️ Hay errores, revísalo en cuanto puedas 🙏
Enviar Feedback ✍️
El texto está en blanco!
Gracias por enviarme tu opinión
👍

Si quieres explorar más cursos y más entradas en el blog, los tienes todos en la página principal, y si el contenido te ha ayudado si lo compartes me ayudas a dar difusión 👍

Privacidad
by kuworking.com
[ 2020 >> kuworking ]