Como hacer responsive design con un custom hook

Hace un tiempo tenia mucha curiosidad de como hacer responsive design en react js, esta claro que se puede con las Medias Queries a nivel de css y pues si, hablo en css, pero que pasá si quiero pintar un componente en la pantalla dependediendo al width del screen, en lo primero que pensarías quizas es en lo siguiente.

jsx
import * as React from 'react'
const MyComponent = () => {
let widthScreen = window.innerWidth
if (widthScreen > 1200) {
return <p>Genial! Estamos en una PC</p>
}
return <p>No estamos en una PC :c</p>
}

Claro que se puede hacer esto y está bien, pero si te dijera que con un custom hook podemos volver todo color de rosa, pues es básicamente la misma idea pero con la lógica encerrada en un custom hook y con algunos parámetros que hacen que se vea más legible y mayor control del state que manejemos.

Creando nuestro custom hook "useResponsive"

Para comenzar, usaremos un event listener llamado resize, asi que si aún no lo conoces te invito a hacerlo, para comenzar vamos a tener unos parámetros (minMediaQuery, maxMediaQuery), estos indicarán el rango (width del screen) que evaluaremos, luego crearemos un state que será boolean, este indicará si estamos dentro del rango para retornar ese valor, entonces se vería algo así.

jsx
import * as React from 'react'
export default function useResponsive(minMediaQuery, maxMediaQuery) {
const [isScreen, setIsScreen] = React.useState(undefined)
}

Tengamos en cuenta que no siempre llegarán ambos parámetros, ya que a veces no queremos un rango sino simplemente un min o un max, esto sería equivalente en css con media queries a esto.

css
@media only screen and (min-width: 600px) {
body {
background-color: lightblue;
}
}

Pues también agregaremos esta funcionalidad, ahora para continuar, crearemos una función que obtenga el width del screen y valide si se encuentra dentro del rango y agregando la funcionalidad de media queries de css, como notarás nuestra función se encuentra envuelto con debounce,

debounce:

básicamente debounce es algo parecido a setTimeout con la diferencia en que si esta función se vuelve a llamar dentro del delay programado, esta no se coloca en la cola de tareas, sino que vuelve a reiniciar el tiempo de delay que le hallamos pasado, entonces cuando el usuario cambie el size del screen, solo se llamará una vez luego del delay programado por nosotros, en este caso es 30 milisegundos.

jsx
import * as React from 'react'
import _debounce from 'lodash/debounce'
export default function useResponsive({ minMediaQuery, maxMediaQuery }) {
const [isScreen, setIsScreen] = React.useState(undefined)
const validateRange = _debounce(() => {
let widthScreen = window.innerWidth
let isRange
//Si solo hay minMediaQuery, solo evaluamos si es mayor a esa media query
if (minMediaQuery && !maxMediaQuery) {
isRange = widthScreen > minMediaQuery
}
//Si solo hay maxMediaQuery, solo evaluamos si es menor a esa media query
if (!minMediaQuery && maxMediaQuery) {
isRange = widthScreen < maxMediaQuery
}
//Si hay minMediaQuery y maxMediaQuery, evaluamos si esta entre el rango de las media query
if (minMediaQuery && maxMediaQuery) {
isRange = widthScreen > minMediaQuery && widthScreen < maxMediaQuery
}
//por último cambiamos el valor solo si es diferente
isScreen !== isRange && setIsScreen(isRange)
}, 30)
}

Una vez creado nuestra función, tenemos que ponerla a funcionar, vamos a crear un useEffect en donde se ejecutará sola una primera vez para asignar el valor a isScreen, luego en otro useEffect lo agregamos al objeto global window como un event listener.

