Project Kanban

Логіка Drag & Drop

Це "м'ясо" нашого проєкту. Ми реалізуємо складну логіку переміщення карток, яка виглядає простою для користувача.

Логіка Drag & Drop

Це "м'ясо" нашого проєкту. Ми реалізуємо складну логіку переміщення карток, яка виглядає простою для користувача.

React Beautiful DnD

Бібліотека працює за допомогою трьох компонентів:

  1. <DragDropContext>: Обгортка всього додатку. Приймає onDragEnd.
  2. <Droppable>: Зона, куди можна кидати (наша Колонка).
  3. <Draggable>: Елемент, який можна тягати (наше Завдання).

Але нас цікавить Redux. Як ми будемо оновлювати стан?

Action Definition

Нам потрібен лише один action, який описує переміщення.

// Payload, який приходить від react-beautiful-dnd
interface MoveTaskPayload {
  source: {
    droppableId: string; // ID колонки "звідки"
    index: number;       // Індекс "звідки"
  };
  destination: {
    droppableId: string; // ID колонки "куди"
    index: number;       // Індекс "куди"
  };
  draggableId: string;   // ID завдання
}

Reducer Implementation

Відкриємо boardSlice.ts і додамо логіку. Завдяки Immer, це буде виглядати як магія.

src/features/board/boardSlice.ts
// ... imports

export const boardSlice = createSlice({
  name: 'board',
  initialState,
  reducers: {
    moveTask: (state, action: PayloadAction<MoveTaskPayload>) => {
      const { source, destination, draggableId } = action.payload;

      // 1. Отримуємо колонки (Immer дозволяє звертатися напряму)
      const startColumn = state.columns[source.droppableId];
      const finishColumn = state.columns[destination.droppableId];

      // Сценарій 1: Переміщення в тій самій колонці
      if (startColumn === finishColumn) {
        const newTaskIds = Array.from(startColumn.taskIds);
        
        // Видаляємо зі старої позиції
        newTaskIds.splice(source.index, 1);
        // Вставляємо в нову позицію
        newTaskIds.splice(destination.index, 0, draggableId);

        // Присвоюємо (Immer це обробить)
        startColumn.taskIds = newTaskIds;
        return;
      }

      // Сценарій 2: Переміщення між колонками
      // Видаляємо з початкової
      startColumn.taskIds.splice(source.index, 1);
      
      // Додаємо в кінцеву
      finishColumn.taskIds.splice(destination.index, 0, draggableId);
    },
  },
});

export const { moveTask } = boardSlice.actions;

Підключення в React

Тепер нам треба викликати цей action в onDragEnd.

src/features/board/Board.tsx
import { DragDropContext, DropResult } from '@hello-pangea/dnd';
import { useAppDispatch } from '../../app/hooks';
import { moveTask } from './boardSlice';

export const Board: React.FC = () => {
  const dispatch = useAppDispatch();
  // ... selectors

  const onDragEnd = (result: DropResult) => {
    const { destination, source, draggableId } = result;

    // Якщо кинули "в нікуди"
    if (!destination) return;

    // Якщо кинули туди ж, де взяли
    if (
      destination.droppableId === source.droppableId &&
      destination.index === source.index
    ) {
      return;
    }

    // Діспатчимо action!
    dispatch(moveTask({
      source,
      destination,
      draggableId
    }));
  };

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      {/* ... render columns */}
    </DragDropContext>
  );
};
Ми опустили деталі розмітки <Droppable> та <Draggable> в компонентах Column та Task, щоб сфокусуватися на логіці даних. В реальному коді вам потрібно обгорнути списки в ці компоненти.

Чому це круто?

Ми щойно реалізували складну логіку зміни порядку в масивах за 10 рядків коду завдяки Immer (splice). В класичному Redux нам довелося б робити глибоке копіювання масивів taskIds, що зайняло б разів у 3 більше коду.

Але це лише локальний стан. Якщо ми перезавантажимо сторінку, зміни зникнуть. Нам потрібно зберігати це на сервері.

👉 Далі: Інтеграція з RTK Query

Copyright © 2026