@tanstack/react-query + react typescript
Хотелось бы рассказать, как я использую @tanstack/react-query
в своих проектах при построении архитектуры приложения.
Все приложения, которые в той или иной мере имеют связь с сервером требуют выполнение стандартного набора действий:
1. Загружать данные;
2. Хранить эти данные;
3. Информировать о том что идет загрузка;
4. Информировать о том что произошла ошибка;
Давайте создадим базовый набор компонентов, методов, типов для построения такого приложения.
Инфраструктура
Будем считать, что у нашего приложения есть backend, и для нас он предоставляет следующие REST ручки.
Получение списка записей GET /list
Добавление нового элемента в список записей POST /list
Удаление элемента из списка записей DELETE /list/{id}
Редактирование элемента PATCH /list/{id}
Для запросов мы будем использовать axios. https://axios-http.com
Создамим базовый набор сущностей в нашем приложении
Объявляем типы
/** Элемент списка */
export type TListItemDto = {
/** Уникальный идентификатор */
id: number;
/** Наименование для отображения в интерфейсе */
name: string;
/** Содержимое элемента */
content: string;
}
/** Список элементов */
export type TListResponseData = Array;
Создаем Http сервис
export const queryClient = new QueryClient();
function useListHttp() {
const client = axios.create();
const get = () => client
.get('/list')
.then(response => response.data);
const add = (payload: Omit) => client
.post('/list', payload)
.then(response => response.data);
const remove = (id: TListItemDto['id']) => client
.delete(`/list/${id}`);
const update = ({id, payload}: { id: TListItemDto['id'], payload: Omit }) => client
.patch(`/list/${id}`, payload)
.then(response => response.data);
return { get, add, remove, update};
}
Описываем хуки для работы с данными на основе @tanstack/react-query
/** Метод будет возвращать ключи для query и mutatuion, не обязателен, можно обойтись без него */
const getKey = (key, type: 'MUTATION' | 'QUERY') => `LIST_${key}__${type}`;
/** Список ключей */
const KEYS = {
get: getKey('GET', 'QUERY'),
add: getKey('ADD', 'MUTATION'),
remove: getKey('REMOVE', 'MUTATION'),
update: getKey('UPDATE', 'MUTATION'),
}
/** Получение списка */
export function useListGet() {
const { get } = useListHttp();
return useQuery({
queryKey: [KEYS.get],
queryFn: get,
enabled: true,
initialData: [],
});
}
/** Добавление в список */
export function useListAdd() {
const http = useListHttp();
return useMutation({
mutationKey: [KEYS.add],
mutationFn: http.add,
onSuccess: (newItem) => {
/* После успешного создания нового элемента, обновляем список ранее загруженных добавленяя в него новой сущности без запроса к api */
queryClient.setQueryData(
[KEYS.get],
(prev: TListResponseData) => [...prev, newItem]
);
},
});
}
/** Удаление из списка */
export function useListRemove() {
const { remove } = useListHttp();
return useMutation({
mutationKey: [KEYS.remove],
mutationFn: remove,
onSuccess: (_, variables: TListItemDto['id']) => {
/* После успешного создания нового элемента, обновляем список ранее загруженных очищая из него удаленноую сущность без запроса к api */
queryClient.setQueryData(
[KEYS.get],
(prev: TListResponseData) => prev.filter(item => item.id !== variables)
);
},
});
}
/** Обновить элемент в списке */
export function useListUpdate() {
const { update } = useListHttp();
return useMutation({
mutationKey: [KEYS.update],
mutationFn: update,
onSuccess: (response, variables: { id: TListItemDto['id'], payload: Omit }) => {
/* После успешного создания нового элемента, обновляем список элементов путем очистки из него удаленной сущности без запроса к api */
queryClient.setQueryData(
[KEYS.get],
(prev: TListResponseData) => prev.map(item => item.id === variables.id ? response : item)
);
},
});
}
Теперь переходим к компонентам
Будем считать что наше приложение вполне типичное и имеет следующую структуру
Схематическое описание структуры компонентов (я автор, я так вижу)
При нажатии на компонент мы будем отрисовывать форму редактирования, если ни один ListItem не выбран, форма будет работать на создание.
Общие компоненты используемые во всем прилежении
function ErrorMessage() {
return 'В процессе загрузки данных произошла ошибка';
}
function PendingMessage() {
return 'Загрузка...';
}
Теперь перейдем к основным компонентам
function List() {
const id = useId();
const { data, isFetching, isError } = useListGet();
const listRemove = useListRemove();
const handleEdit = (item: TListItemDto) => {
// ... go to edit mode
}
const handleRemove = (itemId: TListItemDto['id']) => {
listRemove.mutate(itemId);
}
if (isError) return ;
if (isFetching) return ;
return data.map((item: TListItemDto) => (
handleEdit(item)}>
id: {item.id}
name: {item.name}
content: {item.content}
));
}
export default List;
export type TListItemFormProps = {
item?: TListItemDto
}
function ListItemForm({ item }: TListItemProps) {
const listUpdate = useListUpdate();
const listAdd = useListAdd();
const [name, setName] = useState(item?.name ?? '');
const [content, setContent] = useState(item?.content ?? '');
const isEditMode = item === null;
const isPending = listAdd.isPending || listUpdate.isPending;
const handleSubmit = () => {
if (item) {
listUpdate.mutate({
id: item.id,
payload: { name, content }
});
} else {
listAdd.mutate({ name, content });
}
}
if (isPending) return ;
return (
{isEditMode ? 'Редактирование' : 'Создание'}
);
}
export default ListItemForm;
Итог
Мы построили базовое приложение, которое умеет загружать данные, информировать о статусе загрузки, ошибки и рисует загруженные данные.
Умеет их редактировать, создавать, удалять.
Без написания костылей для хранения данных и состояний этих данных.
Буду рад любому фидбэку, и жду вас для обсуждения в комментариях.