Created on August 29, 2020 at 3:53 pm
Updated on October 30, 2020 at 4:00 pm

A Bad Way to Implement React-DnD

Today I will show you how to implement the React DnD library poorly.

Some notes about the below code. From the gif, you see an add and edit card feature. I took those bits of code out since they weren't relevant to the explanation. Also, my code is using Typescript, so I have some types and interfaces that I didn't copy into here. Finally, I'm using Redux in my application. Normally, for something this small, you don't need Redux, but I plan on building this app out more.

gif

As you can see here, the drag and drop technically works, but the list only corrects itself upon drop.

//calendar component
const CalendarTest: React.FC<any> = (props: any): any => {
  const { daysArray } = useSelector((state: any) => state.timeArrayState);

  return (
    <CalendarMain>
      <h1>Calendar</h1>
      <CalContainer>
        {daysArray.map((day: any) => (
          <OneDayTest
            key={day.date}
            dayOfWeek={day.dayOfWeek}
            date={day.date}
            daysArray={daysArray}
          />
        ))}
      </CalContainer>
    </CalendarMain>
  );
};

export default CalendarTest;
//drop component that contains the task list
const OneDayTest: React.FC<Props> = ({
date,
 dayOfWeek,
 tasks,
 daysArray
}): any => {
  const [arrayOfDays, setArrayOfDays] = useState(daysArray);

  const dispatch = useDispatch();

  useEffect(() => {
    setArrayOfDays(daysArray);
  }, [daysArray]);

  const findCard = id => {
    const tasksArray = arrayOfDays.filter(day => {
      const tasks = day.tasks;
      return !!tasks.filter(task => task.id === id)[0];
    })[0].tasks;

    const card = tasksArray.filter(task => task.id === id)[0];
    return card;
  };

  const moveCard = (atIndex, atDate, id) => {
    const card = findCard(id);
    const newDaysArray = daysArray.map(day => {
      const newTasks = day.tasks.filter(task => task.id !== id);
      if (day.date === atDate) {
        newTasks.splice(atIndex, 0, card);
      }
      const newDay = { ...day, tasks: newTasks };
      return newDay;
    });
    setArrayOfDays(newDaysArray);
  };

  const findIndex = id => {
    const day = daysArray.filter(day => {
      const tasks = day.tasks;
      return !!tasks.filter(task => task.id === id)[0];
    })[0];
    const index = day.tasks.findIndex(task => task.id === id);

    return {
      index: index,
      date: day.date
    };
  };

  const [, drop] = useDrop({
    accept: ItemTypes.CARD,
    drop: (item, monitor) => {
      dispatch(setDaysArray(arrayOfDays));
    }
  });

  return (
    <StyledCard ref={drop}>
      <h4>{dayOfWeek}</h4>
      <h4>{date}</h4>

      <h6>Tasks</h6>
      {arrayOfDays
        .filter(day => day.date === date)[0]
        .tasks.map((task: any, i) => {
          return (
            task.id && (
              <Card
                key={task.id}
                text={task.text}
                moveCard={moveCard}
                id={task.id}
                findIndex={findIndex}
              />
            )
          );
        })}
    </StyledCard>
  );
};
export default OneDayTest;


//card render, this is the draggable component
export const Card: React.FC<CardProps> = ({
id,
 text, date,
 moveCard,
 findIndex
}) => {

  const ref = useRef(null);


  const [, drag] = useDrag({
    item: { type: ItemTypes.CARD, id }
  });

  const [, drop] = useDrop({
    accept: ItemTypes.CARD,
    canDrop: () => false,
    hover: ({ id: draggedId }: DragItem) => {
      if (draggedId !== id) {
        const { index: overIndex, date: overDate } = findIndex(id);

        moveCard(overIndex, overDate, draggedId);
      }
    }
  });


  return (
    <div ref={node => drag(drop(node))} style={{ ...style }}>
      <span>{`${text} ${id}`}</span>

    </div>
  );
};
 

As you can see from the above code, my thinking was for each OneDayTest component to handle its own business logic and internal state. My thinking was, by doing this, only the necessary components that have a drag and drop action done upon it would change.

However, given that the moveCard function would only be called upon when a card was hover over another card, as soon as a card changed dates, the state that the card would move from stayed stale. The cards only updated after the dispatching an action to the store to update the entire array of days.

After moving the logic up to the calendar component, and not have the states update individually, I end up with the functionality I was looking for.

gif

const CalendarTest: React.FC = (): any => {
  const { daysArray } = useSelector(
    (state: any): TimeArrayState => state.timeArrayState
  );
  const dispatch = useDispatch();

  const findCard = (id: string): TaskObj => {
    ...
  };

  const findIndex = (id: string): { index: number; date: string } => {
    ...
  };

  const moveCard = (atIndex: number, atDate: string, id: string): void => {
    ...
  };

  return (
       ...
          
         {daysArray.map((day: any) => (
          <OneDayTest
            key={day.date}
            dayOfWeek={day.dayOfWeek}
            date={day.date}
            tasks={day.tasks}
            moveCard={moveCard}
            findIndex={findIndex}
          />
        ...  
   );
};

From the above code, the moveCard, findIndex, and findCard functions are moved into the Calendar component, and they are being passed down to the Card component through the OneDay component.

There is still an issue with the drag functionality. For whatever reason, the isDragging prop created from the drag component doesn't register across the dates. I think wrapping the card in a DragWrapper and rendering it as a child of OneDayTest might fix that issue.

Also going forward, I plan on changing the data structure to have two separate buckets. One bucket will be the array of days, the second will be an array of tasks. I'll end up just cross referencing the tasks with the days. Currently, each task doesn't know which day it belongs to.

Stay tuned for updates, and thanks for reading!