Search code examples
reactjsreact-dnd

React Card Example issue - Card is replaced instead of being appended to a list of cards in another column


I have been persistently working on this problem where the goal is to drag a card form 'Column 1' and copy that into another column say 'Column 2'. Now when my first card is dragged and drop it into 'Column 2, the card is accordingly added to that column, but when I drag another card and drop into 'Column 2' instead of being appended it just replaces the existing card with itself. I have been debugging the state, but the issue still persists. I haven't gotten a clue what am I doing wrong here?

Here's my code

// Card Component
function Card({ id, text, isDrag }) {
  const [, drag] = useDrag(() => ({
    type: "bp-card",
    item: () => {
      return { id,  text}
    },
    collect: monitor => ({
      isDragging: !!monitor.isDragging(),
    }),
    canDrag: () => isDrag
  }));

  return (
    <div
      className='card'
      ref={drag}
      style={{
        cursor: isDrag ? 'pointer' : 'no-drop'
      }}
    >
      {text}
    </div>
  )
}

// Column Component
function Column({ title, children, onCardDropped }) {
  const [, drop] = useDrop(() => ({
    accept: "bp-card",
    drop: item => {
      onCardDropped(item);
    }
  }));

  return (
    <div className="flex-item" ref={title === 'Column 2' ? drop : null}>
      <p>{title}</p>

      {children.length > 0 && children.map(({ id, text, isDrag }) => (
        <Card
          key={id}
          id={id}
          text={text}
          isDrag={isDrag}
        />
      ))}
    </div>
  )
}

// Main App
function App() {
  const [cards] = useState([
    { id: 1, text: 'Card 1', isDrag: true },
    { id: 2, text: 'Card 2', isDrag: true },
  ]);

  const [columns, setColumns] = useState([
    {
      id: 1,
      title: 'Column 1',
      children: cards
    },
    {
      id: 2,
      title: 'Column 2',
      children: []
    },
  ]);

  const onCardDropped = ({ id, text }) => {
    // let card = null;
    const targetColumnId = 2;

    const transformedColumns = columns.map(column => {
      if (column.id === targetColumnId) {
        return {
          ...column,
          children: [
            ...column.children,
            { id, text }
          ]
        }
      }
      return column;
    });

    setColumns(transformedColumns);
  }

  return (
    <DndProvider backend={HTML5Backend}>
      <div className='flex-container'>
        {columns.map((column) => (
          <Column
            key={column.id}
            title={column.title}
            children={column.children}
            onCardDropped={onCardDropped}
          />
        ))}
      </div>
    </DndProvider>
  );
}

Any help is highly appreciated. Thanks.


Solution

  • You need to consider the previous state using the callback of the set state method. It starts to work after changing the onCardDropped as below.

      const onCardDropped = ({ id, text }) => {
        // let card = null;
        const targetColumnId = 2;
    
        setColumns((prevColumns) =>
          prevColumns.map((column) => {
            if (column.id === targetColumnId) {
              return {
                ...column,
                children: [...column.children, { id, text }]
              };
            }
            return column;
          })
        );
      };
    

    It's always a good idea to use the state from the callback method as opposed to using the state object directly which might be stale.

    Working Demo

    Edit cool-dew-8zl3s9