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,
  },
});
// ...

Відображення даних

Тепер створимо компоненти для відображення. Нам потрібні:

  1. Board: Контейнер.
  2. Column: Колонка.
  3. 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>
  );
};

Тепер, коли ми бачимо наші колонки, настав час додати логіку переміщення. Це буде найскладніший, але найцікавіший редюсер.

👉 Далі: Логіка Drag & Drop

Copyright © 2026