Skip to content

MutationStore

MutationStore выполняет изменяющий HTTP-запрос и даёт полный lifecycle: onMutate, onSuccess, onError, onSettled и invalidateQueries.

Обычно это POST, PUT, PATCH или DELETE. Создание: new MutationStore(...). Публичной функции createMutationStore нет.

RequestStore или MutationStore

Оба API умеют вызвать любой HTTP-метод. Выбор определяется не методом, а поведением после запроса:

СитуацияВыбор
Login, registration, export, validate, простой submitRequestStore
Изменение server state с callbacks и invalidation кэшаMutationStore
Нужен rollback/optimistic contextMutationStore

API-функция

ts
import type { AxiosRequestConfig, AxiosResponse } from "axios"

import { authApi } from "../../../api"

export interface UpdateUserRequestDto {
    name: string
}

export interface User {
    id: string
    name: string
    email: string
}

// API-функция описывает PATCH конкретного пользователя.
export const updateUserApi = (
    userId: string,
    dto: UpdateUserRequestDto,
    config?: AxiosRequestConfig,
): Promise<AxiosResponse<User>> => {
    return authApi.patch<User>(`/api/v1/users/${userId}`, dto, config)
}

Domain store

ts
import { makeAutoObservable } from "mobx"

import { MutationStore, type INormalizedAxiosError } from "@dubium/query-layer"
import type { THandlerStatus } from "@dubium/query-layer/contracts"

import { updateUserApi, type UpdateUserRequestDto, type User } from "../api/update-user.api"

export type UpdateUserFormValues = UpdateUserRequestDto

export class UpdateUserStore {
    // Tuple [string, UpdateUserRequestDto] повторяет аргументы API-функции
    // до последнего AxiosRequestConfig.
    private readonly requestHandler = new MutationStore<User, [string, UpdateUserRequestDto]>({
        // Mutation key используется в diagnostics и participant permissions.
        mutationKey: "users.update",

        // variables будут переданы в request в том же порядке.
        request: (userId, dto, config) => updateUserApi(userId, dto, config),

        options: {
            // Изменяющий запрос не повторяем без подтверждённой идемпотентности.
            retry: false,

            // Callback получает уже распакованные data и полный response.
            onSuccess: (data) => {
                console.info(`Пользователь ${data.id} обновлён`)
            },

            // Callback получает нормализованную ошибку.
            onError: (error) => {
                console.error("Не удалось обновить пользователя", error.message)
            },
        },
    })

    constructor() {
        makeAutoObservable<this, "requestHandler">(
            this,
            {
                // MutationStore уже является MobX-store.
                requestHandler: false,
            },
            {
                autoBind: true,
            },
        )
    }

    get loading(): boolean {
        return this.requestHandler.loading
    }

    get status(): THandlerStatus | null {
        return this.requestHandler.status
    }

    get error(): INormalizedAxiosError | null {
        return this.requestHandler.error
    }

    get updatedUser(): User | null {
        return this.requestHandler.data ?? null
    }

    async save(userId: string, values: UpdateUserFormValues): Promise<User | null> {
        // userId обычно приходит из route params.
        // values приходят из React-формы.
        // execute передаст оба аргумента в request(userId, values, config).
        const response = await this.requestHandler.execute(userId, values)

        return response?.data ?? null
    }

    cancel(): void {
        this.requestHandler.cancel()
    }

    reset(): void {
        this.requestHandler.reset()
    }

    dispose(): void {
        this.requestHandler.dispose()
    }
}

Как передаются values

Тип TVariables у MutationStore — tuple аргументов:

ts
;[string, UpdateUserRequestDto]

Поэтому цепочка выглядит так:

ts
store.save(userId, values)
// requestHandler.execute(userId, values)
// request(userId, values, { signal })
// updateUserApi(userId, values, { signal })

AxiosRequestConfig вручную передавать не нужно. Store создаёт его и добавляет signal. Для заголовка или timeout конкретного вызова используйте executeWithConfig([userId, values], config).

Как обновить связанное чтение

Прямые standalone stores без QueryRuntime владеют изолированными clients. Поэтому после успешного save() вызывайте предметный метод чтения явно:

ts
const user = await updateUserStore.save(userId, values)

if (user) {
    await userDetailsStore.refresh()
    await usersListStore.refresh()
}

В runtime/scope-сценарии используйте scoped invalidation, которая понимает логические ключи participant:

ts
onSuccess: () => {
    void scope.query.invalidate({ queryKey: ["users", "list"] }, { refetchActive: true })
}

Использование в React

ts
import { useEffect, useState } from "react"
import { observer } from "mobx-react-lite"

import { UpdateUserStore } from "../../data/users/update/model/update-user.store"

interface EditUserFormProps {
    userId: string
    initialName: string
}

