Project Kanban
Board Slice: Серце Дошки
Ми вирішили тримати стан дошки локально для миттєвих оновлень. Давайте створимо boardSlice.
Board Slice: Серце Дошки
Ми вирішили тримати стан дошки локально для миттєвих оновлень. Давайте створимо boardSlice.
Чому один слайс, а не окремі tasksSlice та columnsSlice?
Тому що операція Drag & Drop часто зачіпає обидві сутності одночасно (видалення ID з однієї колонки і додавання в іншу). Робити це в одному редюсері набагато простіше і гарантує цілісність даних (Atomicity).
Початковий стан (Mock Data)
Поки що ми захардкодимо дані, щоб протестувати UI.
src/features/board/boardSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { BoardData } from '../../types/kanban';
const initialState: BoardData = {
tasks: {
'task-1': { id: 'task-1', content: 'Налаштувати проєкт', createdAt: '2023-10-01' },
'task-2': { id: 'task-2', content: 'Вивчити RTK Query', createdAt: '2023-10-02' },
'task-3': { id: 'task-3', content: 'Зробити каву', createdAt: '2023-10-03' },
},
columns: {
'column-1': {
id: 'column-1',
title: 'To Do',
taskIds: ['task-1', 'task-2', 'task-3'],
},
'column-2': {
id: 'column-2',
title: 'In Progress',
taskIds: [],
},
'column-3': {
id: 'column-3',
title: 'Done',
taskIds: [],
},
},
columnOrder: ['column-1', 'column-2', 'column-3'],
};
export const boardSlice = createSlice({
name: 'board',
initialState,
reducers: {
// Тут буде наша магія переміщення
},
});
export default boardSlice.reducer;
Додавання в Store
Підключіть цей редюсер у src/app/store.ts.
src/app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import boardReducer from '../features/board/boardSlice';
export const store = configureStore({
reducer: {
board: boardReducer,
},
});
// ...
Відображення даних
Тепер створимо компоненти для відображення. Нам потрібні:
Board: Контейнер.Column: Колонка.Task: Картка завдання.
Створимо простий каркас без DnD, щоб переконатися, що Redux працює.
src/features/board/Board.tsx
import React from 'react';
import { useAppSelector } from '../../app/hooks';
import { Column } from './Column';
export const Board: React.FC = () => {
const columnOrder = useAppSelector((state) => state.board.columnOrder);
const columns = useAppSelector((state) => state.board.columns);
return (
<div className="flex h-full gap-4 p-4 overflow-x-auto bg-blue-500">
{columnOrder.map((columnId) => {
const column = columns[columnId];
return <Column key={column.id} column={column} />;
})}
</div>
);
};
src/features/board/Column.tsx
import React from 'react';
import { Column as ColumnType } from '../../types/kanban';
import { Task } from './Task';
import { useAppSelector } from '../../app/hooks';
interface Props {
column: ColumnType;
}
export const Column: React.FC<Props> = ({ column }) => {
// Важливо: ми дістаємо задачі тут, використовуючи selector
// Це дозволяє уникнути зайвих ререндерів батьківського Board
const tasks = useAppSelector((state) =>
column.taskIds.map(taskId => state.board.tasks[taskId])
);
return (
<div className="w-72 bg-gray-100 rounded-md p-2 flex flex-col shrink-0">
<h3 className="font-bold mb-2 p-2">{column.title}</h3>
<div className="flex flex-col gap-2 min-h-[100px]">
{tasks.map((task) => (
<Task key={task.id} task={task} />
))}
</div>
</div>
);
};
src/features/board/Task.tsx
import React from 'react';
import { Task as TaskType } from '../../types/kanban';
interface Props {
task: TaskType;
}
export const Task: React.FC<Props> = ({ task }) => {
return (
<div className="bg-white p-2 rounded shadow-sm border border-gray-200">
{task.content}
</div>
);
};
Тепер, коли ми бачимо наші колонки, настав час додати логіку переміщення. Це буде найскладніший, але найцікавіший редюсер.