npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@astral/mobx-query

v1.10.3

Published

Библиотека для кеширования запросов.

Downloads

697

Readme

@astral/mobx-query

Библиотека для кеширования запросов.

Особенности:

  • ⚡️️️️ Реактивный кэш на основе mobx
  • ️️️️️️⚡️️️️ Вдохновлено @tanstack/react-query
  • ⚡️️️️ Декларативный способ описания queries и mutations
  • ⚡️️️️ Реализация архитектурного подхода работы с данными Astral Architecture Guide
  • ⚡️️️️ Фоновая подгрузка данных для работы с WebSocket
  • ⚡️️️️ Возможность тестирования

Table of contents

Installation

npm install @astral/mobx-query --save
yarn add @astral/mobx-query

Basic usage

├── api/ 
|    ├── _fakers/
|    ├── endpoints/
|    ├── fetchers/
|    |    ├── docs.ts
|    |    └── index.ts
|    ├── services/
|    |    ├── CacheService/
|    |    |    ├── CacheService.ts
|    |    |    └── index.ts
|    |    └── index.ts
|    └── index.ts                         

Инициализация MobxQuery: api/services/CacheService/CacheService.ts

import { MobxQuery } from '@astral/mobx-query';

// рекомендуется явно задавать параметры для MobXQuery
export const createCacheService = () =>
  new MobxQuery({ enableAutoFetch: true, fetchPolicy: 'cache-first' });

export const cacheService = createCacheService();

Определение fetcher и cacheGroups для docs: api/services/Fetcher/docs.ts

const docsFetcher = {
  queries: {
    doc: cacheService.createQuerySet((id: string) => ({
      execute: () => docsEndpoints.getDoc(id).then(({ data }) => data),
    })),
  },
  infiniteQueries: {
    docList: cacheService.createInfiniteQuerySet(
      (filters: Omit<DocsDTO.DocListFilters, 'offset' | 'count'>) => ({
        execute: ({ offset, count }) =>
          docsEndpoint
            .getDocList({ offset, count, ...filters })
            .then(({ data }) => data.list),
      }),
    ),
  },
  mutations: {
    editDoc: cacheService.createMutationSet(
      (params: DocsDTO.EditDocInput) => docsEndpoints.editDoc(params),
    ),
  },
};

export type DocsFetcher = typeof docsFetcher;

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

import { type DocsFetcher } from '@example/api';

class DocStore {
  constructor(
    private readonly _docID: string,
    private readonly _docsFetcher: DocsFetcher,
  ) {
    makeAutoObservable(this, {}, { autoBind: true });
  }

  private get docQuery() {
    return this._docsFetcher.queries.doc.create(this.docID);
  }

  public get docName() {
    if (!this.docQuery.data) {
      return '';
    }

    return `Название документа: ${this.docQuery.data.name}`;
  }

  public get isLoading() {
    return this.docQuery.isLoading;
  }
}
class DocManagerStore {
  constructor(private readonly _docsFetcher: DocsFetcher) {}

  public changeOrg = () => {
    // инвалидирует все doc query, которые есть в кэше
    this._docsFetcher.queries.doc.invalidate();
  };
}

Core concepts

Query предназначен для получения данных. Не должен производить изменения. QuerySet - набор queries для данных, получаемых по одной и той же сущности с разными параметрами. InfiniteQuery предназначен для получения бесконечного списка данных. Mutation предназначен для изменения данных на сервере. MutationSet - набор mutations для изменения данных. Необходим для консистенстности api с QuerySet.

QuerySet, InfiniteQuerySet и MutationSet создаются через методы MobxQuery:

  • createQuerySet
  • createInfiniteQuerySet
  • createMutationSet

Query, InfiniteQuery и Mutation создаются через методы MobxQuery:

  • createQuery
  • createInfiniteQuery
  • createMutation

Рекомендуется использовать именно Set'ы потому что это выскоуровневое api, скрывающее внутри себя сложность работы с кэшем и позволяющее работать с данными более декларативно. Set'ы используют внутри себя Query, InfiniteQuery и Mutation.

Методы createQuery, createInfiniteQuery и createMutation необходимо использовать для реализации собственных библиотек для работы с данными.

QuerySet

Query предназначен для получения данных. Не должен производить изменения. QuerySet - набор queries для данных, получаемых по одной и той же сущности с разными параметрами.

Создание QuerySet

