@tanstack/react-query + react typescript
- воскресенье, 15 сентября 2024 г. в 00:00:05
Хотелось бы рассказать, как я использую @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>;
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};
}
/** Метод будет возвращать ключи для 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;
Мы построили базовое приложение, которое умеет загружать данные, информировать о статусе загрузки, ошибки и рисует загруженные данные.
Умеет их редактировать, создавать, удалять.
Без написания костылей для хранения данных и состояний этих данных.
Буду рад любому фидбэку, и жду вас для обсуждения в комментариях.