Project Kanban
Логіка Drag & Drop
Це "м'ясо" нашого проєкту. Ми реалізуємо складну логіку переміщення карток, яка виглядає простою для користувача.
Логіка Drag & Drop
Це "м'ясо" нашого проєкту. Ми реалізуємо складну логіку переміщення карток, яка виглядає простою для користувача.
React Beautiful DnD
Бібліотека працює за допомогою трьох компонентів:
<DragDropContext>: Обгортка всього додатку. ПриймаєonDragEnd.<Droppable>: Зона, куди можна кидати (наша Колонка).<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 більше коду.
Але це лише локальний стан. Якщо ми перезавантажимо сторінку, зміни зникнуть. Нам потрібно зберігати це на сервері.