Skip to content

QueryStore

QueryStore — автоматическое кэшируемое чтение. После запуска lifecycle он:

  1. вычисляет queryKey();
  2. проверяет enabled();
  3. подписывается на запись query cache;
  4. вызывает queryFn({ signal }), если данных нет или они устарели;
  5. повторяет цикл, когда observable-значения внутри queryKey или enabled изменяются.

Создавайте его через createQueryStore(...). Factory по умолчанию сразу вызывает start().

Отличие от FetchStore

ВопросFetchStoreQueryStore
Кто запускает запросВаш код вызывает fetch(args)Store запускает его автоматически
Где находятся аргументыВ параметрах fetchВ closure функций queryKey и queryFn
Реакция на MobX-поляНетДа
Типичный кейсЗагрузка по кнопкеДанные экрана зависят от observable id/filters

API-функция

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

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

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

// API-функция принимает id пользователя и Axios config.
export const getUserByIdApi = (userId: string, config?: AxiosRequestConfig): Promise<AxiosResponse<User>> => {
    return authApi.get<User>(`/api/v1/users/${userId}`, config)
}

Domain store с реактивным id

ts
import { makeAutoObservable } from "mobx"

import { createQueryStore, type INormalizedAxiosError, type QueryStore } from "@dubium/query-layer"

import { getUserByIdApi, type User } from "../api/user-details.api"

export class UserDetailsStore {
    // Это observable-параметр экрана.
    // При его изменении должен измениться и queryKey.
    userId: string

    // QueryStore создаём в constructor с autoStart: false.
    // Так внешние поля успеют стать observable до запуска reaction.
    private readonly requestHandler: QueryStore<User>

    constructor(initialUserId: string) {
        this.userId = initialUserId

        this.requestHandler = createQueryStore<User>({
            // Lifecycle запустим вручную после makeAutoObservable.
            autoStart: false,

            // Новый userId создаёт новый адрес в query cache.
            queryKey: () => ["users", "details", this.userId],

            // Пустой id временно запрещает запрос.
            enabled: () => this.userId.length > 0,

            // queryFn вызывается без values.
            // Нужный id читается из observable-поля store.
            queryFn: ({ signal }) => {
                return getUserByIdApi(this.userId, { signal })
            },

            // Одинаковый пользователь считается свежим одну минуту.
            staleTime: 60_000,

            // Полезное имя для диагностики.
            displayName: "UserDetailsQuery",
        })

        makeAutoObservable<this, "requestHandler">(
            this,
            {
                // Вложенный QueryStore уже observable.
                requestHandler: false,
            },
            {
                autoBind: true,
            },
        )

        // Теперь queryKey() действительно отслеживает observable userId.
        this.requestHandler.start()
    }

    // Компонент меняет id через предметный метод.
    setUserId(userId: string): void {
        this.userId = userId
    }

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

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

    // isFetching также true при фоновом обновлении старых данных.
    get isFetching(): boolean {
        return this.requestHandler.isFetching
    }

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

    async refresh(): Promise<User | null> {
        // refetch принудительно обновляет текущий queryKey.
        const response = await this.requestHandler.refetch()
        return response?.data ?? null
    }

    dispose(): void {
        // Останавливает reaction и отсоединяет observer.
        this.requestHandler.dispose()
    }
}

Почему fetch(values) здесь нет

У QueryStore нет аргумента values: это автоматический store. Все данные, которые определяют запрос, находятся в замыкании:

ts
queryKey: () => ["users", "details", this.userId]
queryFn: ({ signal }) => getUserByIdApi(this.userId, { signal })

При setUserId("42") MobX замечает изменение this.userId. queryKey() возвращает новый ключ, QueryStore переключает observer и загружает пользователя 42. При возврате к ранее загруженному id store сначала проверит его cache.

Важно: параметр, влияющий на queryFn, должен влиять и на queryKey. Иначе разные ответы попадут в одну запись кэша.

Подробнее: QueryKey и кэш.

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

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

import { UserDetailsStore } from "../../data/users/details/model/user-details.store"

interface UserDetailsProps {
    userId: string
}

