useWindowSize — Rastrea las dimensiones de la ventana de forma reactiva con actualizaciones debounce
Receta
import { useState, useEffect, useCallback, useRef } from "react";
interface WindowSize {
width: number;
height: number;
}
interface UseWindowSizeOptions {
/** Retraso de debounce en ms. Predeterminado: 100 */
debounceDelay?: number;
/** Tamaño inicial para SSR. Predeterminado: { width: 0, height: 0 } */
initialSize?: WindowSize;
}
function useWindowSize(options: UseWindowSizeOptions = {}): WindowSize {
const {
debounceDelay = 100,
initialSize = { width: 0, height: 0 },
} = options;
const [size, setSize] = useState<WindowSize>(() => {
if (typeof window === "undefined") return initialSize;
return {
width: window.innerWidth,
height: window.innerHeight,
};
});
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
if (typeof window === "undefined") return;
const handleResize = () => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
timerRef.current = null;
}, debounceDelay);
};
// Establecer tamaño inicial al montarse
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [debounceDelay]);
return size;
}Cuándo usarlo: Necesitas dimensiones de ventana en JavaScript para cálculos de diseño responsivo, dimensionamiento de canvas, listas virtualizadas o renderizado condicional que las media queries de CSS no pueden manejar.
Ejemplo práctico
"use client";
function ResponsiveLayout() {
const { width, height } = useWindowSize();
const columns = width >= 1024 ? 3 : width >= 640 ? 2 : 1;
return (
<div>
<p>
Ventana: {width} x {height}
</p>
<div
style={{
display: "grid",
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: 16,
}}
>
{Array.from({ length: 6 }, (_, i) => (
<div
key={i}
style={{
padding: 24,
background: "#f5f5f5",
borderRadius: 8,
textAlign: "center",
}}
>
Tarjeta {i + 1}
</div>
))}
</div>
</div>
);
}
function CanvasSizer() {
const { width } = useWindowSize({ debounceDelay: 200 });
const canvasWidth = Math.min(width - 32, 800);
const canvasHeight = canvasWidth * 0.5625; // 16:9
return (
<canvas
width={canvasWidth}
height={canvasHeight}
style={{ border: "1px solid #ccc" }}
/>
);
}Qué demuestra esto:
- El número de columnas de la cuadrícula se adapta según los puntos de corte del ancho de la ventana
- El canvas mantiene una relación de aspecto 16:9 relativa al ancho de la ventana
- El debounce evita re-renderizados excesivos durante el arrastre de cambio de tamaño
- Retraso de debounce personalizado para el componente canvas (200 ms)
Profundización
Cómo funciona
- Inicializador perezoso:
useStateleewindow.innerWidthywindow.innerHeightsolo en el cliente, retrocediendo ainitialSizedurante SSR. - Manejador de cambio de tamaño debounceado: Un debounce basado en
setTimeoutcolapsa eventos de cambio de tamaño rápidos en una única actualización de estado, previniendo jank. - Limpieza: El listener de evento y cualquier temporizador pendiente se limpian al desmontarse.
- Re-sincronización al montarse: El efecto establece inmediatamente el tamaño actual para manejar casos donde la ventana fue redimensionada mientras el componente estaba desmontado.
Parámetros y valores de retorno
| Opción | Tipo | Predeterminado | Descripción |
|---|---|---|---|
debounceDelay | number | 100 | Milisegundos para debounce de eventos de cambio de tamaño |
initialSize | { width, height } | { width: 0, height: 0 } | Tamaño retornado durante SSR |
| Retorno | Tipo | Descripción |
|---|---|---|
width | number | window.innerWidth actual |
height | number | window.innerHeight actual |
Variaciones
Sin debounce: Para actualizaciones inmediatas (p. ej., vistas previas de cambio de tamaño por arrastre), establece debounceDelay: 0 o elimina setTimeout:
const size = useWindowSize({ debounceDelay: 0 });Con orientación: Detecta paisaje vs. retrato:
function useOrientation() {
const { width, height } = useWindowSize();
return width > height ? "landscape" : "portrait";
}Tamaño del documento (altura de desplazamiento): Rastrea la altura total del documento en lugar del viewport:
// Dentro del manejador de cambio de tamaño:
setSize({
width: document.documentElement.scrollWidth,
height: document.documentElement.scrollHeight,
});Notas de TypeScript
- La interfaz
WindowSizese exporta para que los consumidores puedan escribir su propio estado o props. - No se necesitan genéricos; el tipo de retorno es siempre
{ width: number; height: number }. - La interfaz de opciones utiliza propiedades opcionales con valores predeterminados desestructurados en el cuerpo de la función.
Trampas
- Desincronización de hidratación de SSR — El servidor renderiza con
initialSize(0x0), pero el cliente se actualiza inmediatamente a dimensiones reales. Solución: Esto causa un cambio de diseño en la primera renderización. Para diseños críticos, prefiere media queries de CSS o proporciona una estimación razonable deinitialSize. - Actualizaciones demasiado frecuentes — Sin debounce, cada píxel de un arrastre de cambio de tamaño dispara una re-renderización. Solución: El debounce incorporado maneja esto; ajusta
debounceDelaysegún sea necesario. - Teclado virtual en móvil — En móvil, abrir el teclado redimensiona el viewport y dispara el manejador. Solución: Usa la API
window.visualViewportsi necesitas distinguir el teclado del cambio de tamaño. - Contexto de iframe — Dentro de un iframe,
window.innerWidthrefleja el tamaño del iframe, no de la ventana padre. Solución: Usaparent.windowsi la política entre orígenes lo permite, o pasa el tamaño como prop.
Alternativas
| Paquete | Nombre del Hook | Notas |
|---|---|---|
usehooks-ts | useWindowSize | Sin debounce incorporado |
@uidotdev/usehooks | useWindowSize | Implementación mínima |
ahooks | useSize | Rastrea cualquier elemento, no solo la ventana |
react-use | useWindowSize | Incluye valores predeterminados del lado del servidor |
@react-hook/window-size | useWindowSize | Variante limitada disponible |
Preguntas frecuentes
¿Por qué el hook debounce eventos de cambio de tamaño por defecto?
Sin debounce, cada píxel de un arrastre de cambio de tamaño dispara una actualización de estado y re-renderización. El debounce incorporado colapsa eventos rápidos en una única actualización, previniendo jank y re-renderizaciones innecesarias.
¿Cómo desactivo el debounce para actualizaciones inmediatas?
Establece debounceDelay a 0:
const size = useWindowSize({ debounceDelay: 0 });Esto aún usa setTimeout(..., 0) que se aplaza al siguiente tick. Para actualizaciones verdaderamente sincrónicas, elimina setTimeout del hook.
¿Qué hace initialSize y cuándo se usa?
initialSizeproporciona las dimensiones retornadas durante SSR (predeterminado:{ width: 0, height: 0 }).- En el cliente, el efecto se actualiza inmediatamente a dimensiones reales.
- Pasa una estimación razonable (p. ej.,
{ width: 1024, height: 768 }) para reducir el cambio de diseño.
¿Por qué el efecto establece el tamaño inmediatamente al montarse además de escuchar cambios de tamaño?
El componente puede haber sido desmontado y remontado mientras la ventana fue redimensionada. El conjunto inmediato asegura que el estado sea correcto incluso si ningún evento de cambio de tamaño se dispara después del montaje.
Trampa: Mi página SSR muestra un cambio de diseño porque el servidor renderiza con ancho 0. ¿Cómo lo arreglo?
- Pasa un
initialSizerealista que coincida con tu viewport más común. - Para diseños críticos, prefiere media queries de CSS que no dependan de JavaScript.
- Usa
useMediaQuerypara verificaciones de punto de corte booleanas que puedan tolerar el breve destello.
Trampa: Abrir el teclado en móvil dispara un evento de cambio de tamaño y re-renderiza mi diseño. ¿Por qué?
En navegadores móviles, el teclado virtual redimensiona el viewport. El hook se dispara en cualquier cambio de tamaño, incluyendo apertura/cierre del teclado. Usa la API window.visualViewport para distinguir eventos del teclado de cambios reales de ventana.
¿Cómo diferencia este hook a useMediaQuery para diseños responsivos?
useWindowSizeretorna valores exactos en píxeles (width,height).useMediaQueryretorna un booleano para un punto de corte específico.- Usa
useWindowSizecuando necesitas cálculos (p. ej., dimensionamiento de canvas, conteos de columnas). - Usa
useMediaQuerycuando solo necesitas un toggle booleano.
¿Puedo rastear la altura total de desplazamiento del documento en lugar del viewport?
Sí. Reemplaza window.innerWidth/Height con document.documentElement.scrollWidth/Height dentro del manejador de cambio de tamaño. Esto da el tamaño total del documento incluyendo overflow.
¿Cuál es el tipo de TypeScript del objeto retornado?
El hook retorna WindowSize, que es { width: number; height: number }. La interfaz se exporta para que los consumidores puedan usarla para sus propios tipos de props o state.
¿Cómo construiría un hook useOrientation encima de useWindowSize?
function useOrientation(): "landscape" | "portrait" {
const { width, height } = useWindowSize();
return width > height ? "landscape" : "portrait";
}Relacionado
- useMediaQuery — verificaciones de punto de corte booleanas
- useDebounce — el patrón de debounce en detalle
- useEventListener — enlace de evento declarativo