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)

Programar una alternativa a NProgress con Gatsby

1300 palabras
5 minutos
July 10, 2020
cursosgatsbycss

La librería Nprogress nos permite emular el efecto que en su día ciertos sitios punteros (Google, YouTube, Medium) desarrollaron, que no es más que un elemento gráfico que nos da una respuesta visual cuando la transición entre páginas es demasiado lenta

Prólogo

La librería nprogress nos permite añadir un efecto gráfico a la transición entre páginas que nos permite "notar" que ha habido un cambio en nuestra web

Y cómo funciona?

Vamos a verlo y a implementar una alternativa más ligera, con Gatsby

Se trata de ver y reformular la adaptación de nprogress en Gatsby, adaptación hecha con el plugin gatsby-plugin-nprogress

El resultado será éste

Vamos allá

  1. Prólogo
  2. Implementación en Gatsby
  3. Reformulando nprogress
  4. Css
  5. Construyendo un hook
  6. Añadiendo el elemento
  7. Modificando el DOM
  8. Afinando la estética

Implementación en Gatsby

Mirando el gatsby-plugin-nprogress, el código es sencillo (sinónimo de bueno), elegante y corto, y si lo exploramos aquí vemos que se trata de

  • Utilizar nprogress tal y como nos dicen en su página de instrucciones

  • E implementarlo utilizando la API de Gatsby de un modo muy concreto

js
export const onRouteUpdateDelayed = () => {
NProgress.start()
}
export const onRouteUpdate = () => {
NProgress.done()
}

Esto se ubica en el archivo gatsby-browser.js

En gatsby tenemos dos archivos en esa categoría, el gatsby-browser.js y el gatsby-ssr.js

La diferencia entre los dos?

Uno se ejecuta en el cliente, otro en el servidor

Como en Gatsby no tenemos servidor, esto implica que se ejecuta cuando compilamos el código con gatsby build

Pues leer acerca de esto aquí y aquí, y en el artículo de Chris Biscardi

En este caso queremos ejecutar código cuando las rutas cambian, y esto lo podemos hacer en gatsby-browser.js

En concreto utilizamos onRouteUpdateDelayed y onRouteUpdate (podemos consultar la API aquí)

  • El primero se ejecuta cuando el loading de la página se demora por más de 1 segundo
  • El segundo se ejecuta siempre

Y con esto ya tenemos la manera de ejecutar nuestro código cada vez que una página se demore

Reformulando nprogress

Si ahora nos vamos al archivo central de nprogress vemos que lo que hace grosso modo es cargar un archivo css en el documento

Esto es fantástico ya que nos permite simplemente modificar el archivo css y ya lo tenemos (casi sin necesidad de tocar nada más)

Pero por otro lado podemos ver el trabajo necesario para poder entender e inyectar ese css, un trabajo que se delega a la librería css, y eso no deja de ser algo que tendrá cierta factura computacional (aunque posiblemente insignificante)

El equilibrio eterno entre eficiencia y facilidad de uso

Aquí me planteo hacer una estructura de mínimos, más sencilla, y que me dé un resultado parecido y satisfactorio

Css

Lo primero es representar el mismo efecto, que no es más que una fila con height mínimo que va aumentando de width con el tiempo, simulando una especie de relación con la carga de la página

Eso nprogress lo hace con una suerte de función random (la tienes a continuación)

js
NProgress.inc = function (amount) {
var n = NProgress.status
if (!n) {
return NProgress.start()
} else if (n > 1) {
return
} else {
if (typeof amount !== 'number') {
if (n >= 0 && n < 0.2) {
amount = 0.1
} else if (n >= 0.2 && n < 0.5) {
amount = 0.04
} else if (n >= 0.5 && n < 0.8) {
amount = 0.02
} else if (n >= 0.8 && n < 0.99) {
amount = 0.005
} else {
amount = 0
}
}
n = clamp(n + amount, 0, 0.994)
return NProgress.set(n)
}
}
function clamp(n, min, max) {
if (n < min) return min
if (n > max) return max
return n
}

Y lo implementa ejecutando periódicamente esto

css
transform: translate3d('variable-que-cambia', 0px, 0px);
transition: all 200ms ease 0s;
background: #000;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 2px;