export const EditUserForm = observer((props: EditUserFormProps) => {
    // Один MutationStore на lifecycle формы.
    const [store] = useState(() => new UpdateUserStore())
    const [name, setName] = useState(props.initialName)

    useEffect(() => () => store.dispose(), [store])

    return (
        <form
            onSubmit={(event) => {
                event.preventDefault()

                // Компонент передаёт route id и значения формы domain store.
                void store.save(props.userId, { name })
            }}
        >
            <input aria-label="Имя" value={name} onChange={(event) => setName(event.target.value)} />

            <button disabled={store.loading} type="submit">
                {store.loading ? "Сохраняем…" : "Сохранить"}
            </button>

            {store.error && <p role="alert">{store.error.message}</p>}
            {store.updatedUser && <p>Сохранено: {store.updatedUser.name}</p>}
        </form>
    )
})

Конфигурация MutationStore

ПолеTypeОбязательноеЧто делает
request(...variables, config?) => Promise<AxiosResponse<TData>>даAPI-функция изменяющего запроса.
mutationKeystringнетСтабильный логический ключ для diagnostics и MF permissions.
optionsMutationStore optionsнетLifecycle, retry, invalidation и network policy.
clientQueryClientнетОбщий query/mutation client. Обычно назначается runtime.
operationContextIRequestOperationContextнетВнутренний owner/correlation context; participant scope добавляет его в diagnostics mutation attempts.

Рабочие поля options в mutation mode

OptionTypeЧто делает
onMutate(variables) => TContext | Promise<TContext>Вызывается перед HTTP-запросом и может вернуть context для следующих callbacks.
onSuccess(data, response, variables, context) => void | Promise<void>Вызывается после успешного ответа и после invalidateQueries.
onError(error, variables, context) => void | Promise<void>Вызывается после окончательной ошибки.
onSettled(data, error, variables, context) => void | Promise<void>Вызывается после success/error; при отмене вызывается с data = undefined и error = null.
onCallbackError(error, callbackName) => void | Promise<void>Получает исключение onSuccess/onError/onSettled; ошибка callback не заменяет HTTP result/error.
invalidateQueriesIQueryFilters | IQueryFilters[]После успешной mutation инвалидирует query того же QueryClient. В participant scope для logical keys предпочитайте scope.query.invalidate.
retryboolean | number | (failureCount, error) => booleanПолитика повторов. По умолчанию false; при включении создаётся стабильный Idempotency-Key.
idempotencyKeystringЯвный ключ всех retry-попыток; если не задан, при включённом retry создаётся автоматически.
retryDelaynumber | (failureCount, error) => numberЗадержка между повторами.
requestGroupstringВыбирает именованную группу лимита конкурентности runtime для каждой HTTP-попытки.
cancelPreviousbooleanОтменить предыдущий запуск этого handler перед новой мутацией.
networkMode"online" | "always"Поведение при offline. Для настоящей очереди используйте offline plugin.
isLoadingbooleanУправляет локальным loading низкоуровневого handler.
keepPreviousDatabooleanСохраняет data предыдущего успеха во время новой мутации.

mode, variables, mutationKey, operationContext и signal задаёт сам MutationStore. mutationKey берётся из одноимённого поля config.

Общий IExecuteOptions также содержит query-only поля (dedup, definitionId, force, gcTime, meta, queryKey, staleTime, structuralSharing). Mutation execution текущей реализации их не использует.

operationContext присутствует в config как внутреннее runtime-поле и передаётся в mutation execution. Оно попадает в безопасную diagnostics/correlation metadata каждой HTTP-попытки и обычно назначается participant scope. Это не пользовательский способ добавлять HTTP headers или payload.

Lifecycle callbacks

Порядок успешного запуска:

text
onMutate → HTTP request/retry → invalidateQueries → onSuccess → onSettled

invalidateQueries выполняется синхронно как invalidation cache entries. Если инвалидация настроена без refetchActive, она не ждёт нового HTTP-запроса.

Порядок после окончательной ошибки:

text
onMutate → HTTP request/retry → onError → onSettled

Если onMutate возвращает context, этот context получает onSuccess, onError и onSettled. Он удобен для optimistic update и rollback.

Если onMutate бросает ошибку, HTTP не запускается, а состояние переходит в error/idle. Ошибки остальных lifecycle callbacks передаются в onCallbackError, но не заменяют исходный response или request error; onSettled всё равно выполняется.

Автоматический Idempotency-Key остаётся одним и тем же для всех retry и для повтора после auth refresh. Гарантия от дублей требует поддержки этого header на backend.

При cancellation onError не вызывается; onSettled получает (undefined, null, variables, context).

Состояние и методы

ЧленЧто означает
loading, statusВыполняется ли мутация и чем завершился последний запуск.
data, response, error, hasDataРезультат последнего запуска.
execute(...variables)Выполнить мутацию.
executeWithConfig(variables, config)Выполнить её с Axios config конкретного вызова.
cancel()Отменить активный HTTP-запрос.
reset()Очистить состояние handler.
dispose()Отменить запрос и освободить ресурсы.