@tanstack-react-query + react typescript

в 12:16, , рубрики: @tanstack/react-query, FrontEnd Development, frontend react, frontend-разработка, React, react query, react.js, 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;

Итог

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

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

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

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

Автор: eyes_my_eyes

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js