В примере ниже будет создан doc query, который будет получать данные по id документа. После успешного выполнения запроса данные будут закэшированы.

const docsFetcher = {
  queries: {
    doc: mobxQuery.createQuerySet((id: string) => ({
      execute: () => docsEndpoints.getDoc(id).then(({ data }) => data),
    })),
  },
};

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

Для получения объекта query необходимо вызвать метод create. Параметры create полностью идентичны параметрам, указанным при определении query:

const docsFetcher = {
  queries: {
    doc: mobxQuery.createQuerySet((id: string) => ({
      execute: () => docsEndpoints.getDoc(id).then(({ data }) => data),
    })),
  },
};

// create принимает только один параметр - id: string
const docQuery = docsFetcher.queries.doc.create('docID');

Метод create вернет объект query, который содержит всю информацию по запросу и методы работы с запросом.

Синхронный вызов запроса данных

Метод sync позволяет синхронно запустить запрос на получение данных:

const docQuery = docsFetcher.queries.doc.create(docID);

// есть callbacks на обработку success и error
docQuery.sync({
  onSuccess: (data) => {
    console.log(data);
  },
  onError: (error) => {
    console.log(error);
  },
});

// true
docQuery.isLoading;

Асинхронный вызов запроса данных

Метод async позволяет асинхронно запустить запрос на получение данных:

const docQuery = docsFetcher.queries.doc.create(docID);

// передавать параметры запроса не нужно потому что они уже были переданы при вызове init
const response = await docQuery.async();

docQuery.isSuccess; // true
docQuery.data; // идентичен response

Автоматический запрос данных

Если MobxQuery был создан с флагом enableAutoFetch: true, то данные будут автоматически запрошены при обращении к полю data:

const docQuery = docsFetcher.queries.doc.create(docID);

docQuery.data; // триггер запроса данных

docQuery.isLoading; // true

Если MobxQuery был создан с флагом enableAutoFetch: false, то автоматически запрос данных можно включить для текущего query:

