@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;

Создаем 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 ? 'Редактирование' : 'Создание'}

setName(event.target.value)} /> setContent(event.target.value)} />
); } export default ListItemForm;

Итог

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

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

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

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

© Habrahabr.ru