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() не нужны.