useMediaQuery — Suscribirse a coincidencias de consultas de medios CSS en JavaScript
Recipe
import { useState, useEffect, useCallback } from "react";
/**
* useMediaQuery
* Devuelve `true` cuando la consulta de medios CSS dada coincide.
* Seguro para SSR: devuelve `defaultValue` en el servidor.
*/
function useMediaQuery(
query: string,
defaultValue: boolean = false
): boolean {
const [matches, setMatches] = useState<boolean>(() => {
if (typeof window === "undefined") return defaultValue;
return window.matchMedia(query).matches;
});
useEffect(() => {
if (typeof window === "undefined") return;
const mql = window.matchMedia(query);
setMatches(mql.matches);
const handler = (e: MediaQueryListEvent) => {
setMatches(e.matches);
};
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, [query]);
return matches;
}
/**
* Hooks de conveniencia construidos sobre useMediaQuery
*/
function useIsMobile(breakpoint: number = 768): boolean {
return useMediaQuery(`(max-width: ${breakpoint - 1}px)`);
}
function useIsDesktop(breakpoint: number = 1024): boolean {
return useMediaQuery(`(min-width: ${breakpoint}px)`);
}
function usePrefersDarkMode(): boolean {
return useMediaQuery("(prefers-color-scheme: dark)");
}
function usePrefersReducedMotion(): boolean {
return useMediaQuery("(prefers-reduced-motion: reduce)");
}Cuándo usarlo: Necesitas comportamiento sensible en JavaScript que solo CSS no puede manejar, como renderizar componentes condicionalmente, cargar datos diferentes o ajustar parámetros de hooks basados en el tamaño de pantalla.
Ejemplo de Uso
"use client";
function ResponsiveNav() {
const isMobile = useIsMobile();
const prefersDark = usePrefersDarkMode();
const reducedMotion = usePrefersReducedMotion();
return (
<nav
style={{
background: prefersDark ? "#1a1a2e" : "#ffffff",
transition: reducedMotion ? "none" : "background 0.3s",
}}
>
{isMobile ? <HamburgerMenu /> : <DesktopMenu />}
</nav>
);
}
function HamburgerMenu() {
return <button aria-label="Menu">☰</button>;
}
function DesktopMenu() {
return (
<ul style={{ display: "flex", gap: 16, listStyle: "none" }}>
<li>Home</li>
<li>About</li>
<li>Contact</li>
</ul>
);
}
function AdaptiveGrid() {
const isDesktop = useIsDesktop();
const columns = isDesktop ? 3 : 1;
return (
<div
style={{
display: "grid",
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: 16,
}}
>
<div>Card 1</div>
<div>Card 2</div>
<div>Card 3</div>
</div>
);
}Lo que esto demuestra:
useIsMobilerenderiza condicionalmente un menú de hamburguesa versus navegación de escritoriousePrefersDarkModeaplica un tema sin alternancia de clases CSSusePrefersReducedMotiondesactiva transiciones CSS para usuarios que prefieren movimiento reducido- El layout de grid adapta el número de columnas basado en el ancho de la ventana
- Todos los hooks reaccionan en tiempo real a cambios de tamaño de ventana y cambios de configuración del sistema
Profundización
Cómo Funciona
window.matchMediacrea un objetoMediaQueryListque evalúa una cadena de consulta de medios CSS.- El evento
changese dispara cada vez que el estado de coincidencia cambia (por ejemplo, cuando la ventana cruza un breakpoint), disparando una actualización de estado. - Seguridad para SSR: El inicializador lazy devuelve
defaultValuecuandowindowno está disponible. El efecto solo se ejecuta en el cliente. - Los hooks de conveniencia son envoltorios delgados que pasan strings de consulta pregenerados, manteniendo el código del componente limpio.
Parámetros y Valores de Retorno
| Parameter | Type | Default | Description |
|---|---|---|---|
query | string | — | Cualquier cadena de consulta de medios CSS válida |
defaultValue | boolean | false | Devuelto durante SSR o cuando matchMedia no está disponible |
| Returns | boolean | — | Si la consulta actualmente coincide |
Variaciones
Múltiples consultas: Para lógica sensible compleja, llama el hook múltiples veces:
const isSm = useMediaQuery("(min-width: 640px)");
const isMd = useMediaQuery("(min-width: 768px)");
const isLg = useMediaQuery("(min-width: 1024px)");Objeto de breakpoint: Devuelve un breakpoint nombrado para uso estilo Tailwind:
function useBreakpoint() {
const isSm = useMediaQuery("(min-width: 640px)");
const isMd = useMediaQuery("(min-width: 768px)");
const isLg = useMediaQuery("(min-width: 1024px)");
const isXl = useMediaQuery("(min-width: 1280px)");
if (isXl) return "xl";
if (isLg) return "lg";
if (isMd) return "md";
if (isSm) return "sm";
return "xs";
}Notas de TypeScript
- El tipo de retorno es un simple
boolean, así que no se necesitan genéricos. - Los hooks de conveniencia tienen tipos de retorno fijos; simplemente restringen el uso.
- Para la variante de breakpoint, usa un tipo de retorno union:
"xs" | "sm" | "md" | "lg" | "xl".
Trampas
- Desincronización de hidratación — El servidor renderiza con
defaultValue, pero el cliente puede evaluar a un valor diferente. Esto puede causar un parpadeo. Solución: Acepta el breve parpadeo, o usa diseño sensible basado en CSS para elementos críticos de layout y reservauseMediaQuerypara lógica no visual. - Demasiados listeners — Cada llamada a
useMediaQuerycrea un listenermatchMediaseparado. Solución: Esto está bien para un puñado de breakpoints. Si tienes docenas, considera un único listener con múltiples breakpoints. - Cadena de consulta inválida —
matchMediano lanza error para consultas inválidas; devuelve unMediaQueryListque nunca coincide. Solución: Valida consultas durante el desarrollo. - Peculiaridades de iOS Safari — Versiones más antiguas de Safari usan
addListener/removeListeneren lugar deaddEventListener. Solución: Safari moderno soporta la API estándar. Para soporte heredado, añade un fallback.
Alternativas
| Package | Hook Name | Notes |
|---|---|---|
usehooks-ts | useMediaQuery | API similar, bien probado |
@uidotdev/usehooks | useMediaQuery | Implementación mínima |
ahooks | useResponsive | Devuelve objeto de breakpoint |
react-responsive | useMediaQuery | Soporta renderizado del lado del servidor con hints |
| Tailwind CSS | Responsive classes | Solo CSS, sin JS necesario |
FAQs
¿Qué devuelve window.matchMedia y cómo lo usa el hook?
window.matchMedia(query)devuelve un objetoMediaQueryList.- El objeto tiene un booleano
matchesy dispara un eventochangecuando el estado de coincidencia cambia. - El hook lee
matchesal montarse y se suscribe achangepara actualizaciones en vivo.
¿Cuándo debería usar useMediaQuery en lugar de consultas de medios CSS?
- Cuando necesitas renderizar componentes completamente diferentes condicionalmente (no solo cambios de estilo).
- Cuando necesitas ajustar parámetros de hooks u obtener datos diferentes basados en el tamaño de pantalla.
- Para cambios de estilos puros, las consultas de medios CSS son más rápidas y evitan desincronizaciones de hidratación.
¿Cómo funcionan los hooks de conveniencia (useIsMobile, usePrefersDarkMode, etc.)?
Son envoltorios delgados que llaman useMediaQuery con una cadena de consulta pregenerada. Por ejemplo, useIsMobile(768) llama useMediaQuery("(max-width: 767px)"). No se añade lógica extra.
¿Por qué el breakpoint en useIsMobile se establece a breakpoint - 1?
max-width: 767px se dirige a pantallas más estrechas que 768px. Usar breakpoint - 1 asegura que exactamente 768px de ancho no se considere móvil, coincidiendo con convenciones comunes de breakpoint CSS.
Trampa: Mi layout parpadea al cargar inicialmente porque el servidor renderiza el breakpoint incorrecto. ¿Cómo lo corrijo?
- El servidor usa
defaultValue(false), pero el cliente puede evaluar diferentemente. - Acepta el breve parpadeo, o usa diseño sensible basado en CSS para elementos críticos de layout.
- Reserva
useMediaQuerypara lógica no visual como obtención de datos o alternadores de características.
Trampa: Pasé una cadena de consulta de medios inválida y el hook nunca devuelve true. ¿Por qué sin error?
window.matchMedia no lanza error para consultas inválidas. Devuelve un MediaQueryList que nunca coincide. No hay validación a nivel de navegador. Verifica tus cadenas de consulta durante el desarrollo.
¿Cada llamada a useMediaQuery crea un listener separado? ¿Es eso caro?
Sí, cada llamada crea su propio listener matchMedia. Para un puñado de breakpoints esto está bien. Si tienes docenas, consolida en un único hook useBreakpoint que devuelva una cadena de nombre de breakpoint.
¿Cómo escribiría un hook useBreakpoint que devuelve una union de nombres de breakpoint?
function useBreakpoint(): "xs" | "sm" | "md" | "lg" | "xl" {
const isSm = useMediaQuery("(min-width: 640px)");
const isMd = useMediaQuery("(min-width: 768px)");
const isLg = useMediaQuery("(min-width: 1024px)");
const isXl = useMediaQuery("(min-width: 1280px)");
if (isXl) return "xl";
if (isLg) return "lg";
if (isMd) return "md";
if (isSm) return "sm";
return "xs";
}¿Por qué el hook acepta un parámetro defaultValue?
Durante SSR, window.matchMedia no está disponible. defaultValue proporciona un fallback seguro para que el hook devuelva un booleano predecible en el servidor. Por defecto es false.
¿Puedo detectar preferencias del usuario como movimiento reducido con este hook?
Sí. Usa el hook de conveniencia usePrefersReducedMotion o pasa la consulta directamente:
const reducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");Relacionado
- useWindowSize — rastrear dimensiones exactas en píxeles
- useLocalStorage — persistir preferencia de tema
- useEventListener — el patrón de listener subyacente