0

I want to make drag and drop with react native on the web, but I can't find any library that I can use to make it work on the web and the mobile. Most of the packages I found only works on mobile, ex: react-native-drax, rn-dnd-kanban. If there's a package that I could use to create a drag-and-drop that works on both web and mobile, please tell me.

I already made an app that have drag-and-drop feature with expo with react-native-gesture-handler ~2.16.1 and react-native-reanimated ~3.10.1. But what I made have issue that it only works on web and have poorly performance that in consumes so much CPU.

Here's the code that I'm working on.

import React, { useEffect, useState, useRef } from "react";
import { StyleSheet, Text, View, FlatList, Dimensions } from "react-native";
import Animated, {
  runOnJS,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
} from "react-native-reanimated";
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
  ScrollView,
} from "react-native-gesture-handler";
import { generateNewId } from "@/utils/index";

interface Card {
  id: string;
  title: string;
}

interface Column {
  id: string;
  title: string;
  cards: Card[];
}

interface Position {
  x: number;
  y: number;
  width: number;
  height: number;
}

interface DragContext {
  type: "card" | "column";
  columnId: string;
  cardId?: string;
  cardTitle?: string;
}

const initialColumns: Column[] = [
  {
    id: "b1c1",
    title: "To Do",
    cards: [
      { id: "b1c1c1", title: "Workspace - Board" },
      { id: "b1c1c2", title: "Workspace - Priority" },
    ],
  },
  {
    id: "b1c2",
    title: "In Progress",
    cards: [{ id: "b1c2c1", title: "Workspace - Drag And Drop" }],
  },
  { id: "b1c3", title: "Done", cards: [] },
  {
    id: "b1c4",
    title: "Upcoming",
    cards: [{ id: "b1c4c1", title: "Workspace - Backends" }],
  },
  {
    id: "b1c5",
    title: "Pending",
    cards: [{ id: "b1c5c1", title: "Workspace - Assignee" }],
  },
];

function Box({
  children,
  style,
  id,
  activeBox,
  setActiveBox,
  columnId,
  title,
  isColumn = false,
  onDragStart,
  onDragMove,
  onDragEnd,
  measureRef,
  dragHandleRef,
  isDragging,
}: {
  children: React.ReactNode;
  style: any;
  id: string;
  activeBox: string;
  setActiveBox: (id: string) => void;
  columnId?: string;
  title?: string;
  isColumn?: boolean;
  onDragStart?: (context: DragContext) => void;
  onDragMove?: (x: number, y: number) => void;
  onDragEnd?: (x: number, y: number) => void;
  measureRef?: (ref: View) => void;
  dragHandleRef?: React.RefObject<View>;
  isDragging?: boolean;
}) {
  const pressed = useSharedValue(false);
  const offset = useSharedValue({ x: 0, y: 0 });
  const boxRef = useRef<View>(null);
  const startPosition = useSharedValue({ x: 0, y: 0 });

  useEffect(() => {
    if (boxRef.current && measureRef) {
      measureRef(boxRef.current);
    }
  }, [measureRef]);

  const pan = Gesture.Pan()
    .minDistance(1)
    .onBegin((event) => {
      // Only start drag if touching drag handle for columns
      if (isColumn && dragHandleRef?.current) {
        dragHandleRef.current.measure((x, y, width, height, pageX, pageY) => {
          const isTouchInHandle =
            event.absoluteX >= pageX &&
            event.absoluteX <= pageX + width &&
            event.absoluteY >= pageY &&
            event.absoluteY <= pageY + height;

          if (!isTouchInHandle) return;
        });
      }

      pressed.value = true;
      startPosition.value = { x: event.absoluteX, y: event.absoluteY };
      runOnJS(setActiveBox)(id);
      if (onDragStart) {
        runOnJS(onDragStart)({
          type: isColumn ? "column" : "card",
          columnId: isColumn ? id : columnId || "",
          cardId: isColumn ? undefined : id,
          cardTitle: isColumn ? undefined : title,
        });
      }
    })
    .onChange((event) => {
      offset.value = {
        x: event.translationX,
        y: event.translationY,
      };
      if (onDragMove) {
        runOnJS(onDragMove)(event.absoluteX, event.absoluteY);
      }
    })
    .onFinalize((event) => {
      if (onDragEnd) {
        runOnJS(onDragEnd)(event.absoluteX, event.absoluteY);
      }
      offset.value = withSpring({ x: 0, y: 0 });
      pressed.value = false;
      if (activeBox === id) runOnJS(setActiveBox)("");
    });

  const animatedStyles = useAnimatedStyle(() => ({
    transform: [
      { translateX: offset.value.x },
      { translateY: offset.value.y },
      { scale: withTiming(pressed.value ? 1.1 : 1) },
    ],
    backgroundColor: pressed.value ? "#FFE04B" : style.backgroundColor,
    borderRadius: withTiming(pressed.value ? 25 : 10, { duration: 200 }),
    zIndex: pressed.value ? 999 : 1,
    opacity: isDragging ? 0.5 : 1,
  }));

  return (
    <View>
      <Animated.View ref={boxRef} style={[style, animatedStyles]}>
        <GestureDetector gesture={pan}>
          <View ref={dragHandleRef} style={styles.dragHandle}>
            <Text style={styles.dragHandleText}>:::</Text>
          </View>
        </GestureDetector>
        <View>{children}</View>
      </Animated.View>
    </View>
  );
}

