Using ResizeObserver in React for Responsive Designs

Build truly adaptive React components that respond to container dimensions, not just viewport breakpoints

What Is ResizeObserver and Why It Matters

ResizeObserver represents a fundamental shift in how we approach responsive design in web applications. Unlike traditional methods that rely on viewport dimensions or media queries, ResizeObserver allows components to observe their own container dimensions and respond dynamically to any size changes. This capability proves essential for building sophisticated layouts where components need to adapt based on available space rather than predetermined breakpoints.

Before ResizeObserver, developers had limited options for detecting element size changes. The window resize event could notify when the entire viewport changed, but it provided no information about individual container dimensions. Adding complexity, elements could resize for reasons unrelated to viewport changes, such as dynamic content loading, collapsible sections, or sibling element modifications. Developers resorted to workarounds like polling or complicated event chain tracking, which proved error-prone and inefficient.

ResizeObserver solves these challenges by providing a dedicated API for observing element dimension changes. When you attach a ResizeObserver to an element, the browser notifies your code whenever that element's size changes, regardless of the cause. This notification includes detailed information about the new dimensions, enabling precise responsive behavior.

For React applications specifically, ResizeObserver enables a new category of responsive components. Consider a data visualization component that needs to redraw charts based on available width, a typography component that scales font sizes proportionally to container size, or a card grid that adjusts column counts based on parent width. These scenarios, previously difficult to implement cleanly, become straightforward with ResizeObserver's direct element observation approach. For additional techniques on creating dynamic, adaptable interfaces, explore our guide on using custom properties to leverage CSS variables in your responsive designs.

The ResizeObserver API

Understanding the ResizeObserver API requires familiarity with its core components: the observer itself, the entries it provides, and the methods for managing observation lifecycle. The API follows a pattern consistent with other modern JavaScript observer APIs, making it intuitive for developers familiar with IntersectionObserver or MutationObserver.

Creating and Configuring an Observer

The ResizeObserver constructor accepts a callback function that fires whenever any observed element changes size. This callback receives an array of ResizeObserverEntry objects, one for each element that changed, allowing efficient batch processing when multiple elements are observed simultaneously.

const observer = new ResizeObserver((entries) => {
 for (const entry of entries) {
 // Handle size change for entry.target
 }
});

The callback executes in a context where it has access to all changed entries, enabling you to process multiple dimension changes in a single operation. This batching behavior improves performance by minimizing the number of callback invocations during complex layout changes.

ResizeObserverEntry Properties

Each ResizeObserverEntry provides several properties describing the element's new dimensions. The contentRect property returns a DOMRectReadOnly object containing the element's content rectangle dimensions, including x, y, width, height, top, right, bottom, and left values. This rectangle represents the content area inside the element's padding box, providing the most commonly needed dimension information.

For more detailed size information, three additional properties provide specialized measurements:

  • contentRect: The content rectangle dimensions (x, y, width, height, top, right, bottom, left)
  • borderBoxSize: Array of ResizeObserverSize objects for border box dimensions
  • contentBoxSize: Array of ResizeObserverSize objects for content box dimensions
  • devicePixelContentBoxSize: Dimensions in device pixel units for high-DPI displays

These size properties return arrays because the specification anticipates future support for multi-column layouts where elements may have multiple fragment boxes. Currently, these arrays contain a single element, but writing code that handles arrays ensures forward compatibility.

Managing Observation Lifecycle

Three methods control the ResizeObserver's observation lifecycle:

MethodPurpose
observe(element)Begins observing a specified element
unobserve(element)Stops observing a specific element
disconnect()Removes all observations and stops the observer

The observe() method immediately triggers the callback with the element's current dimensions, allowing immediate initialization of responsive behavior. You can observe multiple elements with a single observer, which proves efficient when multiple related components need coordinated size responses.

The unobserve() method stops observing a specific element while maintaining observation of other elements. This method proves useful when components may be conditionally rendered or when you want to temporarily suspend size tracking without disconnecting the entire observer.

