Інтеграція з RTK Query
Інтеграція з RTK Query
Тепер зробимо наш додаток "живим". Ми будемо завантажувати початковий стан з сервера і відправляти зміни назад.
Створення API Slice
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.
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 (Оптимістичні оновлення).
Логіка Drag & Drop
Це "м'ясо" нашого проєкту. Ми реалізуємо складну логіку переміщення карток, яка виглядає простою для користувача.
Optimistic Updates
Оптимістичне оновлення — це стратегія, коли ми оновлюємо інтерфейс так, ніби запит на сервер вже пройшов успішно. Це створює відчуття миттєвої реакції.