0

I have a problem getting react-beautiful-dnd to work without flickering. I have followed the example in the egghead course. Here is my code sample.

Item List Container

  onDragEnd = (result) => {
    if (this.droppedOutsideList(result) || this.droppedOnSamePosition(result)) {
      return;
    }
    this.props.itemStore.reorderItem(result);
  }

  droppedOnSamePosition = ({ destination, source }) => destination.droppableId
    === source.droppableId && destination.index === source.index;

  droppedOutsideList = result => !result.destination;

  render() {
    return (
      <DragDropContext onDragEnd={this.onDragEnd}>
        <div>
          {this.props.categories.map((category, index) => (
            <ListCategory
              key={index}
              category={category}
              droppableId={category._id}
            />
          ))}
        </div>
      </DragDropContext>
    );
  }

Item Category

const ListCategory = ({
  category, droppableId,
}) => (
  <Droppable droppableId={String(droppableId)}>
    {provided => (
      <div
        {...provided.droppableProps}
        ref={provided.innerRef}
      >
        <ListTitle
          title={category.name}
        />
        <ListItems category={category} show={category.items && showIndexes} />
        {provided.placeholder}
      </div>
    )}
  </Droppable>
);

List items

   <Fragment>
      {category.items.map((item, index) => (
        <ListItem
          key={index}
          item={item}
          index={index}
        />
      ))}
    </Fragment>

Items

  render() {
    const {
      item, index, categoryIndex, itemStore,
    } = this.props;

    return (
      <Draggable key={index} draggableId={item._id} index={index}>
        {(provided, snapshot) => (
          <div
            role="presentation"
            className={cx({
              'list-item-container': true,
              'selected-list-item': this.isSelectedListItem(item._id),
            })}
            ref={provided.innerRef}
            {...provided.draggableProps}
            {...provided.dragHandleProps}
            style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
            onClick={this.handleItemClick}
          >
            <div className={cx('select-title')}>
             <p className={cx('list-item-name')}>{item.title}</p>
            </div>
                {capitalize(item.importance)}
            </div>
          </div>
        )}
      </Draggable>
    );
  }

Method to reorder Items (I'm using Mobx-State_Tree)

    reorderItem: flow(function* reorderItem(result) {
      const { source, destination } = result;

      const categorySnapshot = getSnapshot(self.itemCategories);
      const sourceCatIndex = self.itemCategories
        .findIndex(category => category._id === source.droppableId);
      const destinationCatIndex = self.itemCategories
        .findIndex(category => category._id === destination.droppableId);
      const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
      const [draggedItem] = sourceCatItems.splice(source.index, 1);

      if (sourceCatIndex === destinationCatIndex) {
        sourceCatItems.splice(destination.index, 0, draggedItem);
        const prioritizedItems = setItemPriorities(sourceCatItems);
        applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
        try {
          yield itemService.bulkEditPriorities(prioritizedItems);
        } catch (error) {
          console.error(`Problem editing priorities: ${error}`);
        }
      } else {
        const destinationCatItems = Array.from(categorySnapshot[destinationCatIndex].items);
        destinationCatItems.splice(destination.index, 0, draggedItem);

        const prioritizedSourceItems = setItemPriorities(sourceCatItems);
        applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedSourceItems);

        const prioritizedDestItems = setItemPriorities(destinationCatItems);
        applySnapshot(self.itemCategories[destinationCatIndex].items, prioritizedDestItems);
        try {
          const sourceCatId = categorySnapshot[sourceCatIndex]._id;
          const originalItemId = categorySnapshot[sourceCatIndex].items[source.index]._id;
          yield itemService.moveItemToNewCategory(originalItemId, sourceCatId, destinationCatIndex);
        } catch (error) {
          console.error(`Problem editing priorities: ${error}`);
        }
      }
    }),

Sample data

const itemData = [
    {
      _id: 'category-1',
      title: 'Backlog',
      items: [
        { _id: 'item-1', title: 'Here and back again' },
    },
    {
      _id: 'category-2',
      title: 'In progress',
      items: []
    },
   {
      _id: 'category-3',
      title: 'Done',
      items: []
    }
  }
}
Summary

When and item is dragged and dropped, I check to see if the item is dropped in the outside the dnd context or in the same position it was dragged from. If true, i do nothing. If the item is dropped within the context, i check to see if it was dropped in the same category. if true, i remove the item from its current position, put it in the target position, update my state, and make an API call.

