Хотелось бы рассказать, как я использую @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<TListItemDto>;
Создаем Http сервис
export const queryClient = new QueryClient();
function useListHttp() {
const client = axios.create();
const get = () => client
.get<TListResponseData>('/list')
.then(response => response.data);
const add = (payload: Omit<TListItemDto, 'id'>) => client
.post<TListItemDto>('/list', payload)
.then(response => response.data);
const remove = (id: TListItemDto['id']) => client
.delete<void>(`/list/${id}`);
const update = ({id, payload}: { id: TListItemDto['id'], payload: Omit<TListItemDto, 'id'> }) => client
.patch<TListItemDto>(`/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<TListItemDto, 'id'> }) => {
/* После успешного создания нового элемента, обновляем список элементов путем очистки из него удаленной сущности без запроса к 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 <ErrorMessage />;
if (isFetching) return <PendingMessage />;
return data.map((item: TListItemDto) => (
<div key={`${id}_${item.id}`} onClick={() => handleEdit(item)}>
<div>id: {item.id}</div>
<div>name: {item.name}</div>
<div>content: {item.content}</div>
<button onClick={() => handleRemove(item.id)}>
{/* Если удаляется текущий элемент, отображаем информацию о процессе улаоения */}
{listRemove.isPending && listRemove.variables === item.id ? 'Удаление' : 'Удалить'}
</button>
</div>
));
}
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 <PendingMessage />;
return (
<Fragment>
<h1>{isEditMode ? 'Редактирование' : 'Создание'}</h1>
<form onSubmit={handleSubmit}>
<input type="text"
placeholder={'name'}
value={name}
onChange={(event) => setName(event.target.value)} />
<input type="text"
placeholder={'content'}
value={content}
onChange={(event) => setContent(event.target.value)} />
<button type='submit' disabled={isPending}>
{isPending ? 'Идет сохранение...' : 'Сохранить'}
</button>
</form>
</Fragment>
);
}
export default ListItemForm;
Итог
Мы построили базовое приложение, которое умеет загружать данные, информировать о статусе загрузки, ошибки и рисует загруженные данные.
Умеет их редактировать, создавать, удалять.
Без написания костылей для хранения данных и состояний этих данных.
Буду рад любому фидбэку, и жду вас для обсуждения в комментариях.
Автор: eyes_my_eyes