En definitiva necesitamos añadir un elemento div con un css similar a este, y hacerlo sólo cuando tengamos un evento onRouteUpdateDelayed

La manera de hacerlo? Podríamos simplemente añadirlo en nuestro website gatsby

O podríamos hacerlo vía plugin y así hacerlo más reutilizable (tienes la documentación de los plugins aquí)

Me quedo con lo primero, y lo intentaré hacer con un simple hook

Construyendo un hook

Qué quiero que haga el hook?

Quiero utilizarlo en mi gatsby-browser, y que se encargue de todo

Empiezo creando un archivo al que llamo useProgress.js

Y tiene que hacer las siguientes cosas:

  • Devolver un objeto con las funciones start() y done()
  • Añadir un elemento div que le pasaré como argumento

Y para usarlo, algo así

js
const ProgressBar = styled.div`
transition: all 200ms ease 0s;
background: #000;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 2px;
`
const progress = useProgress(progressBar)
progress.start()
progress.done()

Con lo cual el esqueleto del hook sería algo así

js
export const useProgress = element => {
const start = () => {}
const done = () => {}
return {
start,
done,
}
}

Lo primero entonces es plantearse cómo puedo añadir el element que recibo en el virtual DOM(?)

Con gatsby puedo hacerlo con el ancla onRenderBody, y con el argumento setPostBodyComponents, puedes leerlo aquí

Esta ancla sólo existe en el servidor, esto es en gatsby-ssr.js

Por lo tanto, esto nos invita a utilizar el ' hook' en ese mismo archivo

Algo así

js
// gatsby-ssr.js
export const onRenderBody = ({ setPostBodyComponents }) => {
const progress = useProgress(progressBar)
setPostBodyComponents([progress.element()])
}

Y la manera de cambiar ese elemento será con un useRef

En nprogress utilizan la propiedad css transform y translate3d (documentación), que nos permite modificar la posición de un elemento en un espacio tridimensional

css
transform: translate3d('variable-que-cambia', 0px, 0px);

Pero para qué queremos 3D cuando aquí simplemente se trata de una barra que se va llenando de color?

En su lugar, vamos a cambiar simplemente el width de la barra, y listos

Eso sí, voy a añadirle un componente css base al hook para que podamos tener algo por defecto y simplificar el uso del mismo

Añadiendo el elemento

Vamos a probar a ver si podemos añadir el elemento con éxito

js
// useProgress.js
import React, { useRef } from 'react'
import styled from '@emotion/styled'
const ProgressBar = styled.div`
transition: all 200ms ease 0s;
background: #000;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 2px;
& > div {
width: ${props => props.width}px;
}
`
export const useProgress = props => {
const Bar = props => {
const barRef = useRef()
return (
<ProgressBar {...props} ref={barRef}>
<div></div>
</ProgressBar>
)
}
const start = () => {}
const done = () => {}
const element = () => <Bar {...props} />
return {
start,
done,
element,
}
}
js
// gatsby-ssr.js
import { useProgress } from './src/components/useProgress'
export const onRenderBody = ({ setPostBodyComponents }) => {
const progress = useProgress()
setPostBodyComponents([progress.element()])
}

Y vemos que sí, que así estamos añadiendo el componente <Bar /> al documento

Ahora lo que queremos es modificar este componente cada vez que cambiemos de página, algo que hacemos como se hace con el plugin de gatsby que he comentado antes

js
// gatsby-ssr.js
import { useProgress } from './src/components/useProgress'
export const onRenderBody = ({ setPostBodyComponents }) => {
const progress = useProgress()
setPostBodyComponents([progress.element()])
}
js
// gatsby-browser.js
export const onRouteUpdateDelayed = () => progress.start()
export const onRouteUpdate = () => progress.done()

Pero esto no funcionará

El problema? que el objeto progress no está (evidentemente) accesible

Cómo podemos hacerlo?

Lo suyo es, en lugar de utilizar onRenderBody, utilizar wrapRootElement, una ancla que está en gatsby-ssr y también en gatsby-browser

Este hook, el wrapRootElement nos permite añadir componentes que no se re-escribirán cuando el usuario cambie de ruta

Esto es perfecto por ejemplo para añadir lógicas de autentificación, o dicho de otro modo, viene a ser una suerte de useEffect en todo nuestro website

