Skip to main content

RB DND + Window Scroller

The example below integrates React Virtuoso with React Beautiful DND and the document scroller.

List attached to RB DND + Window Scroller

import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { Virtuoso } from "react-virtuoso";
import * as ReactBeautifulDnd from "react-beautiful-dnd";

// Virtuoso's resize observer can this error,
// which is caught by DnD and aborts dragging.
window.addEventListener("error", (e) => {
  if (
    e.message ===
      "ResizeObserver loop completed with undelivered notifications." ||
    e.message === "ResizeObserver loop limit exceeded"
  ) {
    e.stopImmediatePropagation();
  }
});

export default function App(){  
  const [items, setItems] = useState(() => {
    return Array.from({ length: 1000 }, (_, k) => ({
      id: `id:${k}`,
      text: `item ${k}`,
    }))
  })

  const reorder = React.useCallback((list, startIndex, endIndex) => {
    const result = Array.from(list)
    const [removed] = result.splice(startIndex, 1)
    result.splice(endIndex, 0, removed)

    return result
  }, [])

  const onDragEnd = React.useCallback(
    (result) => {
      if (!result.destination) {
        return
      }
      if (result.source.index === result.destination.index) {
        return
      }

      setItems((items) => reorder(items, result.source.index, result.destination.index))
    },
    [setItems, reorder]
  )

  const Item = React.useMemo(() => {
    return ({ provided, item, isDragging }) => {
      // For borders and visual space,
      // use container with padding rather than a margin
      // margins confuse virtuoso rendering
      return (
        <div
          {...provided.draggableProps}
          {...provided.dragHandleProps}
          ref={provided.innerRef}
          style={{ ...provided.draggableProps.style, paddingBottom: '8px' }}
        >
          <div
            style={{
              border: `1px solid ${isDragging ? 'red' : 'black'}`,
            }}
          >
            {item.text}
          </div>
        </div>
      )
    }
  }, [])

  const HeightPreservingItem = React.useMemo(() => {
    return ({ children, ...props }) => {
      return (
        // the height is necessary to prevent the item container from collapsing, which confuses Virtuoso measurements
        <div {...props} style={{ height: props['data-known-size'] || undefined }}>
          {children}
        </div>
      )
    }
  }, [])

  return (
    <div style={{ overflow: 'auto' }}>
      <ReactBeautifulDnd.DragDropContext onDragEnd={onDragEnd}>
        <ReactBeautifulDnd.Droppable
          droppableId="droppable"
          mode="virtual"
          renderClone={(provided, snapshot, rubric) => (
            <Item provided={provided} isDragging={snapshot.isDragging} item={items[rubric.source.index]} />
          )}
        >
          {(provided) => {
            return (
              <div ref={provided.innerRef}>
                <Virtuoso
                  useWindowScroll
                  components={{
                    Item: HeightPreservingItem,
                  }}
                  data={items}
                  itemContent={(index, item) => {
                    return (
                      <ReactBeautifulDnd.Draggable draggableId={item.id} index={index} key={item.id}>
                        {(provided) => <Item provided={provided} item={item} isDragging={false} />}
                      </ReactBeautifulDnd.Draggable>
                    )
                  }}
                />
              </div>
            )
          }}
        </ReactBeautifulDnd.Droppable>
      </ReactBeautifulDnd.DragDropContext>
    </div>
  )
}