export default function App() {
  const [activeBox, setActiveBox] = useState("");
  const [columns, setColumns] = useState(initialColumns);
  const [columnMeasurements, setColumnMeasurements] = useState<
    Map<string, Position>
  >(new Map());
  const [cardMeasurements, setCardMeasurements] = useState<
    Map<string, Position>
  >(new Map());
  const [dragContext, setDragContext] = useState<DragContext | null>(null);
  const [hoveredColumn, setHoveredColumn] = useState<string | null>(null);
  const [hoveredCard, setHoveredCard] = useState<string | null>(null);
  const columnHandleRefs = useRef<Map<string, React.RefObject<View>>>(
    new Map()
  );

  // Initialize refs for column handles
  useEffect(() => {
    columns.forEach((column) => {
      if (!columnHandleRefs.current.has(column.id)) {
        columnHandleRefs.current.set(column.id, React.createRef<View>());
      }
    });
  }, [columns]);

  const measureColumn = (columnId: string) => (ref: View) => {
    ref.measure((x, y, width, height, pageX, pageY) => {
      setColumnMeasurements((prev) =>
        new Map(prev).set(columnId, {
          x: pageX,
          y: pageY,
          width,
          height,
        })
      );
    });
  };

  const measureCard = (cardId: string) => (ref: View) => {
    ref.measure((x, y, width, height, pageX, pageY) => {
      setCardMeasurements((prev) =>
        new Map(prev).set(cardId, {
          x: pageX,
          y: pageY,
          width,
          height,
        })
      );
    });
  };

  const handleDragStart = (context: DragContext) => {
    setDragContext(context);
    console.log("Started dragging:", context);
  };

  const handleDragMove = (x: number, y: number) => {
    if (!dragContext) return;

    // Reset hoveredCard and hoveredColumn flags initially
    let isHoveringCard = false;
    let isHoveringColumn = false;

    // Handle column movement
    columnMeasurements.forEach((position, columnId) => {
      if (
        x >= position.x &&
        x <= position.x + position.width &&
        y >= position.y &&
        y <= position.y + position.height
      ) {
        isHoveringColumn = true; // Set flag if hovering over a column
        if (hoveredColumn !== columnId) {
          setHoveredColumn(columnId);
          console.log(`Hovering over column: ${columnId}`);
        }
      }
    });
    // If not hovering over any column, reset hoveredColumn
    if (!isHoveringColumn) {
      setHoveredColumn(null);
    }

    // Handle card movement
    cardMeasurements.forEach((position, cardId) => {
      if (
        cardId !== dragContext.cardId &&
        x >= position.x &&
        x <= position.x + position.width &&
        y >= position.y &&
        y <= position.y + position.height
      ) {
        isHoveringCard = true; // Set flag if hovering over a card
        if (hoveredCard !== cardId) {
          setHoveredCard(cardId);
          console.log(`Hovering over card: ${cardId}`);
        }
      }
    });
    // If not hovering over any card, reset hoveredCard
    if (!isHoveringCard) {
      setHoveredCard(null);
    }
  };

  function handleDragEnd() {
    if (!dragContext) return;

    // Check if the drag ended over a column
    if (dragContext.type === "card" && hoveredColumn) {
      setColumns((prevColumns) =>
        prevColumns.map((column) => {
          if (column.id === hoveredColumn) {
            let updatedCards = column.cards.filter(
              (card) => card.id !== dragContext.cardId
            );

            const targetIndex = hoveredCard
              ? updatedCards.findIndex((card) => card.id === hoveredCard)
              : updatedCards.length;

            updatedCards.splice(targetIndex, 0, {
              id: dragContext.cardId!,
              title: dragContext.cardTitle!,
            });

            return { ...column, cards: updatedCards };
          } else if (column.id === dragContext.columnId) {
            // Remove card from its original column
            return {
              ...column,
              cards: column.cards.filter(
                (card) => card.id !== dragContext.cardId
              ),
            };
          }
          return column;
        })
      );
    }
    //     // Reset states
    setDragContext(null);
    setHoveredColumn(null);
    setHoveredCard(null);
    return;
  }

  return (
    <GestureHandlerRootView style={styles.container}>
      <ScrollView horizontal style={styles.scrollView}>
        {columns.map((column) => (
          <Box
            key={column.id}
            id={column.id}
            activeBox={activeBox}
            setActiveBox={setActiveBox}
            style={[
              styles.columnBox,
              hoveredColumn === column.id && styles.columnBoxHovered,
            ]}
            isColumn={true}
            onDragStart={handleDragStart}
            onDragMove={handleDragMove}
            onDragEnd={handleDragEnd}
            measureRef={measureColumn(column.id)}
            dragHandleRef={columnHandleRefs.current.get(column.id)}
          >
            <Text style={styles.columnTitle}>{column.title}</Text>
            {column.cards.map((card) => (
              <Box
                key={card.id}
                id={card.id}
                title={card.title}
                activeBox={activeBox}
                setActiveBox={setActiveBox}
                columnId={column.id}
                style={[
                  styles.card,
                  hoveredCard === card.id && styles.cardHovered,
                ]}
                onDragStart={handleDragStart}
                onDragMove={handleDragMove}
                onDragEnd={handleDragEnd}
                measureRef={measureCard(card.id)}
                isDragging={dragContext?.cardId === card.id}
              >
                <Text style={styles.cardTitle}>{card.title}</Text>
              </Box>
            ))}
          </Box>
        ))}
      </ScrollView>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  scrollView: {
    flex: 1,
  },
  columnBox: {
    padding: 10,
    backgroundColor: "#e0e0e0e0",
    borderRadius: 8,
    width: 280,
    margin: 10,
    minHeight: 400,
  },
  columnBoxHovered: {
    backgroundColor: "#e0e0e0",
    borderColor: "#2196F3",
    borderWidth: 2,
  },
  columnTitle: {
    fontSize: 18,
    fontWeight: "600",
    marginBottom: 10,
    color: "#333",
  },
  card: {
    marginVertical: 5,
    padding: 15,
    backgroundColor: "white",
    borderRadius: 8,
    width: "100%",
    height: 100,
    shadowColor: "#000",
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.1,
    shadowRadius: 3,
    elevation: 3,
  },
  cardHovered: {
    backgroundColor: "#f8f8f8",
    borderColor: "#2196F3",
    borderWidth: 2,
  },
  cardTitle: {
    fontSize: 16,
    color: "#333",
  },
  dragHandle: {
    padding: 5,
    alignItems: "center",
    justifyContent: "center",
  },
  dragHandleText: {
    fontSize: 16,
    color: "#999",
  },
});

1 Answer 1

-1

Have you looked at https://github.com/react-grid-layout/react-grid-layout?

It supports drag and drop and also resizing on web with React, and I believe some people have gotten it to work well on mobile interfaces by adding a hold toggle to enable dragging vs scrolling.

I also have a web demo template here.

Hope this helps!

Sign up to request clarification or add additional context in comments.

1 Comment

The OP wanted a solution for React Native and web, not just web. Saying "mobile interfaces" isn't enough to answer if that means React Native or perhaps Ionic-React.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.