@tanstack

I would like to tell you how I use @tanstack/react-query in their projects when building the application architecture.

All applications that have some connection to the server require a standard set of actions:

1. Upload data;
2. Store this data;
3. Inform that loading is in progress;
4. Inform that an error has occurred;

Let's create a basic set of components, methods, types to build such an application.

Infrastructure

Let's assume that our application has a backend, and it provides the following REST handles for us.

  1. Getting a list of records GET /list

  2. Adding a new item to the POST record list /list

  3. Deleting an item from a list of records DELETE /list/{id}

  4. Editing a PATCH Element /list/{id}

For queries we will use axios. https://axios-http.com

Let's create a basic set of entities in our application

Declaring types

/** Элемент списка */
export type TListItemDto = {
    /** Уникальный идентификатор */
    id: number;
    /** Наименование для отображения в интерфейсе */
    name: string;
    /** Содержимое элемента */
    content: string;
}

/** Список элементов */
export type TListResponseData = Array<TListItemDto>;

Create Http service

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

We describe hooks for working with data based on @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)
            );
        },
    });
}

Now let's move on to the components.

Let's assume that our application is quite typical and has the following structure

Schematic description of the structure of components (I am the author, this is how I see it)

Schematic description of the structure of components (I am the author, this is how I see it)

When clicking on the component, we will draw the editing form, if no ListItem is selected, the form will work on creation.

Common components used throughout the entire endeavor

function ErrorMessage() {
    return 'В процессе загрузки данных произошла ошибка';
}

function PendingMessage() {
    return 'Загрузка...';
}

Now let's move on to the main components.

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;

Summary

We have built a basic application that can download data, provide information about the download status, errors, and draw the downloaded data.

Can edit, create, and delete them.

Without writing crutches for storing data and the states of this data.

I would appreciate any feedback and look forward to your discussion in the comments.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *