Реализация Resize Observer для адаптивных компонентов сайта
ResizeObserver следит за изменением размеров конкретного DOM-элемента. Не viewport, а именно элемента. Это ключевое отличие от CSS media queries — здесь адаптивность привязана к контейнеру компонента, а не к ширине экрана.
Классический случай: компонент графика занимает то всю ширину страницы, то треть. CSS media query этого не видит. ResizeObserver — видит.
Базовое использование
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
// entry.contentRect — размеры контентной области
// entry.borderBoxSize — размеры с учётом border
// entry.contentBoxSize — без padding/border
// entry.devicePixelContentBoxSize — в физических пикселях
const { width, height } = entry.contentRect
console.log(`${entry.target.id}: ${width}x${height}`)
}
})
observer.observe(element)
observer.unobserve(element)
observer.disconnect()
Container Queries через JavaScript до нативной поддержки
Нативные CSS Container Queries (@container) поддерживаются в современных браузерах, но для старых или для сложной логики — ResizeObserver:
function applyContainerBreakpoints(
element: HTMLElement,
breakpoints: Record<number, string>
): () => void {
const sortedBreakpoints = Object.entries(breakpoints)
.map(([w, cls]) => [Number(w), cls] as [number, string])
.sort(([a], [b]) => a - b)
const observer = new ResizeObserver(([entry]) => {
const width = entry.contentRect.width
// Убрать все классы брейкпоинтов
sortedBreakpoints.forEach(([, cls]) => element.classList.remove(cls))
// Добавить подходящий
for (const [minWidth, cls] of sortedBreakpoints) {
if (width >= minWidth) element.classList.add(cls)
}
})
observer.observe(element)
return () => observer.disconnect()
}
// Использование:
applyContainerBreakpoints(cardEl, {
0: 'card--xs',
320: 'card--sm',
480: 'card--md',
640: 'card--lg',
})
Автоматический resize канваса
function makeResponsiveCanvas(
canvas: HTMLCanvasElement,
draw: (ctx: CanvasRenderingContext2D, width: number, height: number) => void
): () => void {
const ctx = canvas.getContext('2d')!
const dpr = window.devicePixelRatio || 1
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect
// Устанавливаем реальный размер в пикселях
canvas.width = Math.round(width * dpr)
canvas.height = Math.round(height * dpr)
// CSS-размер остаётся прежним
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
// Масштабируем контекст для retina
ctx.scale(dpr, dpr)
draw(ctx, width, height)
})
observer.observe(canvas.parentElement ?? canvas)
return () => observer.disconnect()
}
React-хук
function useResizeObserver<T extends HTMLElement = HTMLDivElement>(): [
RefObject<T | null>,
DOMRectReadOnly | null,
] {
const ref = useRef<T>(null)
const [rect, setRect] = useState<DOMRectReadOnly | null>(null)
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new ResizeObserver(([entry]) => {
setRect(entry.contentRect)
})
observer.observe(el)
return () => observer.disconnect()
}, [])
return [ref, rect]
}
// Адаптивный компонент на основе хука:
function AdaptiveChart({ data }: { data: number[] }) {
const [ref, rect] = useResizeObserver<HTMLDivElement>()
const isCompact = (rect?.width ?? 0) < 400
return (
<div ref={ref} style={{ width: '100%' }}>
{isCompact ? (
<CompactChart data={data} width={rect?.width} />
) : (
<FullChart data={data} width={rect?.width} height={rect?.height} />
)}
</div>
)
}
Debounce для частых изменений
ResizeObserver срабатывает при каждом изменении размера — при ресайзе окна это может быть несколько раз в секунду. Для тяжёлых вычислений нужен debounce:
function useResizeObserverDebounced<T extends HTMLElement>(
delay = 150
): [RefObject<T | null>, DOMRectReadOnly | null] {
const ref = useRef<T>(null)
const [rect, setRect] = useState<DOMRectReadOnly | null>(null)
const timerRef = useRef<ReturnType<typeof setTimeout>>()
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new ResizeObserver(([entry]) => {
clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => setRect(entry.contentRect), delay)
})
observer.observe(el)
return () => {
observer.disconnect()
clearTimeout(timerRef.current)
}
}, [delay])
return [ref, rect]
}
Что входит в работу
Настройка ResizeObserver для нужных компонентов, React-хуки с debounce, адаптация компонентов по ширине контейнера (а не viewport), автоматический resize canvas при необходимости.
Срок: 0.5 дня.