Finally, disconnect() removes all element observations and effectively stops the observer. This cleanup step becomes critical in React components to prevent memory leaks and ensure observers don't persist after component unmounting. For more on building robust React applications, check out our guide on React Router v6 to implement proper navigation and component lifecycle management.

Implementing ResizeObserver in React

React's component lifecycle and effect system integrate naturally with ResizeObserver, though proper implementation requires attention to cleanup and stale closure considerations. The most robust approach involves using the useEffect hook with appropriate cleanup logic to manage observer lifecycle.

Basic Implementation Pattern

A straightforward ResizeObserver implementation in React involves creating the observer in a useEffect callback, observing a ref-targeted element, and disconnecting during cleanup. This pattern ensures the observer exists only while the component is mounted and properly releases resources when the component unmounts.

import { useEffect, useRef } from 'react';

function ResponsiveComponent() {
 const containerRef = useRef(null);

 useEffect(() => {
 const observer = new ResizeObserver((entries) => {
 for (const entry of entries) {
 console.log('Element size changed:', entry.contentRect);
 }
 });

 if (containerRef.current) {
 observer.observe(containerRef.current);
 }

 return () => observer.disconnect();
 }, []);

 return <div ref={containerRef}>Content here</div>;
}

This implementation creates a new observer when the component mounts, observes the container element, and disconnects the observer when the component unmounts. The empty dependency array ensures the effect runs only once during the component's lifecycle, preventing unnecessary observer recreation.

Handling Responsive State

Most practical applications need to store dimension information in state for use in rendering. When combining ResizeObserver with React state, be cautious about update frequency and stale closures. Dimension changes can occur rapidly during layout operations, so consider debouncing state updates or using refs for frequently changing values.

Creating a Reusable useResizeObserver Hook

Custom hooks encapsulate ResizeObserver logic for reuse across multiple components. A well-designed hook handles common concerns like cleanup, error prevention, and flexible configuration while providing a simple interface for consuming components.

Hook Implementation

import { useEffect, useState, useRef } from 'react';

function useResizeObserver() {
 const [dimensions, setDimensions] = useState(null);
 const elementRef = useRef(null);

 useEffect(() => {
 const element = elementRef.current;
 if (!element) return;

 const observer = new ResizeObserver((entries) => {
 const [entry] = entries;
 if (entry) {
 setDimensions(entry.contentRect);
 }
 });

 observer.observe(element);

 return () => observer.disconnect();
 }, [elementRef]);

 return [elementRef, dimensions];
}

This hook returns a ref that components can attach to any element and the current dimensions state. Components use the hook by spreading the returned ref onto their root element and accessing dimensions for responsive logic.

Advanced Hook with Options

For more complex use cases, extend the hook to accept options for observing specific box types or handling multiple elements:

function useResizeObserver(options = {}) {
 const [dimensions, setDimensions] = useState(null);
 const elementRef = useRef(null);

 useEffect(() => {
 const element = elementRef.current;
 if (!element) return;

 const observer = new ResizeObserver((entries) => {
 const [entry] = entries;
 if (entry) {
 const { borderBoxSize, contentBoxSize, devicePixelContentBoxSize } = entry;
 setDimensions({
 borderBoxSize: borderBoxSize[0],
 contentBoxSize: contentBoxSize[0],
 contentRect: entry.contentRect
 });
 }
 });

 observer.observe(element, options);

 return () => observer.disconnect();
 }, [elementRef, options]);

 return [elementRef, dimensions];
}

This enhanced version exposes all available size properties while maintaining the simple ref-based interface. Components can access whichever measurement type suits their needs. By encapsulating this logic in a custom hook, you ensure consistent behavior across your React application while keeping your components clean and focused on their primary responsibilities. For building complete React applications with proper navigation and routing, explore our comprehensive guide on React Router v6.

Practical Use Cases

ResizeObserver enables responsive behaviors that were previously difficult or impossible to implement cleanly. Understanding common use cases helps identify opportunities to apply this API in your applications.

Responsive Typography

