Skip to main content

React Beautiful DND

The example below integrates React Virtuoso with React Beautiful DND.

import React, { useCallback, useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
import { Virtuoso } from 'react-virtuoso'
import { DragDropContext, Droppable, Draggable } 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()
  }
})

// Generate our initial big data set
const initial = Array.from({ length: 1000 }, (_, k) => ({
  id: `id:${k}`,
  text: `item ${k}`,
}))

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

  return result
}

function Item({ provided, item, isDragging }) {
  return (
    <div style={{ paddingBottom: '8px' }}>
      <div
        {...provided.draggableProps}
        {...provided.dragHandleProps}
        ref={provided.innerRef}
        style={provided.draggableProps.style}
        className={`item ${isDragging ? 'is-dragging' : ''}`}
      >
        {item.text}
      </div>
    </div>
  )
}

const HeightPreservingItem = ({ children, ...props }) => {
  const [size, setSize] = useState(0)
  const knownSize = props['data-known-size']
  useEffect(() => {
    setSize((prevSize) => {
      return knownSize == 0 ? prevSize : knownSize
    })
  }, [knownSize])
  return (
    <div
      {...props}
      className="height-preserving-container"
      // check styling in the style tag below
      style={{ '--child-height': `${size}px`, }}
    >
      {children}
    </div>
  )
}

export default function App() {
  const [items, setItems] = useState(initial)

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

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

  return (
    <div style={{ padding: '1rem' }}>
      <style>
        {`
          .height-preserving-container:empty {
            min-height: calc(var(--child-height));
            box-sizing: border-box;
          }
      `}
      </style>
      <DragDropContext onDragEnd={onDragEnd}>
        <Droppable
          droppableId="droppable"
          mode="virtual"
          renderClone={(provided, snapshot, rubric) => (
            <Item provided={provided} isDragging={snapshot.isDragging} item={items[rubric.source.index]} />
          )}
        >
          {(provided) => {
            return (
              <Virtuoso
                components={{
                  Item: HeightPreservingItem,
                }}
                scrollerRef={provided.innerRef}
                data={items}
                style={{ width: 300, height: 500 }}
                itemContent={(index, item) => {
                  return (
                    <Draggable draggableId={item.id} index={index} key={item.id}>
                      {(provided) => <Item provided={provided} item={item} isDragging={false} />}
                    </Draggable>
                  )
                }}
              />
            )
          }}
        </Droppable>
      </DragDropContext>
    </div>
  )
}