const docQuery = docsFetcher.queries.doc.createWithConfig((
  { enableAutoFetch: true },
  docID
);

docQuery.data; // триггер запроса данных

docQuery.isLoading; // true

InfiniteQuerySet

InfiniteQuery предназначен для получения бесконечного списка данных. InfiniteQuerySet - набор InfiniteQuery для данных, получаемых по одной и той же сущности с разными параметрами.

Создание InfiniteQuerySet

В примере ниже будет создан docList InfiniteQuerySet. После успешного выполнения запроса данные будут закэшированы.

const docsFetcher = {
  infiniteQueries: {
    docList: mobxQuery.createInfiniteQuerySet(
      (filters: Omit<DocsDTO.DocListFilters, 'offset' | 'count'>) => ({
        execute: ({ offset, count }) =>
          docsEndpoint
            .getDocList({ offset, count, ...filters })
            .then(({ data }) => data.list),
      }),
    ),
  },
};

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

Для получения объекта InfiniteQuery необходимо вызвать метод create. Параметры create полностью идентичны параметрам, указанным при определении InfiniteQuery:

const docsFetcher = {
  infiniteQueries: {
    docList: mobxQuery.createInfiniteQuerySet(
      // Omit необходим для того, чтобы не передавать offset и count при вызове запроса из логики. Offset и count будут сформированы и переданы автоматически
      (filters: Omit<DocsDTO.DocListFilters, 'offset' | 'count'>) => ({
        execute: ({ offset, count }) =>
          docsEndpoint
            .getDocList({ offset, count, ...filters })
            // fetch должен возвращать array
            .then(({ data }) => data.list),
      }),
    ),
  },
};

// первым параметром create является - filters: Omit<DocsDTO.DocListFilters, 'offset' | 'count'>
const docListQuery = docsFetcher.infiniteQueries.docList.create({ search: 'test' });

Загрузка данных

import { when } from 'mobx';

const docListQuery = docsFetcher.infiniteQueries.docList.create({ search: 'test' });

await docListQuery.async(); // запрос на получение первых 30 записей

docListQuery.data.length; // 30 записей

docListQuery.fetchMore(); // запрос на получение следующих 30 записей

// ждем загрузки
await when(() => !query.isLoading);

docListQuery.data.length; // 60 записей

isEndReached. Определение конца списка

Флаг isEndReached будет установлен в true, если записи для загрузки закончились:

import { when } from 'mobx';

const docListQuery = docsFetcher.infiniteQueries.docList.create({ search: 'test' });

await docListQuery.async(); // запрос на получение первых 30 записей

docListQuery.data.length; // 30 записей

docListQuery.fetchMore(); // запрос на получение следующих 30 записей

// ждем загрузки
await when(() => !query.isLoading);

docListQuery.data.length; // 19 записей. Последняя страница содержит 9 записей, а не 30, как было запрошено, значит больше данных нет

docListQuery.isEndReached; // true

Флаг isEndReached устанавливается в true, когда количество полученных элементов меньше запрошенного количества, что означает отсутствие дополнительных данных на сервере.

Изменение количества запрашиваемых записей

Для изменения конфигурации InfiniteQuery необходимо использовать метод createWithConfig:

const docListQuery = docsFetcher.infiniteQueries.docList.createWithConfig(
  { incrementCount: 10, enabledAutoFetch: true },
  { search: 'test' },
);

Интерфейс InfiniteQueryConfig.

MutationSet

Mutation предназначен для отправки данных на сервер. MutationSet - набор mutations, изменяющих одну и ту же сущность. Предоставляет интерфейс, идентичный QuerySet, для консистенстности api.

Создание и использование MutationSet

В примере ниже будет создан editDoc mutation, который отправляет данные на сервер для редактирования документа:

const docsFetcher = {
  mutations: {
    editDoc: mobxQuery.createMutationSet((params: DocsDTO.EditDocInput) => docsEndpoints.editDoc(params)),
  },
};

const editDocMutation = docsFetcher.mutations.editDoc.create();

// params являются параметрами, указанными при определении mutation - DocsDTO.EditDocInput
await editDocMutation.async({ params: { id: 'docID', name: 'test' } });

Метод create вернет объект mutation, который содержит всю информацию по запросу и методы работы с запросом.

Синхронный вызов mutation

Метод sync позволяет синхронно запустить запрос:

const editDocMutation = docsFetcher.mutations.editDoc.create();

// есть callbacks на обработку success и error
editDocMutation.sync({
  params: { id: 'docID', name: 'test' },
  onSuccess: (data) => {
    console.log(data);
  },
  onError: (error) => {
    console.log(error);
  },
});

// true
editDocMutation.isLoading;

Асинхронный вызов mutation

Метод async позволяет асинхронно запустить запрос:

const editDocMutation = docsFetcher.mutations.editDoc.create();

await editDocMutation.async({ params: { id: 'docID', name: 'test' } });

Кэширование QuerySet и InfiniteQuerySet

QuerySet и InfiniteQuerySet позволяет закэшировать данные, которые были получены ранее.

FetchPolicy

FetchPolicy определяет политику получения данных.

Существует два типа политики:

  • cache-first - если в кэше есть данные, они будут возвращены, если нет, то данные будут получены из сети, после чего ответ будет записан в кэш
  • network-only - данные всегда берутся из сети, при этом ответ записывается в кэш

Глобальная установка fetchPolicy

Для глобальной установки конкретной политики fetchPolicy необходимо передать параметр fetchPolicy при создании MobxQuery:

import { MobxQuery } from 'mobx-query';

export const createQuery = () => new MobxQuery({ fetchPolicy: 'cache-first' });

Локальная установка fetchPolicy

Для каждого отдельного query можно установить свою fetchPolicy при инициализации.

Пример для Query:

const docQuery = docsFetcher.queries.doc.createWithConfig(({ fetchPolicy: 'network-only' }, 'docID');

Пример для InfiniteQuery:

const docListQuery = docsFetcher.infiniteQueries.docList.createWithConfig(
  { incrementCount: 10, fetchPolicy: 'network-only' },
  { search: 'test' },
);

Принцип работы fetchPolicy

cache-first:

const docQuery = docsFetcher.queries.doc.createWithConfig(({ fetchPolicy: 'cache-first' }, 'docID');

await docQuery.async(); // запрос будет выполнен потому что до этого запроса с такими параметрами не было

await docQuery.async(); // запрос не будет выполнен потому что данные уже есть в кэше. Promise будет завершен сразу

network-only:

const docQuery = docsFetcher.queries.doc.createWithConfig(({ fetchPolicy: 'network-only' }, 'docID');

await docQuery.async(); // запрос будет выполнен
await docQuery.async(); // запрос будет выполнен

const docQueryWithCache = docsFetcher.queries.doc.createWithConfig(({ fetchPolicy: 'cache-first' }, 'docID');

await docQueryWithCache.async(); // запрос не будет выполнен потому что прежде данные были получены с политикой `network-only` и записаны в кэш

Как работает кэш

Все данные, возвращаемые QuerySet и InfiniteQuerySet, кэшируются в едином хранилище @astral/mobx-query.

При первом вызове create в хранилище создается запись с ключем, состоящим из:

  • Хэш от функции конфигурации
  • queryParams

Пример:

const docFetcher = {
  queries: {
    doc: mobxQuery.createQuerySet((id: string, filters: { search: string }) => ({
      execute: () => docsEndpoints.getDoc(id, filters),
    })),
  },
};

// ключ кэша будет равен "12fj1d,doc,1,"{"search":"test"}""
// 12fj1d - это хэш от функции конфигурации
const docQuery = docFetcher.queries.doc.create('1', { search: 'test' });
// ключ кэша будет равен "12fj1d,doc,2,"{"search":"test2"}""
const docQuery = docsFetcher.queries.doc.create('1', { search: 'test2' });

Автоматическая чистка кэша

Mobx-query кэш организован через WeekRef, поэтому не используемые данные автоматически удаляются сборщиком мусора.

Инвалидация кэша QuerySet и InfiniteQuerySet

Если данные в query стали неактуальными, то необходимо вызвать метод invalidate:

// инвалидация всех query с именем `doc`
docsFetcher.queries.doc.invalidate();

После вызова invalidate данные в кэше будут помечены как невалидные и при следующем обращении к query будет выполнен запрос на сервер. Если при вызове метода invalidate на изменения .data подписан любой store, то произойдет моментальный перезапрос активных query:

const docQuery = docsFetcher.queries.doc.create('docID');

await docQuery.async(); // данные записаны в кэш

docsFetcher.queries.doc.invalidate();

docQuery.data; // триггер запроса данных потому что они были помечены как невалидные
docQuery.isLoading; // true

Инвалидация всех query по их имени

Для инвалидации всех query по их имени можно вызвать метод invalidate:

const docQuery1 = docsFetcher.queries.doc.create('1');
const docQuery2 = docsFetcher.queries.doc.create('2');

docsFetcher.queries.doc.invalidate(); // данные docQuery1 и docQuery2 помечены как невалидные

Инвалидация query по частичному совпадению параметров

Если необходимо инвалидировать данные по конкретным параметрам, то их необходимо передать в метод invalidate:

const docsFetcher = {
  queries: {
    doc: mobxQuery.createQuerySet((id: string) => ({
      execute: () => docsEndpoints.getDoc(id).then(({ data }) => data),
    })),
  },
};

docsFetcher.queries.doc.invalidate('1'); // документ с id = 1 будет помечен как невалидный

Параметры invalidate полностью совпадают с параметрами, описанными при определении query:

const docsFetcher = {
  queries: {
    doc: mobxQuery.createQuerySet((id: string, search: string, filters: Filters) => ({
      execute: () => docsEndpoints.getDoc(id, search, filters),
    })),
  },
};

docsFetcher.queries.doc.invalidate('1', 'test', { sort: 'asc' });

Инвалидация query по частичному совпадению параметра-объекта

const docsFetcher = {
  queries: {
    doc: mobxQuery.createQuerySet((id: string, filters: { sort: string; search: string }) => ({
      execute: () => docsEndpoints.getDoc(id, filters),
    })),
  },
};

const query1 = docsFetcher.queries.doc.create('1', { sort: 'asc', search: 'test' });
const query2 = docsFetcher.queries.doc.create('1', { sort: 'desc', search: 'test' });

docsFetcher.queries.doc.invalidate('1', { sort: 'asc' }); // данные только query1 будут помечены как невалидные

Кастомная установка ключей кэширования

При определении QuerySet и InfiniteQuerySet можно указать параметр keys и name, которые будут использоваться для формирования ключей кэша.

  • name - название набора. Будет использоваться вместо хэша от функции конфигурации
  • keys - будут использоваться вместо параметров функции конфигурации
const docsFetcher = {
  queries: {
    doc: mobxQuery.createQuerySet(
      (id: string, search: string) => ({
        keys: [id],
        execute: () => docsEndpoints.getDoc(id, search),
      }),
      { name: 'doc' }
    ),
  },
};

// ключ кэша будет равен "doc,1"
const query = docsFetcher.queries.doc.create('1', 'test');

docsFetcher.queries.doc.invalidate('1'); // данные query будут помечены как невалидные
docsFetcher.queries.doc.invalidate('1', 'test'); // второй параметр будет проигнорирован

Фоновая загрузка данных QuerySet и InfiniteQuerySet

Для фонового обновления данных возможно использовать флаг isBackground для query и infiniteQuery. Данный метод хорошо подходит для инвалидации данных по событиям из WebSocket.

Пример для QuerySet:

const docQuery = docsFetcher.queries.doc.createWithConfig(({ isBackground: true }, 'docID');

docQuery.sync(); // запрос будет выполнен в фоновом режиме

docQuery.isLoading; // true. Первый запрос изменит статусные флаги

await when(() => docQuery.isSuccess);

docsFetcher.queries.doc.invalidate();

docQuery.isLoading; // false. При этом запрос данных уже выполняется в фоновом режиме, если где-то есть подписчик на изменения `.data`
docQuery.background.isLoading; // true

Изменение кэша QuerySet и InfiniteQuerySet

Для изменения данных в кэше необходимо использовать forceUpdate, содержащийся в query:

const docQuery = docsFetcher.queries.doc.create('docID');

// в кэше будет записано "{ name: 'test' }", а статустные флаги перейдут в состояние 'success'
docQuery.forceUpdate({ name: 'test' });

Тестирование Fetcher на сонове QuerySet и InfiniteQuerySet.

Для мокинга QuerySet и InfiniteQuerySet необходимо использовать mock следующего вида:

type Fetcher = {
  queries: Record<string, QuerySet<any[], any>>;
  infiniteQueries: Record<InfiniteQuerySet<any[], any>>;
  mutations: Record<MutationSet<any, any>>;
};

const mockFetcher = <TFetcher extends Fetcher>(config: DeepPartial<TFetcher>) => config as TFetcher;

Не используйте 'vitest-mock-extended' для мокинга Fetcher. Причина: vitest-mock-extended оборачивает объект в Proxy, что нарушает работу Mobx.

Тестирование с включенным enabledAutoFetch

MobxQuery инициализируется с параметром: enabledAutoFetch:

const createMobxQuery = () => new MobxQuery({
  enabledAutoFetch: true,
});

booksFetcher.ts

export const booksFetcher = {
  queries: {
    bookList: mobxQuery.createQuerySet((params: BooksDTO.BookListInput) => ({
      execute: () => booksEndpoint.getBookList(params),
    })),
  },
};

export type BooksFetcher = typeof booksFetcher;

BooksListStore - использует BooksFetcher для получения данных:

class BooksListStore {
  public sort?: SortData;

  constructor(private readonly _booksFetcher: BooksFetcher) {
    makeAutoObservable(this);
  }

  private get listQuery() {
    return this._booksFetcher.queries.bookList.create(this.sort);
  }

  public get list(): ListItem[] {
    const data = this.listQuery.data || [];

    return data.map(({ id, name, price }) => ({
      id,
      name,
      price: formatPriceToView(price),
    }));
  }
}

Тест BooksListStore:

import { when } from 'mobx';
import { mockCacheGroups } from '@astral/mobx-query-vitest-mock';

describe('BooksListStore', () => {
  it('Список книг форматируется для отображения', async () => {
    // Для каждого теста необходимо инициализировать свой instance MobxQuery,
    // в противном случае каждый тест будет модифицировать кэш
    const mobxQuery = createMobxQuery();

    const fakeBookList = makeFakeBookList(2, { price: 1000 });
    const fakeBookListItem = fakeBookList.data[0];

   const booksFetcherMock = mockFetcher<BooksFetcher>({
      queries: {
        bookList: () => mobxQuery.createQuerySet(() => ({
          execute: async () => fakeBookList,
        })),
      },
    });

    const sut = new BooksListStore(booksFetcherMock);

    // Ждем автоматической загрузки данных
    // Загрузка данных начнется автоматически при обращении к sut.list за счет параметра enabledAutoFetch
    await when(() => Boolean(sut.list?.length));

    expect(sut.list[0]).toEqual({
      id: fakeBookListItem.id,
      name: fakeBookListItem.name,
      price: '1 000 руб.',
    });
  });
});

Core

Концепции, описанные ниже являются ядром библиотеки и используются внутри QuerySet, InfiniteQuerySet и MutationSet.

Query

Query позволяет получать данные из API и кешировать их. Query не должны производить изменения.

Создание Query

В примере ниже будет создан doc query, который будет получать данные по id документа. После успешного выполнения запроса данные будут закэшированы.

const createDocQuery = (id: string) => 
  // первый параметр - ключи, по которому в кэше будет храниться ответ запроса
  cacheService.createQuery(['doc', id], 
    (params: { id: string }) => docsEndpoints.getDoc(params.id)
  ); 

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

const createDocQuery = (id: string) => 
  // первый параметр - ключи, по которому в кэше будет храниться ответ запроса
  cacheService.createQuery(['doc', id], 
    (params: { id: string }) => docsEndpoints.getDoc(params.id)
  ); 

const docQuery = createDocQuery('docID');

docQuery является объектом, который содержит всю информацию по запросу и методы работы с запросом:

Interface Query

export type Query<TResult = unknown, TError = unknown, TIsBackground = boolean> = {
  /**
   * Текущие данные запроса
   */
  data: TResult | undefined;

  /**
   * Флаг загрузки
   */
  isLoading: boolean;

  /**
   * Флаг успешного выполнения запроса
   */
  isSuccess: boolean;

  /**
   * Флаг наличия ошибки
   */
  isError: boolean;

  /**
   * Текущая ошибка
   */
  error: TError | null;

  /**
   * Флаг, обозначающий простаивание, т.е. запроса еще не было
   */
  isIdle: boolean;

  /**
   * Синхронизирует данные с сервером
   * @param onSuccess - Callback успешного выполнения
   * @param onError - Callback ошибки
   */
  sync: ({ onSuccess, onError }: SyncParams<TResult, TError>) => void;

  /**
   * Асинхронный метод получения данных
   */
  async: () => Promise<TResult>;

  /**
   * Метод инвалидации текущего query
   */
  invalidate: () => void;

  // Статусы, изменяющиеся после первого успешного запроса в режиме фоновой загрузки isBackground: true
  background: {
    /**
     * Флаг обозначающий загрузку данных в фоновом режиме
     */
    isLoading: boolean;

    /**
     * Флаг обозначающий, что последний запрос был зафейлен в фоновом режиме
     */
    isError: boolean;

    /**
     * Данные о последней ошибке в фоновом режиме
     */
    error?: TError;

    /**
     * Флаг, обозначающий успешность завершения последнего запроса в фоновом режиме
     */
    isSuccess: boolean;
  };
};

Синхронный вызов запроса данных

Метод sync позволяет синхронно запустить запрос на получение данных:

const createDocQuery = (id: string) => 
  // первый параметр - ключи, по которому в кэше будет храниться ответ запроса
  cacheService.createQuery(['doc', id], 
    (params: { id: string }) => docsEndpoints.getDoc(params.id)
  ); 

const docQuery = createDocQuery('docID');

// есть callbacks на обработку success и error
docQuery.sync({
  onSuccess: (data) => {
    console.log(data);
  },
  onError: (error) => {
    console.log(error);
  },
});

// true
docQuery.isLoading;

Асинхронный вызов запроса данных

Метод async позволяет асинхронно запустить запрос на получение данных:

const createDocQuery = (id: string) => 
  // первый параметр - ключи, по которому в кэше будет храниться ответ запроса
  cacheService.createQuery(['doc', id], 
    (params: { id: string }) => docsEndpoints.getDoc(params.id)
  ); 

const docQuery = createDocQuery('docID');

// передавать параметры запроса не нужно потому что они уже были переданы при вызове init
const response = await docQuery.async();

docQuery.isSuccess; // true
docQuery.data; // идентичен response

Автоматический запрос данных

Если MobxQuery был создан с флагом enableAutoFetch: true, то данные будут автоматически запрошены при обращении к полю data:

const cacheService = new MobxQuery({ enableAutoFetch: true });

const createDocQuery = (id: string) => 
  // первый параметр - ключи, по которому в кэше будет храниться ответ запроса
  cacheService.createQuery(['doc', id], 
    (params: { id: string }) => docsEndpoints.getDoc(params.id)
  ); 

docQuery.data; // триггер запроса данных

docQuery.isLoading; // true

Если MobxQuery был создан с флагом enableAutoFetch: false, то автоматически запрос данных можно включить для текущего query:

const createDocQuery = (id: string) => 
  cacheService.createQuery(['doc', id], 
    (params: { id: string }) => docsEndpoints.getDoc(params.id),
    { enableAutoFetch: true }
  ); 

docQuery.data; // триггер запроса данных

docQuery.isLoading; // true

InfiniteQuery

InfiniteQuery предназначен для получения бесконечного списка данных.

Создание InfiniteQuery

В примере ниже будет создан docList InfiniteQuery. После успешного выполнения запроса данные будут закэшированы.

const createDocListInfiniteQuery = (filters: Omit<DocsDTO.DocListFilters, 'offset' | 'count'>) => 
  cacheService.createInfiniteQuery(['docList', filters], 
    (params: { offset: number; count: number }) => docsEndpoints.getDocList({ offset: params.offset, count: params.count, ...filters })
  );

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

const createDocListInfiniteQuery = (filters: Omit<DocsDTO.DocListFilters, 'offset' | 'count'>) => 
  cacheService.createInfiniteQuery(['docList', filters], 
    (params: { offset: number; count: number }) => docsEndpoints.getDocList({ offset: params.offset, count: params.count, ...filters })
  );

// первым параметром create является - filters: Omit<DocsDTO.DocListFilters, 'offset' | 'count'>
const docListQuery = createDocListInfiniteQuery({ search: 'test' });

Загрузка данных

import { when } from 'mobx';

const createDocListInfiniteQuery = (filters: Omit<DocsDTO.DocListFilters, 'offset' | 'count'>) => 
  cacheService.createInfiniteQuery(['docList', filters], 
    (params: { offset: number; count: number }) => docsEndpoints.getDocList({ offset: params.offset, count: params.count, ...filters })
  );

const docListQuery = createDocListInfiniteQuery({ search: 'test' });

await docListQuery.async(); // запрос на получение первых 30 записей

docListQuery.data.length; // 30 записей

docListQuery.fetchMore(); // запрос на получение следующих 30 записей

// ждем загрузки
await when(() => !query.isLoading);

docListQuery.data.length; // 60 записей

isEndReached. Определение конца списка

Флаг isEndReached будет установлен в true, если записи для загрузки закончились:

import { when } from 'mobx';

const createDocListInfiniteQuery = (filters: Omit<DocsDTO.DocListFilters, 'offset' | 'count'>) => 
  cacheService.createInfiniteQuery(['docList', filters], 
    (params: { offset: number; count: number }) => docsEndpoints.getDocList({ offset: params.offset, count: params.count, ...filters })
  );

const docListQuery = createDocListInfiniteQuery({ search: 'test' });

await docListQuery.async(); // запрос на получение первых 30 записей

docListQuery.data.length; // 30 записей

docListQuery.fetchMore(); // запрос на получение следующих 30 записей

// ждем загрузки
await when(() => !query.isLoading);

docListQuery.data.length; // 19 записей. Последняя страница содержит 9 записей, а не 30, как было запрошено, значит больше данных нет

docListQuery.isEndReached; // true

Флаг isEndReached устанавливается в true, когда количество полученных элементов меньше запрошенного количества, что означает отсутствие дополнительных данных на сервере.

Изменение количества запрашиваемых записей

Для изменения конфигурации InfiniteQuery необходимо использовать третий параметр config:

const createDocListInfiniteQuery = (filters: Omit<DocsDTO.DocListFilters, 'offset' | 'count'>) => 
  cacheService.createInfiniteQuery(['docList', filters], 
    (params: { offset: number; count: number }) => docsEndpoints.getDocList({ offset: params.offset, count: params.count, ...filters }),
    { incrementCount: 10 }
  );

Интерфейс InfiniteQueryConfig

export type InfiniteQueryConfig = {
  /**
   * Количество записей, которое будет загружено при первом и следующих запросах
   * @default 30
   */
  incrementCount?: number;
  /**
   * Обработчик ошибки
   */
  onError?: (error: unknown) => void;
  /**
   * Флаг, отвечающий за автоматический запрос данных при обращении к полю data
   */
  enabledAutoFetch?: boolean;
  /**
   * Политика кэширования данных.
   */
  fetchPolicy?: FetchPolicy;
  /**
   * Режим фонового обновления
   * @default false
   */
  isBackground?: boolean;
};

Mutation

Mutation предназначен для отправки данных на сервер с целью произведения изменений.

Создание и использование mutations

В примере ниже будет создан editDoc mutation, который отправляет данные на сервер для редактирования документа:

const createEditDocMutation = (params: DocsDTO.EditDocInput) => 
  cacheService.createMutation((params: DocsDTO.EditDocInput) => docsEndpoints.editDoc(params));

const editDocMutation = createEditDocMutation();

// params являются параметрами, указанными при определении mutation - DocsDTO.EditDocInput
await editDocMutation.async({ params: { id: 'docID', name: 'test' } });

Интерфейс Mutation

type Mutation<TResult, TError = unknown, TExecutorParams = void> = {
  /**
   * Синхронный метод выполнения мутации
   */
  sync: (options: {
    onSuccess?: (res: TResult) => void;
    onError?: (e: TError) => void;
    params?: TExecutorParams;
  }) => void;

  /**
   * Асинхронный метод выполнения мутации
   */
  async: (params: TExecutorParams) => Promise<TResult>;

  /**
   * Флаг загрузки
   */
  isLoading: boolean;

  /**
   * Флаг успешного выполнения запроса
   */
  isSuccess: boolean;

  /**
   * Флаг наличия ошибки
   */
  isError: boolean;

  /**
   * Текущая ошибка
   */
  error: TError | null;

  /**
   * Флаг, обозначающий простаивание, т.е. запроса еще не было
   */
  isIdle: boolean;

  // Статусы, изменяющиеся после первого успешного запроса в режиме фоновой загрузки isBackground: true
  background: {
    /**
     * Флаг обозначающий загрузку данных в фоновом режиме
     */
    isLoading: boolean;

    /**
     * Флаг обозначающий, что последний запрос был зафейлен в фоновом режиме
     */
    isError: boolean;

    /**
     * Данные о последней ошибке в фоновом режиме
     */
    error?: TError;

    /**
     * Флаг, обозначающий успешность завершения последнего запроса в фоновом режиме
     */
    isSuccess: boolean;
  };
};

Синхронный вызов mutation

Метод sync позволяет синхронно запустить запрос:

const createEditDocMutation = (params: DocsDTO.EditDocInput) => 
  cacheService.createMutation((params: DocsDTO.EditDocInput) => docsEndpoints.editDoc(params));

const editDocMutation = createEditDocMutation();

// есть callbacks на обработку success и error
editDocMutation.sync({
  params: { id: 'docID', name: 'test' },
  onSuccess: (data) => {
    console.log(data);
  },
  onError: (error) => {
    console.log(error);
  },
});

// true
editDocMutation.isLoading;
Асинхронный вызов mutation

Метод async позволяет асинхронно запустить запрос:

const createEditDocMutation = (params: DocsDTO.EditDocInput) => 
  cacheService.createMutation((params: DocsDTO.EditDocInput) => docsEndpoints.editDoc(params));

const editDocMutation = createEditDocMutation();

await editDocMutation.async({ params: { id: 'docID', name: 'test' } });

Особенности инвалидации Queries

Как при создании query, так и при инвалидации, нужно использовать массив ключей. Предполагается, что query может быть инвалидирован по нескольким ключам.

const query = mobxQuery.createQuery(
  ['key one', 'key two'], // ключ - массив строк
  () => Promise.resolve('foo'),
  { enabledAutoFetch: true }
);

mobxQuery.invalidate(['key two']); // query будет инвалидирован
mobxQuery.invalidate(['key one']); // query будет инвалидирован

Но, стоит учитывать, что ключом является цельный элемент массива, а не составляющие элемента.

const query = mobxQuery.createQuery(
  [['key one', 'key two']], // ключ - двумерный массив строк
  () => Promise.resolve('foo'),
  { enabledAutoFetch: true }
);

mobxQuery.invalidate(['key one']); // ключ не совпадает, query НЕ будет инвалидирован

Инвалидация будет происходить только для query, поле data которых считывается в данный момент. Для query, data которых будут отрендерены позже, запрос произойдет только в момент использования. Для превентивного обновления данных потребуется последовательное использование sync/async методов сразу после invalidate.

Массовая инвалидация

Для инвалидации всех query необходимо использовать метод invalidateQueries:

mobxQuery.invalidateQueries();

Изменение кэша

Для изменения данных в кэше необходимо использовать forceUpdate, содержащийся в query:

const query = mobxQuery.createQuery(['key'], () => Promise.resolve('1'));

// в кэше будет записано '2', а статустные флаги перейдут в состояние 'success'
query.forceUpdate('2');