@tanstack/react-query + react typescript

от автора

Хотелось бы рассказать, как я использую @tanstack/react-query в своих проектах при построении архитектуры приложения.

Все приложения, которые в той или иной мере имеют связь с сервером требуют выполнение стандартного набора действий:

1. Загружать данные;
2. Хранить эти данные;
3. Информировать о том что идет загрузка;
4. Информировать о том что произошла ошибка;

Давайте создадим базовый набор компонентов, методов, типов для построения такого приложения.

Инфраструктура

Будем считать, что у нашего приложения есть backend, и для нас он предоставляет следующие REST ручки.

  1. Получение списка записей GET /list

  2. Добавление нового элемента в список записей POST /list

  3. Удаление элемента из списка записей DELETE /list/{id}

  4. Редактирование элемента 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;

Итог

Мы построили базовое приложение, которое умеет загружать данные, информировать о статусе загрузки, ошибки и рисует загруженные данные.

Умеет их редактировать, создавать, удалять.

Без написания костылей для хранения данных и состояний этих данных.

Буду рад любому фидбэку, и жду вас для обсуждения в комментариях.


ссылка на оригинал статьи https://habr.com/ru/articles/843340/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *