Project Kanban

Інтеграція з RTK Query

Тепер зробимо наш додаток "живим". Ми будемо завантажувати початковий стан з сервера і відправляти зміни назад.

Інтеграція з RTK Query

Тепер зробимо наш додаток "живим". Ми будемо завантажувати початковий стан з сервера і відправляти зміни назад.

Створення API Slice

src/features/api/apiSlice.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { BoardData } from '../../types/kanban';

export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Board'],
  endpoints: (builder) => ({
    getBoard: builder.query<BoardData, void>({
      query: () => 'board',
      providesTags: ['Board'],
    }),
    updateBoard: builder.mutation<void, BoardData>({
      query: (board) => ({
        url: 'board',
        method: 'POST', // або PUT/PATCH
        body: board,
      }),
      // Ми НЕ інвалідуємо тег 'Board' тут навмисно!
      // Чому? Читайте нижче про Optimistic Updates.
    }),
  }),
});

export const { useGetBoardQuery, useUpdateBoardMutation } = apiSlice;

Синхронізація (Hydration)

У нас є проблема: дані живуть у двох місцях. В кеші RTK Query і в нашому локальному boardSlice. Нам треба їх синхронізувати.

Модифікуємо Board.tsx.

src/features/board/Board.tsx
import { useEffect } from 'react';
import { useGetBoardQuery, useUpdateBoardMutation } from '../../features/api/apiSlice';
import { setBoard } from './boardSlice'; // Треба додати цей action

export const Board: React.FC = () => {
  const { data, isLoading } = useGetBoardQuery();
  const [updateBoard] = useUpdateBoardMutation();
  const dispatch = useAppDispatch();
  
  // Поточний стан дошки (локальний)
  const board = useAppSelector(state => state.board);

  // 1. При першому завантаженні копіюємо дані в локальний слайс
  useEffect(() => {
    if (data) {
      dispatch(setBoard(data));
    }
  }, [data, dispatch]);

  const onDragEnd = (result: DropResult) => {
    // ... логіка перевірок ...

    // 2. Оновлюємо ЛОКАЛЬНИЙ стан (миттєво)
    dispatch(moveTask({ source, destination, draggableId }));

    // 3. Відправляємо оновлений стан на СЕРВЕР
    // Оскільки moveTask синхронний, state.board вже оновлений
    // Але тут є нюанс: у React state updates можуть бути батчинг.
    // Тому краще брати актуальний стейт з store.getState() або передавати обчислені дані.
    
    // Для простоти прикладу, ми викликаємо мутацію з невеликою затримкою або використовуємо middleware listener.
    // Але найпростіше: просто відправити "сигнал" серверу, що ми перемістили таску.
    // Або відправити весь борд.
    
    // updateBoard(обчислений_новий_борд);
  };
  
  if (isLoading) return <div>Loading...</div>;

  return ( ... );
};
Чекати useEffect, щоб записати дані в Redux — це не ідеальний патерн (це викликає подвійний рендер). Кращим рішенням є використання extraReducers в слайсі, щоб слухати apiSlice.endpoints.getBoard.matchFulfilled.

Покращена синхронізація (extraReducers)

Давайте перепишемо boardSlice.ts, щоб він автоматично ловив дані з API.

import { apiSlice } from '../api/apiSlice';

// ...

extraReducers: (builder) => {
  builder.addMatcher(
    apiSlice.endpoints.getBoard.matchFulfilled,
    (state, action) => {
      // Коли запит успішний, ми замінюємо локальний стан даними з сервера
      return action.payload;
    }
  );
},

Тепер нам не потрібен useEffect в компоненті! Як тільки useGetBoardQuery отримає дані, вони автоматично потраплять в boardSlice.

Проблема "Мерехтіння"

Якщо ми просто відправимо запит на сервер, а потім сервер поверне нові дані, і ми знову оновимо стор... користувач може побачити "стрибок" картки назад і вперед, якщо інтернет повільний.

Щоб цього уникнути, використовуються Optimistic Updates (Оптимістичні оновлення).

👉 Далі: Optimistic Updates

Copyright © 2026