Типизация данных мутации с TypeScript
Мутации без типизации — это как отправлять посылку без наклейки с адресом. Вы никогда не знаете, куда она придёт или что пойдёт не так. TypeScript поможет нам минимизировать ошибки.
Интерфейсы для типизации мутаций
Для начала, определим интерфейсы, которые помогут нам структурировать данные. Например, если мы добавляем новую задачу, она будет содержать название title и описание description:
interface Task {
id: number;
title: string;
description: string;
}
interface CreateTaskInput {
title: string;
description: string;
}
Хук useMutation с Generics
Теперь, используя TypeScript, мы можем типизировать параметры и возвращаемые данные мутации:
import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
const createTask = async (task: CreateTaskInput): Promise<Task> => {
const response = await axios.post<Task>('/api/tasks', task);
return response.data;
};
const useCreateTaskMutation = () => {
return useMutation<Task, Error, CreateTaskInput>({
mutationFn: createTask,
});
};
Обратите внимание на Generics, которые принимает useMutation:
Task— тип данных, которые возвращает сервер (успешный результат).Error— тип возможной ошибки.CreateTaskInput— тип данных, передаваемых в мутацию.
Теперь наша мутация полностью типизирована, и TypeScript будет помогать нам на каждом шагу.
Обработка состояний мутации
Хук useMutation предоставляет удобные состояния "жизни" мутации:
isPending— мутация выполняется.isSuccess— мутация успешно завершена.isError— произошла ошибка.error— информация об ошибке.
Давайте реализуем пример добавления новой задачи с обработкой состояний.
Пример компонента
import React, { useState } from 'react';
import { useCreateTaskMutation } from './mutations'; // Помните пример выше?
const AddTaskForm: React.FC = () => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const {
mutate, // Функция для вызова мутации
isPending, // Состояние загрузки
isError, // Состояние ошибки
error, // Детали ошибки
isSuccess, // Успешное выполнение
} = useCreateTaskMutation();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutate(
{ title, description },
{
onSuccess: () => {
// Очистка формы после успешного создания
setTitle('');
setDescription('');
},
}
);
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Task title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<textarea
placeholder="Task description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<button type="submit" disabled={isPending}>
{isPending ? 'Adding...' : 'Add Task'}
</button>
</form>
{isError && <p style={{ color: 'red' }}>Error: {error?.message}</p>}
{isSuccess && <p style={{ color: 'green' }}>Task added successfully!</p>}
</div>
);
};
export default AddTaskForm;
Обратите внимание, как мы используем состояния isLoading, isError и isSuccess для улучшения UX (пользовательского опыта).
Обработка ошибок в мутациях
Ошибки — это неизбежная часть работы с сервером. React Query предоставляет несколько удобных способов обработки ошибок.
onError и возвращение кэшированных данных
Мы можем использовать опцию onError для выполнения действий при ошибке. Например, мы можем уведомить пользователя о проблеме или даже вернуть кэшированные данные (если они есть):
const { mutate } = useCreateTaskMutation({
onError: (error: Error) => {
alert(`Something went wrong: ${error.message}`);
},
});
Отмена изменений в случае ошибки
Для более сложных случаев можно использовать оптимистические обновления. Например, в случае ошибки изменения откатываются:
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
const deleteTask = async (taskId: number): Promise<void> => {
await axios.delete(`/api/tasks/${taskId}`);
};
export const useDeleteTaskMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteTask,
// Оптимистичное обновление при удалении задачи
onMutate: async (taskId: number) => {
await queryClient.cancelQueries({ queryKey: ['tasks'] });
const previousTasks = queryClient.getQueryData<Task[]>(['tasks']);
queryClient.setQueryData<Task[]>(['tasks'], (oldTasks) =>
oldTasks?.filter((task) => task.id !== taskId)
);
return { previousTasks };
},
// Откат в случае ошибки
onError: (_error, _taskId, context) => {
if (context?.previousTasks) {
queryClient.setQueryData(['tasks'], context.previousTasks);
}
},
// Повторный запрос задач после завершения мутации
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
});
};
Здесь:
onMutateиспользует оптимистическое удаление задачи из кэша.onErrorвосстанавливает предыдущие данные в случае ошибки.
Обработка сложных мутаций
Если необходимо отправить сложные данные (например, вложенные JSON-объекты), не забывайте о типизации. Используйте интерфейсы и обобщения TypeScript, чтобы избежать несоответствий:
import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
interface NestedTaskInput {
title: string;
description: string;
subtasks: {
title: string;
completed: boolean;
}[];
}
const createNestedTask = async (task: NestedTaskInput): Promise<Task> => {
const response = await axios.post<Task>('/api/nested-tasks', task);
return response.data;
};
export const useCreateNestedTaskMutation = () => {
return useMutation<Task, Error, NestedTaskInput>({
mutationFn: createNestedTask,
});
};
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