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.
jsximport * as React from 'react'const MyComponent = () => {let widthScreen = window.innerWidthif (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.
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í.
jsximport * 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,
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.
jsximport * 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.innerWidthlet isRange//Si solo hay minMediaQuery, solo evaluamos si es mayor a esa media queryif (minMediaQuery && !maxMediaQuery) {isRange = widthScreen > minMediaQuery}//Si solo hay maxMediaQuery, solo evaluamos si es menor a esa media queryif (!minMediaQuery && maxMediaQuery) {isRange = widthScreen < maxMediaQuery}//Si hay minMediaQuery y maxMediaQuery, evaluamos si esta entre el rango de las media queryif (minMediaQuery && maxMediaQuery) {isRange = widthScreen > minMediaQuery && widthScreen < maxMediaQuery}//por último cambiamos el valor solo si es diferenteisScreen !== 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.
jsximport * 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 vezReact.useEffect(() => {validateScreen()}, [])React.useEffect(() => {//hacemos que se ejecute cada vez que el `size` de la pantalla cambiewindow.addEventListener('resize', validateDimensions)//por ultimo cuando nuestro componente que use este hook//se desmonte, eliminaremos el event listenerreturn () => {window.removeEventListener('resize', validateDimensions)}}, [isScreen])const validateScreen = _debounce(() => {let widthScreen = window.innerWidthlet isRangeif (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 mismovalue
?,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 delstate
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:
jsximport 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ámetroconst isMobile = useResponsive({ maxMediaQuery: 640 })return <div>{isMobile && <p>Genial :D, Estamos en un Mobile</p>}</div>}
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.
jsximport { useState, useEffect } from 'react'//las queries recibidas solo deberán aceptar estos valorestype MediaQuery = 640 | 768 | 1024 | 1280 | 1536interface Parameters {minMediaQuery?: MediaQuerymaxMediaQuery?: 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.innerWidthlet screenif (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
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!