Skip to Content
Menu
All Posts

Article

Constrain React DnD to the Horizontal Axis

I recently started soloing a pretty complex private project that required me to use the drag-and-drop gesture at some point. Already burning out 💀 and from experience, I knew that working it mysel...

Toluwani Adewale 3 min read
Updated

I recently started soloing a pretty complex private project that required me to use the drag-and-drop gesture at some point. Already burning out 💀 and from experience, I knew that working it myself (using HTML's Drag and Drop API) was a slippery slope that I didn't want to slide through, so I opted for a package and came across two great ones; react-dnd and react-beautiful-dnd.
React-dnd being a more complex implementation with a lower level of abstraction stood out to me since I'd be getting the best of both worlds, i.e. more control and less time to build - or so I thought. The documentation, faulty as it stands doesn't explicitly do much justice to it, and given the requirements of my project I spent hours trying to constrain sortable tabs on the X-axis - more time than I should have. In this article, I'll share a tip on how I overcame the challenge.

I assume that you've successfully set up your React project and installed the necessary packages including react-dnd and react-dnd-html5-backend, so we'll jump right into the specifics. 

Firstly, we need to refactor our current implementation to show an empty image when a component is dragged. HTML5 backend forces us to drag elements on any plane which isn't what we want hence we'll have to let go of that specific implementation.

We'll use ConnectDragPreview from the useDrag hook to render an empty image element.

import { useDrag } from "react-dnd";
import { getEmptyImage } from "react-dnd-html5-backend";

...

  const [{ isDragging }, drag, preview] = useDrag({
    type: DragableType.Tab,
    item: () => {
      return { index, tab };
    },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  });

  drag(drop(ref));

  useEffect(() => {
    preview(getEmptyImage());
  });

...

Secondly, React DnD provides another hook called useDragLayer, this allows us to insert any component as the preview of the component being dragged. In our case, we've already rendered an empty image as the preview, so we can tweak the drag layer to reinsert our dragged component, but constrain it to the horizontal axis using the transform CSS property.

import React from "react";
import type { CSSProperties } from "react";
import type { XYCoord } from "react-dnd";
import { useDragLayer } from "react-dnd";
import Tab from "./Tab";

export default function CustomDragLayer() {
  const layerStyles: CSSProperties = {
    position: "fixed",
    pointerEvents: "none",
    zIndex: 100,
    left: 0,
    top: 0,
    width: "100%",
    height: "100%",
    opacity: 0.8,
  };

  const { isDragging, item, initialOffset, currentOffset } = useDragLayer(
    (monitor) => ({
      item: monitor.getItem(),
      itemType: monitor.getItemType(),
      initialOffset: monitor.getInitialSourceClientOffset(),
      currentOffset: monitor.getSourceClientOffset(),
      isDragging: monitor.isDragging(),
    })
  );

  const tab = item?.tab;

  function getItemStyles(
    initialOffset: XYCoord | null,
    currentOffset: XYCoord | null
  ) {
    if (!initialOffset || !currentOffset) {
      return {
        display: "none",
      };
    }

    let { x, y } = currentOffset;
    y = initialOffset.y;

    const transform = `translate(${x}px, ${y}px)`;
    return {
      transform,
      WebkitTransform: transform,
    };
  }

  if (!isDragging) {
    return null;
  }

  return (
    <div style={layerStyles}>
      <div style={getItemStyles(initialOffset, currentOffset)}>
        <Tab tab={tab} />
      </div>
    </div>
  );
}

 Note:  Since we're using a constrained custom drag layer, dragging outside the restricted axis will constrain the component on that axis as expected, but will not implement the useDrop logic. This is because although the backend allows us to drag on any plane, the DragLayerMonitor simply cannot monitor drag gestures outside our restricted axis.

You can find the complete implementation here: react-dnd-horizontal.

Responses (3)

Have a question, correction, or another approach? Add your response.

Your name and response will be public.

@sd

sad

1231132

12132312

asdf

asdfasdf