If it was dropped in a different category, i remove the item from the source category, add to the new category, update the state and make an API call. Am I missing something?

Chukwuma Zikora
  • 109
  • 1
  • 4
  • 10

2 Answers2

0

I am using both mst and the react-beautiful-dnd library I will just paste my onDragEnd action method

onDragEnd(result: DropResult) {
  const { source, destination } = result;

  // dropped outside the list
  if (!destination) {
    return;
  }

  if (source.droppableId === destination.droppableId) {
    (self as any).reorder(source.index, destination.index);
  }
},
reorder(source: number, destination: number) {
  const tempLayout = [...self.layout];
  const toMove = tempLayout.splice(source, 1);
  const item = toMove.pop();

  tempLayout.splice(destination + lockedCount, 0, item);

  self.layout = cast(tempLayout);
},

I think in order to avoid the flicker you need to avoid using applySnapshot

You can replace this logic

      const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
      const [draggedItem] = sourceCatItems.splice(source.index, 1);

      sourceCatItems.splice(destination.index, 0, draggedItem);
      const prioritizedItems = setItemPriorities(sourceCatItems);
      applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);

just splice the items tree

const [draggedItem] = categorySnapshot[sourceCatIndex].items.splice(destination.index, 0, draggedItem)

this way you don't need to applySnapshot on the source items after

etudor
  • 1,183
  • 1
  • 11
  • 19
0

I believe this issue is caused by multiple dispatches happening at the same time.

There're couple of things going on at the same time. The big category of stuff is going on is the events related to onDragStart, onDragEnd and onDrop. Because that's where an indicator has to show to the user they are dragging and which item they are dragging from and to.

So especially you need to put a timeout to onDragStart.

  const invoke = (fn: any) => { setTimeout(fn, 0) } 

Because Chrome and other browser will cancel the action if you don't do that. However that is also the key to prevent flickery.

const DndItem = memo(({ children, index, onItemDrop }: DndItemProps) => {
  const [dragging, setDragging] = useState(false)
  const [dropping, setDropping] = useState(false)
  const dragRef = useRef(null)
  const lastEnteredEl = useRef(null)

  const onDragStart = useCallback((e: DragEvent) => {
    const el: HTMLElement = dragRef.current
    if (!el || (
      document.elementFromPoint(e.clientX, e.clientY) !== el
    )) {
      e.preventDefault()
      return
    }
    e.dataTransfer.setData("index", `${index}`)
    invoke(() => { setDragging(true) })
  }, [setDragging])
  const onDragEnd = useCallback(() => {
    invoke(() => { setDragging(false) })
  }, [setDragging])
  const onDrop = useCallback((e: any) => {
    invoke(() => { setDropping(false) })
    const from = parseInt(e.dataTransfer.getData("index"))
    onItemDrop && onItemDrop(from, index) 
  }, [setDropping, onItemDrop])
  const onDragEnter = useCallback((e: DragEvent) => {
    lastEnteredEl.current = e.target
    e.preventDefault()
    e.stopPropagation()
    setDropping(true)
  }, [setDropping])
  const onDragLeave = useCallback((e: DragEvent) => {
    if (lastEnteredEl.current !== e.target) {
      return
    }
    e.preventDefault()
    e.stopPropagation()
    setDropping(false)
  }, [setDropping])

  return (
    <DndItemStyle
      draggable="true"
      onDragStart={onDragStart}
      onDragEnd={onDragEnd}
      onDrop={onDrop}
      onDragOver={onDragOver}
      onDragEnter={onDragEnter}
      onDragLeave={onDragLeave}
      dragging={dragging}
      dropping={dropping}
    >
      {(index < 100) && (
        cloneElement(children as ReactElement<any>, { dragRef })
      )}
    </DndItemStyle>
  )
})

I have to apply two more timeout invoke in the above DndItem, the reason for that is during the drop, there're two many events are competing with each other, to name a few

  • onDragEnd, to sugar code the indicator
  • onDrop, to re-order

I need to make sure re-order happens very quickly. Because otherwise you get double render, one with the previous data, and one with the next data. And that's why the flickery is about.

In short, React + Dnd needs to apply setTimeout so that the order of the paint can be adjusted to get the best result.

windmaomao
  • 7,120
  • 2
  • 32
  • 36