Web

React Window Virtualization for Dynamic Lists with Media

Fix layout shakes in react window and react virtualized for dynamic React lists with frequent updates, images, buttons. Code examples, react-virtuoso alternative, patterns for smooth performance.

1 answer 1 view

How to implement virtualization for a dynamic React list with frequent updates, media content, and buttons?

I have a list of data from the backend that updates frequently (items are added, removed, or modified). Each item can contain text, media (e.g., images), and interactive buttons.

Challenges with react-virtualized:

  • Layout shakes horribly when data changes.
  • CellMeasurer often fails to account for button heights accurately.

What I’m seeking:

  • Alternative libraries that handle dynamic updates, complex item content (media + buttons), and smooth performance better than react-virtualized.
  • Or advanced react-virtualized examples/code snippets that address these issues with real-world dynamic data scenarios.

Most online examples are too simplistic and don’t cover frequent updates or mixed content types.

Use a library that handles variable heights and item measurement (react-virtuoso or react-window with ResizeObserver + stable itemKey) and apply these patterns: reserve layout space for media, measure and debounce size changes, avoid index-based keys, and reset measurement caches selectively. If you must stay on react-virtualized, combine CellMeasurerCache clearing + list.recomputeRowHeights(index) triggered after media/button mount/resize and use stable keys to stop DOM-reuse focus jumps.


Contents


Problem: why react-virtualized shakes with frequent updates and mixed media

You’re seeing layout shakes because virtualization relies on stable item sizes or accurate measurements; when items change height (images load, text wraps, buttons render, or items are inserted/removed) the cached sizes become stale and the virtual scroller reuses DOM nodes (index-based recycling), which moves content around. CellMeasurer can help, but it often measures too early (before fonts/CSS/buttons finish rendering) or the cache isn’t invalidated correctly, so measured heights are wrong and rows jump.

Two common root causes:

  • DOM/node reuse with index-as-key: the list rebinds a different item into an existing DOM node, causing focus and button state to jump.
  • Late-changing heights (images, async content, buttons styled by fonts): measurements taken on mount miss later size changes unless you re-measure.

(For search-intent context, Yandex Wordstat shows “react window” and “react virtualized” are high-volume queries — the choice of wording matters when people search for solutions.) See Yandex Wordstat for query volumes: Yandex Wordstat — topRequests: react window.


React Window and alternatives for dynamic lists

Options that handle dynamic updates better than an out-of-the-box react-virtualized setup:

  • react-virtuoso — excellent at automatic variable-height measurement and tolerant of frequent inserts/removals; it minimizes layout flashes for mixed content.
  • react-window (the lighter successor to react-virtualized) — use VariableSizeList + manual measurement (ResizeObserver) and itemKey to avoid DOM-reuse problems.
  • RecyclerListView — performant for extreme throughput (often used in RN and heavy web use-cases), but more complex to integrate.
  • Stay with react-virtualized only if you need some specific components; otherwise prefer react-window or react-virtuoso and apply the patterns below.

Note: search queries often mix “react window” vs “react windows” (ambiguous); if you’re optimizing help pages, disambiguate the library name vs OS queries and mention both forms. See the related Wordstat entries for awareness: Yandex Wordstat — topRequests: react virtualized.


React virtualization patterns that fix layout shakes

These patterns are applicable across libraries (react-window, react-virtuoso, react-virtualized).

  1. Stable keys — never use index as key. With react-window, set itemKey so nodes aren’t recycled incorrectly:
  • itemKey=
  1. Reserve space for media — give images an intrinsic aspect ratio or an explicit placeholder height so the initial measurement is close to final (no shift). Use CSS aspect-ratio or width+height attributes.

  2. Measure changes reliably — use ResizeObserver (or onLoad for images) to detect when an item’s height actually changes, then tell the virtualizer to re-measure that item (resetAfterIndex or recomputeRowHeights).

  3. Debounce/aggregate measurement updates — image loads or many small updates can fire many reflows. Debounce calls to reset/recompute to batch re-layouts.

  4. Use overscan — a small positive overscanCount reduces visible white gaps during fast scroll.

  5. Preserve focus & interactive state — save focused item id before updates, use stable keys (itemKey), and restore focus after render if needed.

  6. Memoize item renderers — avoid re-rendering every item on each data change. Use React.memo and pass stable props.

  7. Use startTransition for non-urgent updates — mark frequent incoming data changes as low priority to keep scrolling smooth.

  8. Prefer libraries that auto-measure variable heights for you (react-virtuoso) if you want less plumbing.


Code: react-window VariableSizeList + ResizeObserver pattern

This pattern uses VariableSizeList, an item-size map, ResizeObserver inside each item, and a debounced resetAfterIndex. It addresses frequent inserts/removes, images, and buttons.

jsx
// Example (conceptual)
import React, { useRef, useEffect, useCallback } from 'react';
import { VariableSizeList as List } from 'react-window';

function useDebounced(fn, wait = 50) {
 const t = useRef(null);
 return (...args) => {
 clearTimeout(t.current);
 t.current = setTimeout(() => fn(...args), wait);
 };
}

export default function VirtualList({ items, height, width, estimatedItemSize = 120 }) {
 const listRef = useRef();
 const sizeMap = useRef(new Map()); // key -> measured height

 const getSize = index => {
 const id = items[index].id;
 return sizeMap.current.get(id) || estimatedItemSize;
 };

 const debouncedReset = useDebounced((index) => {
 if (listRef.current) listRef.current.resetAfterIndex(index, false);
 }, 50);

 const Row = ({ index, style }) => {
 const item = items[index];
 const containerRef = useRef();

 useEffect(() => {
 if (!containerRef.current) return;
 const ro = new ResizeObserver(entries => {
 for (const entry of entries) {
 const h = Math.round(entry.contentRect.height);
 const prev = sizeMap.current.get(item.id);
 if (prev !== h) {
 sizeMap.current.set(item.id, h);
 debouncedReset(index); // schedule recalculation once per burst
 }
 }
 });
 ro.observe(containerRef.current);
 return () => ro.disconnect();
 }, [item.id, index]);

 return (
 <div style={style}>
 <div ref={containerRef}>
 <img
 src={item.image}
 alt=""
 style={{ width: '100%', height: 'auto' }}
 onLoad={() => debouncedReset(index)} // ensure measurement after load
 />
 <div>{item.text}</div>
 <button onClick={() => console.log('clicked', item.id)}>Action</button>
 </div>
 </div>
 );
 };

 return (
 <List
 ref={listRef}
 height={height}
 width={width}
 itemCount={items.length}
 itemSize={getSize}
 overscanCount={4}
 itemKey={index => items[index].id} // crucial: stable DOM mapping
 >
 {Row}
 </List>
 );
}

Notes:

  • itemKey avoids DOM reuse problems that cause focus/button issues.
  • ResizeObserver plus debounced resetAfterIndex prevents repeated full recalculations.
  • Set an estimatedItemSize close to average item height to reduce initial jump.
  • Use onLoad on images to trigger measurement after the image finishes.

Code: react-virtuoso example (handles variable heights automatically)

If you want minimal plumbing, react-virtuoso measures native item heights and is robust for frequent changes:

jsx
import { Virtuoso } from 'react-virtuoso';

export default function VirtuosoList({ items }) {
 return (
 <Virtuoso
 data={items}
 itemContent={(index, item) => (
 <div data-item-id={item.id}>
 <img src={item.image} style={{ width: '100%', height: 'auto' }} alt="" />
 <div>{item.text}</div>
 <button onClick={() => doAction(item.id)}>Action</button>
 </div>
 )}
 increaseViewportBy={200} // similar to overscan; tune to your needs
 computeItemKey={index => items[index].id}
 />
 );
}