jsx
import * as React from 'react'
import _debounce from 'lodash/debounce'
export default function useResponsive({ minMediaQuery, maxMediaQuery }) {
const [isScreen, setIsScreen] = React.useState(undefined)
//ejecutamos nuestra función de validación solo la primera vez
React.useEffect(() => {
validateScreen()
}, [])
React.useEffect(() => {
//hacemos que se ejecute cada vez que el `size` de la pantalla cambie
window.addEventListener('resize', validateDimensions)
//por ultimo cuando nuestro componente que use este hook
//se desmonte, eliminaremos el event listener
return () => {
window.removeEventListener('resize', validateDimensions)
}
}, [isScreen])
const validateScreen = _debounce(() => {
let widthScreen = window.innerWidth
let isRange
if (minMediaQuery && !maxMediaQuery) {
isRange = widthScreen > minMediaQuery
}
if (!minMediaQuery && maxMediaQuery) {
isRange = widthScreen < maxMediaQuery
}
if (minMediaQuery && maxMediaQuery) {
isRange = widthScreen > minMediaQuery && widthScreen < maxMediaQuery
}
isScreen !== isRange && setIsScreen(isRange)
}, 30)
return isScreen
}

Nota: Como verás cuando cambiamos el valor de nuestro state, solo lo hacemos cuando este sea diferente al state actual, ya que quizás el width del screen puede haber cambiado 1 pixel y eso mandará un re render de nuestro componente, entonces

¿Que sucede si se actualiza varias veces nuestro state con el mismo value?,

Cuando un state se actualiza con su mismo valor por tercera vez consecutiva, este no se vuelve a renderizar el componente, esto es muy bueno, ya que si usamos un console.log para ver valor del state en la consola veremos que se muestra 2 veces, con esto podriamos notar que react está impidiendo que nuestro componente se rendericé infinitamente y ver claramente nuestro error.

Ahora podrás usarlo en tus componentes de la siguiente manera:

jsx
import useResponsive from './hook/useResponsive'
const MyComponent = () => {
//le pasamos el rango a evaluar, recuerda que es posible no pasarle uno de ellos,
//pero no es posible no pasar ningún parámetro
const isMobile = useResponsive({ maxMediaQuery: 640 })
return <div>{isMobile && <p>Genial :D, Estamos en un Mobile</p>}</div>
}

Añadiendo Typescript

Ahora pasaremos a un siguiente nivel y lo haremos con typescript, si aún no usas typescript, pues no te preocupes, ya entendiste la idea principal de nuestro custom hook.

jsx
import { useState, useEffect } from 'react'
//las queries recibidas solo deberán aceptar estos valores
type MediaQuery = 640 | 768 | 1024 | 1280 | 1536
interface Parameters {
minMediaQuery?: MediaQuery
maxMediaQuery?: MediaQuery
}
const useResponsive = ({ minMediaQuery, maxMediaQuery }: Parameters) => {
const [isScreen, setIsScreen] = useState<undefined | boolean>(undefined)
useEffect(() => {
validateScreen()
}, [])
useEffect(() => {
window.addEventListener('resize', validateScreen)
return () => {
window.removeEventListener('resize', validateScreen)
}
}, [isScreen])
const validateScreen = _debounce(() => {
let widthScreen = window.innerWidth
let screen
if (minMediaQuery && !maxMediaQuery) {
screen = widthScreen > minMediaQuery
}
if (maxMediaQuery && !minMediaQuery) {
screen = widthScreen < maxMediaQuery
}
if (minMediaQuery && maxMediaQuery) {
screen = widthScreen > minMediaQuery && widthScreen < maxMediaQuery
}
isScreen !== screen && setIsScreen(screen)
}, 30)
return isScreen
}
export default useResponsive

Conclusión

Como vez, es muy fácil implementar este custom hook para realizar responsive design en nuestra web y quizás esta no sea la mejor opción, podrás encontrar varias soluciones en npm, pero la idea de este blog es ayudar que tu imaginación crezca en cuanto a crear tus propias funcionalidades para tu web, recuerda que así nacen las grandes librerías, espero haberte ayudado.

¡Gracias por leer!