An Easy to Use React DnD Sortable Component

I hope this is actually easy to use...

Target

  • Easy to use

  • Less code

Code

You can simply copy the following code to a dnd-sortable.tsx file, and use as a component.

import React, { FC, ReactNode, useCallback, useRef, useState } from "react";
import { useDrag, useDrop } from "react-dnd";
import type { XYCoord, Identifier } from "dnd-core";
import update from "immutability-helper";

export interface NodeProps {
  id: any;
  index: number;
  node: ReactNode;
  moveNode: (dragIndex: number, hoverIndex: number) => void;
}

interface DragItem {
  index: number;
  id: string;
  type: string;
}

export const Node: FC<NodeProps> = ({ id, index, node, moveNode: moveNode }) => {
  const ref = useRef<HTMLDivElement>(null);
  const [{ handlerId }, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
    accept: "item",
    collect(monitor) {
      return {
        handlerId: monitor.getHandlerId(),
      };
    },
    hover(item: DragItem, monitor) {
      if (!ref.current) {
        return;
      }
      const dragIndex = item.index;
      const hoverIndex = index;

      // Don't replace items with themselves
      if (dragIndex === hoverIndex) {
        return;
      }

      // Determine rectangle on screen
      const hoverBoundingRect = ref.current?.getBoundingClientRect();

      // Get vertical middle
      const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;

      // Determine mouse position
      const clientOffset = monitor.getClientOffset();

      // Get pixels to the top
      const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;

      // Only perform the move when the mouse has crossed half of the items' height
      // When dragging downwards, only move when the cursor is below 50%
      // When dragging upwards, only move when the cursor is above 50%

      // Dragging downwards
      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
        return;
      }

      // Dragging upwards
      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
        return;
      }

      // Time to actually perform the action
      moveNode(dragIndex, hoverIndex);

      // Note: we're mutating the monitor item here!
      // Generally it's better to avoid mutations,
      // but it's good here for the sake of performance
      // to avoid expensive index searches.
      item.index = hoverIndex;
    },
  });

  const [{ isDragging }, drag] = useDrag({
    type: "item",
    item: () => {
      return { id, index };
    },
    collect: (monitor: any) => ({
      isDragging: monitor.isDragging(),
    }),
  });

  const opacity = isDragging ? 0.5 : 1;
  drag(drop(ref));
  return (
    <div ref={ref} style={{ opacity }} data-handler-id={handlerId}>
      {node}
    </div>
  );
};

export interface Item {
  id: number;
  node: ReactNode;
}

export default function DndSortable({ nodeList }: { nodeList: Array<ReactNode> }) {
 // Set id for every react node for react-dnd to use
  const [itemList, setItemList] = useState(nodeList.map((node, index) => ({ id: index, node: node })));

  const moveNode = useCallback((dragIndex: number, hoverIndex: number) => {
    setItemList((prevNodes: Item[]) =>
      update(prevNodes, {
        $splice: [
          [dragIndex, 1],
          [hoverIndex, 0, prevNodes[dragIndex] as Item],
        ],
      })
    );
  }, []);

  const renderNode = useCallback((id: number, index: number, node: ReactNode) => {
    return <Node key={id} index={index} id={id} node={node} moveNode={moveNode} />;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return <>{itemList.map((item, index) => renderNode(item.id, index, item.node))}</>;
}

Usage

const cards = useRef([
  {
    id: 1,
    text: "Write a cool JS library",
  },
  {
    id: 2,
    text: "Make it generic enough",
  },
  {
    id: 3,
    text: "Write README",
  },
  {
    id: 4,
    text: "Create some examples",
  },
  {
    id: 5,
    text: "Spam in Twitter and IRC to promote it (note that this element is taller than the others)",
  },
  {
    id: 6,
    text: "The origin text here is a symbol that is not ok",
  },
  {
    id: 7,
    text: "PROFIT",
  },
]);

...

<DndSortable
  nodeList={cards.current.map((card, i) => {
    return (
      <div key={i} className="dnd-sortable">
        {card.text}
      </div>
    );
  })}
/>

Example

Reference

  1. The react-dnd official simple sortable example: Sortable Simple

Last updated