Why this helps:

  • Virtuoso measures items when they mount and after size changes, so you don’t need manual ResizeObserver plumbing.
  • It handles inserts/removals smoothly and preserves focus when keys are stable.

If you’re evaluating libraries, compare react-virtuoso vs react-window for your update rates and complexity.


Advanced react-virtualized fixes (CellMeasurer tips)

If you must stick with react-virtualized, apply these targeted fixes:

  • Use a CellMeasurerCache with a reasonable defaultHeight and fixedWidth (if width is constant).
  • When images or fonts finish loading inside a cell, call:
  • cache.clear(rowIndex)
  • listRef.current.recomputeRowHeights(rowIndex)
  • Use rowRenderer that wraps each row in CellMeasurer so per-row measurement is on-demand.
  • Avoid index-as-key. If your List/Grid uses index everywhere, switch to a mapping so DOM nodes represent item ids.
  • If many items change height simultaneously, avoid clearing the entire cache every frame; instead mark affected indices and recompute them in batches (debounce).
  • If you need to force a full recompute (rare), call cache.clearAll() then force an update—but this can briefly reflow the whole list.

Example sketch (conceptual):

jsx
// pseudo for react-virtualized
const cache = new CellMeasurerCache({ defaultHeight: 120, fixedWidth: true });
const listRef = useRef();

function onImageLoad(index) {
 cache.clear(index);
 listRef.current.recomputeRowHeights(index);
}

Performance tuning & checklist for media + buttons

Quick checklist to apply and measure:

  • Keys & DOM
  • [ ] Use stable ids via itemKey/computeItemKey/keys — never index.
  • Measurement
  • [ ] Estimate an average item height to reduce initial jump.
  • [ ] Use ResizeObserver or image onLoad to trigger per-item re-measure.
  • [ ] Debounce resets (50–150ms) to batch rapid changes.
  • Scrolling & UX
  • [ ] Set overscanCount / increaseViewportBy a bit to avoid blank frames.
  • [ ] Use startTransition for heavy incoming updates so scrolling stays responsive.
  • Rendering
  • [ ] Memoize item components (React.memo) and avoid inline function props passing new references every render.
  • [ ] Keep item DOM shallow and move heavy logic out of item renderers.
  • Media
  • [ ] Add width/height attributes or use CSS aspect-ratio to reserve space.
  • [ ] Lazy-load images with IntersectionObserver when appropriate.
  • Focus & state
  • [ ] Save focused item id and restore after data updates if necessary.
  • [ ] Avoid uncontrolled DOM state that virtualizer might recycle.
  • Measure success
  • [ ] Test with real update patterns and use Chrome Performance (FPS, main-thread times).
  • [ ] Use representative device/browser (mobile is often worst-case).

When to pick which library (tradeoffs)

  • Choose react-virtuoso if: you want minimal measurement plumbing, lots of variable-height items, frequent updates, and quick dev iteration.
  • Choose react-window if: you want a small, fast core and are willing to add ResizeObserver + item measurement code.
  • Choose react-virtualized if: you rely on specific components it provides and are prepared to manage CellMeasurer cache complexities.
  • Choose RecyclerListView if: you need the utmost throughput for thousands of updates per second (more complex integration).

If you’re unsure, prototype a small slice of your app with react-virtuoso and with react-window + ResizeObserver and compare memory, smoothness, and development cost.


Sources


Conclusion

For smooth react virtualization with frequent updates, media, and interactive buttons: prefer a library that measures variable heights automatically (react-virtuoso) or use react window with ResizeObserver + itemKey + debounced resetAfterIndex to avoid layout shakes. If you must keep react-virtualized, clear and recompute CellMeasurerCache per affected index after images/buttons mount and never use index-as-key. Start with those patterns and iterate while profiling; you’ll likely see dramatic reductions in layout jumps and focus/interaction bugs with either react-virtuoso or the react window pattern.

Authors
Verified by moderation
Moderation
React Window Virtualization for Dynamic Lists with Media