Typography that scales proportionally with container width creates visually harmonious layouts. Rather than fixed breakpoints or viewport-based units, element-based scaling ensures text appears appropriately sized regardless of where the component appears in the layout. When combined with CSS custom properties, you can create truly dynamic typography systems that respond to both container size and user preferences.

function ResponsiveHeading({ children }) {
 const [ref, dimensions] = useResizeObserver();
 const [fontSize, setFontSize] = useState('1.5rem');

 useEffect(() => {
 if (dimensions?.width) {
 // Calculate font size proportional to container width
 const newSize = Math.max(1, Math.min(4, dimensions.width / 200));
 setFontSize(`${newSize}rem`);
 }
 }, [dimensions?.width]);

 return (
 <h1 ref={ref} style={{ fontSize }}>
 {children}
 </h1>
 );
}

This component calculates font size based on actual container width rather than viewport width, enabling truly modular typography that adapts to its context.

Adaptive Data Visualizations

Charts and graphs often need to resize to fit their containers, particularly in dashboard layouts or responsive pages. ResizeObserver provides the dimension information necessary for proper chart resizing:

function ResponsiveChart() {
 const [ref, dimensions] = useResizeObserver();
 const chartRef = useRef(null);

 useEffect(() => {
 if (chartRef.current && dimensions?.width) {
 // Redraw chart with new dimensions
 drawChart(chartRef.current, {
 width: dimensions.width,
 height: dimensions.height || 400
 });
 }
 }, [dimensions?.width, dimensions?.height]);

 return (
 <div ref={ref} style={{ width: '100%', height: '400px' }}>
 <canvas ref={chartRef} />
 </div>
 );
}

The chart component automatically redraws whenever its container changes size, ensuring visualizations always fit their available space.

Dynamic Grid Layouts

Card grids and responsive galleries benefit from knowing their container dimensions. Rather than relying solely on viewport breakpoints, element-aware layouts can adapt based on actual available space:

function ResponsiveGrid({ children }) {
 const [ref, dimensions] = useResizeObserver();
 const [columns, setColumns] = useState(3);

 useEffect(() => {
 if (dimensions?.width) {
 // Determine column count based on container width
 if (dimensions.width < 480) setColumns(1);
 else if (dimensions.width < 768) setColumns(2);
 else if (dimensions.width < 1200) setColumns(3);
 else setColumns(4);
 }
 }, [dimensions?.width]);

 return (
 <div ref={ref} style={{
 display: 'grid',
 gridTemplateColumns: `repeat(${columns}, 1fr)`,
 gap: '1rem'
 }}>
 {children}
 </div>
 );
}

This grid component calculates column count based on its actual width, enabling responsive behavior that works regardless of where the grid appears in the page layout. For creating visually appealing overlays and backgrounds for your responsive components, explore our guide on CSS background image color overlay techniques.

Handling ResizeObserver Errors

One common issue when using ResizeObserver involves infinite loops that trigger the error "ResizeObserver loop completed with undelivered notifications." This error occurs when ResizeObserver callbacks modify observed elements in ways that trigger additional callbacks, creating potentially infinite recursion.

Understanding the Error

The browser prevents lockup by deferring resize events that would cause cyclic dependencies. When this happens, the console displays a warning and an error event fires on the Window object. The error doesn't prevent your application from functioning, but indicates inefficient resize handling that may cause visual flicker or performance issues.

Preventing Infinite Loops

Several strategies prevent ResizeObserver loops. First, avoid modifying observed elements in callbacks when possible. If modification is necessary, use requestAnimationFrame to defer changes until after the current frame renders:

useEffect(() => {
 const observer = new ResizeObserver((entries) => {
 requestAnimationFrame(() => {
 for (const entry of entries) {
 // Modify element safely
 entry.target.style.width = `${entry.contentRect.width + 10}px`;
 }
 });
 });

 if (ref.current) observer.observe(ref.current);
 return () => observer.disconnect();
}, []);

Using requestAnimationFrame defers modifications until after rendering, preventing loop conditions. Alternatively, track expected sizes and skip updates when dimensions haven't meaningfully changed:

useEffect(() => {
 const observer = new ResizeObserver((entries) => {
 for (const entry of entries) {
 const newWidth = entry.contentRect.width;
 const currentWidth = parseInt(entry.target.style.width) || 0;

 // Only update if change exceeds threshold
 if (Math.abs(newWidth - currentWidth) > 10) {
 entry.target.style.width = `${newWidth}px`;
 }
 }
 });

 if (ref.current) observer.observe(ref.current);
 return () => observer.disconnect();
}, []);

This approach uses a threshold to prevent recursive updates, ensuring stable performance even when elements are modified in callbacks.

Browser Support and Performance

ResizeObserver enjoys broad browser support, making it safe for production use in modern applications. According to browser compatibility data, the API works in Chrome 64 and later, Edge 79 and later, Firefox 69 and later, and Safari 13.1 and later. This coverage includes essentially all browsers in current use, though checking specific requirements for your audience remains advisable.

BrowserVersion
Chrome64+
Edge79+
Firefox69+
Safari13.1+

Performance Best Practices

ResizeObserver is designed for efficient operation in modern browsers. The API notifies callbacks only when actual dimension changes occur, avoiding continuous polling or excessive event generation. When multiple dimensions change simultaneously, callbacks receive all changes in a single entries array, enabling batch processing.

For optimal performance, follow these guidelines:

  • Observe the minimum number of elements necessary - Each observed element adds overhead
  • Use a single observer for multiple related elements - Reduces memory footprint
  • Avoid modifying observed elements in callbacks - Prevents infinite loops
  • Consider using refs instead of state for frequently changing values
  • Implement debouncing for high-frequency dimension changes

Fallback Strategies

For projects supporting older browsers that lack ResizeObserver support, polyfills provide compatible functionality. The resize-observer-polyfill package offers a standards-compliant implementation that falls back to the native API when available. When using polyfills, test thoroughly as performance characteristics may differ from native implementations.

Implementing these patterns in your React development workflow ensures robust, performant responsive components that work across all modern browsers. For building splash screens and entry experiences in React Native applications, explore our guide on building splash screens in React Native to create seamless user on-boarding experiences.

Key ResizeObserver Implementation Principles

Best practices for building responsive React components

Proper Lifecycle Management

Always disconnect observers in cleanup functions to prevent memory leaks and ensure observers don't persist after component unmounting.

Thoughtful State Handling

Use refs for high-frequency updates, state for render-dependent values, and consider debouncing to optimize performance.

Error Awareness

Prevent infinite loops with requestAnimationFrame and implement change thresholds to avoid recursive resize callbacks.

Custom Hook Encapsulation

Abstract ResizeObserver complexity into reusable hooks that provide clean interfaces for component consumption.

Conclusion

ResizeObserver provides a powerful mechanism for building truly responsive React components that adapt to their container dimensions. By observing element size changes directly, applications can implement sophisticated responsive behaviors that weren't practical with traditional media query approaches.

The key to effective ResizeObserver implementation lies in:

  1. Proper lifecycle management - Always disconnect observers in cleanup functions
  2. Thoughtful state handling - Use refs for high-frequency updates, state for render-dependent values
  3. Error awareness - Prevent infinite loops with requestAnimationFrame and change thresholds

Custom hooks encapsulate this complexity, providing clean interfaces for component consumption while maintaining optimal performance. By mastering these patterns, you can build truly adaptive, context-aware interfaces that deliver consistent user experiences across all screen sizes and contexts.

As web applications increasingly demand context-aware responsive behavior, ResizeObserver becomes an essential tool in the modern React developer's toolkit. Start by identifying components that would benefit from element-aware responsiveness, then apply the patterns and practices outlined in this guide to implement clean, maintainable solutions.

Looking to build sophisticated responsive interfaces for your next project? Our web development team specializes in modern React techniques including ResizeObserver for creating adaptive, context-aware user experiences. For applications that require intelligent automation and AI-powered features, explore our AI automation services to enhance user engagement and streamline workflows.

Frequently Asked Questions

Build Truly Responsive React Applications

Our team specializes in modern React development techniques including ResizeObserver for adaptive, context-aware interfaces.