Todo lo que esté en wrapRootElement se ejecutará sólo una vez y quedará persistente en la aplicación, a no ser que se haga un refresco con el navegador

Pues vamos allá

js
// gatsby-ssr.js y gatsby-browser.js
import { useProgress } from './src/components/useProgress'
const progress = useProgress()
export const wrapRootElement = ({ element }) => (
<>
{element}
{progress.element()}
</>
)
export const onRouteUpdateDelayed = () => progress.start()
export const onRouteUpdate = () => progress.done()

Y para que esté disponible he movido progress fuera del componente

Y ahora sólo nos falta añadir la lógica de la estética

Pero esto no funciona

Por qué no?

Perque cuando hago const progress = useProgress()

Pienso que estoy utilizando un custom hook (lógico)

Pero si quiero utilizar un useState, entonces veo que nos salta el error de que estamos violando las normas de los hooks

Es decir, si queremos utilizar un hook, tenemos que hacerlo dentro de un componente React, sea un hook oficial o un custom hook

Esto es algo que no podemos hacer en gatsby-browser.js, por lo que necesitamos repensarlo todo

  • Estamos añadiendo una barra siempre con wrapRootElement (podríamos hacerlo sólo cuando tengamos un evento onRouteUpdateDelayed
  • Estamos queriendo eliminar la barra cuando tengamos el evento onRouteUpdate
  • Pero al no poder definir estados ni hooks en gatsby-browser.js, cómo podemos comunicarnos entre los dos eventos?

La solución es volver a los clásicos, al DOM original, algo que por otro lado también es lo que hace nprogress

Modificando el DOM

  • Primero, añadir el elemento con wrapRootElement
  • Segundo, modificarlo con onRouteUpdateDelayed
  • Tercero, terminarlo de modificar con onRouteUpdate

Vamos allá, y le cambio el nombre de useProgress ya que ya no es un hook, aunque en espíritu lo sigue siendo

js
// DOMProgress.js
import React from 'react'
import styled from '@emotion/styled'
export const progress = {
element: props => (
<Bar id="kuworking_nprogress" {...props}>
<div></div>
</Bar>
),
start: () => (document.getElementById('kuworking_nprogress').firstElementChild.style.width = '50px'),
done: () => (document.getElementById('kuworking_nprogress').firstElementChild.style.width = '100%'),
}
const Bar = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
& > div {
transition: all 200ms ease 0s;
width: 200px;
height: 2px;
background: #000;
}
`
js
// gatsby-browser.js
import React from 'react'
import { progress } from './src/components/DOMProgress'
export const wrapRootElement = ({ element }) => {
return (
<>
{element}
{progress.element()}
</>
)
}
export const onRouteUpdateDelayed = () => progress.start()
//export const onRouteUpdate = () => progress.done()
export const onRouteUpdate = () => {
const wait = ms => new Promise((r, j) => setTimeout(r, ms))
;(async () => {
progress.start()
await wait(4000)
progress.done()
})()
}

Fíjate que he incluido una rutina en onRouteUpdate para poder simular cierto delay de 4 segundos y así poder visualizar mejor lo que hacemos

Bien, pues con esto ya lo tenemos, modificamos el width de la barra una vez accedemos a ella con el clásico getElementById, le modificamos el estilo, y listos

Ahora nos queda pulirlo

Afinando la estética

Necesitamos dos cosas

  • Que haya un incremento del width
  • Que la barra desaparezca después de cargarse la página

Lo conseguimos así

js
// DOMProgress.js
import React from 'react'
import styled from '@emotion/styled'
const percentage = 10
const ref = { value: '' }
export const progress = {
element: props => (
<Bar id="kuworking_nprogress" {...props}>
<div style={{ width: '0%' }}></div>
</Bar>
),
start: () => {
const increasing = () => {
const temp_width =
parseInt(document.getElementById('kuworking_nprogress').firstElementChild.style.width || 0) + percentage
const width = temp_width > 100 ? 100 : temp_width
document.getElementById('kuworking_nprogress').firstElementChild.style.width = width + '%'
}
ref.value = setInterval(increasing, 200)
},
done: () => {
clearInterval(ref.value)
document.getElementById('kuworking_nprogress').firstElementChild.style.width = '100%'
document.getElementById('kuworking_nprogress').firstElementChild.style.opacity = '0'
},
}
const Bar = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
& > div {
transition: all 1000ms ease 0s;
height: 2px;
background: #000;
}
`

Aquí utilizamos setInterval como funciona clásicamente

Si esto fuera React (si lo estuviéramos ahora estaría utilizando el useState) el uso de setInterval tiene su miga, si te interesa aquí lo tienes muy bien explicado

Ahora lo único que nos queda es darle ese matiz random ya que de momento la progresión es totalmente simétrica

Pero yo en lugar de darle ese efecto trompicones, prefiero jugar con las inercias para tener un movimiento más dinámico

Al final, mi código queda como ves

Aquí para conseguir que el navegador se "entere" de los cambios tengo que jugar con paradas en la ejecución obligatorias

Esto es así ya que sino, lo que hacen los navegadores es agrupar instrucciones y ejecutarlas todas de golpe. Esto lo evito "esperando" con el await wait

Y para sacar las curvas cubic-bezier he utilizado herramientas como ésta o con el mismo developer tools de Chrome

js
// DOMProgress.js
import React from 'react'
import styled from '@emotion/styled'
const wait = ms => new Promise((r, j) => setTimeout(r, ms))
export const progress = {
element: props => (
<Bar id="kuworking_nprogress" {...props}>
<div></div>
</Bar>
),
start: async () => {
const st = document.getElementById('kuworking_nprogress').firstElementChild.style
st.transition = 'none' // reset para que las posiciones retornen al estado original de inmediato
await wait(100)
st.width = '0%'
st.opacity = '1'
await wait(100)
st.transition = 'width 10000ms cubic-bezier(0.010, 0.880, 0.000, 1.005)'
await wait(100)
st.width = '60%' // se mueve lentamente
},
done: async () => {
const st = document.getElementById('kuworking_nprogress').firstElementChild.style
st.width = parseInt(st.width) + 20 + '%' // necesito cambiarle el width para que se cambie la velocidad, y aprovecho para dar un brinco
st.transition = 'width 500ms cubic-bezier(1.000, -0.030, 0.990, 0.045), opacity 2000ms ease'
await wait(100)
st.width = '100%'
st.opacity = '0'
},
}
const Bar = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
& > div {
height: 2px;
background: #000;
}
`
js
// gatsby-ssr.js y gatsby-browser.js
import React from 'react'
import { progress } from './src/components/DOMProgress'
export const wrapRootElement = ({ element }) => {
return (
<>
{element}
{progress.element()}
</>
)
}
export const onRouteUpdateDelayed = () => progress.start()
export const onRouteUpdate = () => progress.done()

Y para terminar, decido que también quiero que haya un efecto visual cuando se cambia de página (no sólo cuando haya una demora)

js
import React from 'react'
import styled from '@emotion/styled'
const wait = ms => new Promise((r, j) => setTimeout(r, ms))
const reset = async st => {
st.transition = 'none'
await wait(100)
st.width = '0%'
st.opacity = '1'
await wait(100)
}
const increment = (st, i) => {
const width = parseInt(st.width) + i
st.width = (width > 100 ? 100 : width) + '%'
}
const transition_slow = async st => {
st.transition = 'width 10000ms cubic-bezier(0.010, 0.880, 0.000, 1.005)'
await wait(100)
}
const transition_fast = async st => {
st.transition = 'width 500ms cubic-bezier(1.000, -0.030, 0.990, 0.045), opacity 2000ms ease'
await wait(100)
}
export const progress = {
element: props => (
<Bar id="kuworking_nprogress" {...props}>
<div></div>
</Bar>
),
start: async () => {
const st = document.getElementById('kuworking_nprogress').firstElementChild.style
await reset(st)
await transition_slow(st)
st.width = '60%'
},
done: async () => {
const st = document.getElementById('kuworking_nprogress').firstElementChild.style
await reset(st)
increment(st, 20)
await transition_fast(st)
st.width = '100%'
st.opacity = '0'
},
}
const Bar = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
& > div {
height: 2px;
background: #000;
}
`

Y listos

🙋‍♂️

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 dame las gracias por ejemplo por twitter con este enlace 👍

Privacidad
by kuworking.com
[ 2020 >> kuworking ]