export const UserDetails = observer((props: UserDetailsProps) => {
    // Store создаётся один раз с первоначальным id.
    const [store] = useState(() => new UserDetailsStore(props.userId))

    useEffect(() => {
        // Prop изменился — обновляем observable id.
        // QueryStore сам запустит запрос для нового queryKey.
        store.setUserId(props.userId)
    }, [props.userId, store])

    useEffect(() => {
        // Cleanup выполняется только при unmount.
        return () => store.dispose()
    }, [store])

    if (store.loading) {
        return <p>Загружаем пользователя…</p>
    }

    if (store.error && !store.user) {
        return <p role="alert">{store.error.message}</p>
    }

    if (!store.user) {
        return <p>Пользователь не найден</p>
    }

    return (
        <article>
            <h1>{store.user.name}</h1>
            <p>{store.user.email}</p>

            <button disabled={store.isFetching} onClick={() => void store.refresh()}>
                {store.isFetching ? "Обновляем…" : "Обновить"}
            </button>
        </article>
    )
})

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

ПолеTypeDefaultЧто делает
queryKey() => readonly unknown[]Реактивно вычисляет адрес данных в cache. Обязательное поле.
queryFn({ signal }) => Promise<AxiosResponse<TData>>Выполняет API-функцию. Обязательное поле.
autoStartbooleantrue у factoryАвтоматически вызвать start(). Конструктор new QueryStore сам I/O не запускает.
enabled() => booleantrueРазрешает запрос. Может читать observable-поля.
clearOnDisabledbooleantrueОчистить локальный snapshot, когда enabled() стал false. Cache при этом не удаляется.
staleTimenumberнастройки clientВремя свежести данных в миллисекундах.
gcTimenumberнастройки clientВремя хранения query после исчезновения последнего observer.
retryboolean | number | functionнастройки clientПолитика повторов при ошибке.
retryDelaynumber | functionнастройки clientЗадержка между повторами.
requestGroupstringобщий пулВыбирает именованную группу лимита конкурентности runtime.
networkMode"online" | "always""online"Поведение при отсутствии сети.
dedupbooleantrueОбъединять одновременные запросы одного ключа.
structuralSharingbooleantrueСохранять ссылки на неизменившиеся части данных.
keepPreviousDatabooleanfalseПолитика сохранения локального snapshot при смене ключа.
definitionIdstringСтабильный id shared definition; проверяется runtime и cache.
meta{ persist?: boolean; broadcast?: boolean } & Record<string, unknown>Opt-in метаданные плагинов и диагностики.
displayNamestring"QueryStore"Имя MobX store в diagnostics.
clientQueryClientизолированный clientВнешний общий кэш. Обычно передаётся через QueryRuntime/scope.

dedup: false запускает независимые одновременные HTTP-запросы одного ключа; cache применяет результат последнего запуска. Query считается stale точно на границе elapsed === staleTime, поэтому staleTime: 0 не создаёт окно свежести в одну миллисекунду. Для active focus/reconnect filtering используется минимальный staleTime активных observers общего Query.

При каждом изменении динамического ключа runtime повторно проверяет требование definitionId. Store передаёт id в observer/cache, поэтому несовместимые id одного физического ключа завершаются QueryDefinitionConflictError.

Опции методов

МетодOptionTypeЧто делает
fetchforcebooleanИгнорировать fresh state.
fetchcancelRefetchbooleanАдминистративно отменить текущий shared request перед новым. Используйте осторожно.
refetchcancelRefetchbooleanТа же политика отмены; force уже включён.
invalidaterefetchActivebooleanСразу перезапросить активный query после invalidation.
disposecancelbooleanОтменить запрос только если store владеет собственным client.

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

ЧленЧто означает
data, response, errorПоследние данные, Axios response и ошибка.
statusidle, loading, success или error.
fetchStatusidle, fetching или paused.
loadingПервичная загрузка без cached data.
isFetchingЛюбой сетевой запрос, включая background refetch.
isStaleДанные отсутствуют, invalidated или старше staleTime.
queryKey, queryHashKeyТекущий структурный ключ и его hash.
start(), stop()Включить или остановить reaction/observer lifecycle.
fetch(), refetch()Получить данные или принудительно обновить их.
invalidate()Пометить текущую запись устаревшей.
cancel()Отсоединить ожидание этого consumer, не ломая других consumers.
resetLocal()Очистить только snapshot текущего store.
resetQuery() / removeQuery() / reset()Удалить shared query и очистить snapshot.
dispose()Остановить lifecycle и освободить observer.

Простой статический query

Если queryKey не зависит от внешнего MobX-поля, можно использовать короткую инициализацию прямо в поле класса:

ts
private readonly requestHandler = createQueryStore({
  queryKey: () => ["profile", "me"],
  queryFn: ({ signal }) => getMyProfileApi({ signal }),
})

В таком случае отдельные autoStart: false и start() не нужны.