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.8.0

Published

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

Downloads

146

Readme

@astral/mobx-query

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

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

  • Ориентирована на специфику frontend приложений
  • Для обеспечения реактивности используется mobx
  • По идеологии использования схожа с @tanstack/react-query
  • TS only, totally no any

Table of contents

Installation

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

Basic meaning

  • executor - исполнитель запроса, который будет совершать запрос. Второй аргумент при создании query
  • enabledAutoFetch - включает автоматический запрос данных при обращении к полю data.
  • fetchPolicy - политика, говорящая о том, как следует работать с новыми запросами
    • 'cache-first' - политика применяемая по умолчанию, при отсутствии данных в памяти, будет исполнен executor, его ответ запишется в кеш, и при последующих обращениях данные будут взяты из кеша
    • 'network-only' - каждый запрос будет приводить к вызову executor, его ответ будет записан в кеш(для использования в cache-first)

Basic usage

Для начала вам потребуется создать инстанс кеш сервиса

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

const mobxQuery = new MobxQuery({
    onError: (error) => {
        console.log(error); // место для вашей обработки ошибок по умолчанию, опционально
    },
    fetchPolicy: 'cache-first', // 'cache-first' по умолчанию, опционально 
    enabledAutoFetch: false, // false по умолчанию, опционально
});

Варианты использования query

1. Ручной синхронный.

Можно вызывать встроенный метод sync, передавая в него колбэк опциональные параметры onSucess и onError. В onSuccess будут переданы полученные данные от успешного запроса, а в onError, соответственно, будет переданы данные ошибки в случае провального запроса. Если при вызове обработчик onError не был передан, вызовется стандартный, переданный при создании MobxQuery инстанса.

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

query.sync({
    onSuccess: (data) => {
        console.log(data); // место для реагирования на ответ
    },
    onError: (error) => {
        console.log(error); // место для вашей ошибки
    }
});

Пример в sandbox

2. Ручной асинхронный.

Можно вызвать встроенный метод async. Возвращает промис, соответственно в then попадут данные успешного запроса. Будьте внимательны, используя метод "async", позаботьтесь о добавлении ".catch", иначе ошибка запроса попадет в глобальный exception.

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

query
    .async()
    .then((data) => {
        console.log(data); // место для реагирования на ответ
    })
    .catch((e) => {
        console.log(e); // место для вашей ошибки
    });

Пример в sandbox

3. Автоматический.

При создании query, предусмотрен вариант автоматического запроса при обращении к полю data из query. Требуется активация флага enabledAutoFetch при создании query, либо установка стандартного значения, при создании MobxQuery инстанса. Т.е. благодаря реактивности предоставляемой mobx, пока не произойдет считывания поля data или же не будут вызваны sync/async методы, запрос данных так же не произойдет.

import { observer } from 'mobx-react-lite';

const query = mobxQuery.createQuery(
    ['some cache key'],
    () => Promise.resolve('foo'),
    { enabledAutoFetch: true }
);

const MyComponent = observer(() => <div>{query.data}</div>) // <div>foo</div>

Пример в sandbox

Инвалидация данных

Существует необходимость инвалидировать данные, типичным примером являются CRUD операции. В контексте нашей библиотеки, инвалидация подразумевает под собой отметку для query, означающую, что данные устарели, и их необходимо обновить. Для корректной работы инвалидации, при создании query требуется использование ключа. Ключ для создания может быть как примитивом, так и объектом. Главное, чтобы они были подходящими для JSON сериализации.

Инстанс MobxQuery содержит специальный метод invalidate, принимающий в качестве аргумента массив ключей.

const query = mobxQuery.createQuery(
    ['some cache key'],
    () => Promise.resolve('foo'),
    { enabledAutoFetch: true }
);

mobxQuery.invalidate(['some cache key'])

Пример в sandbox

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

  • Как при создании 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();

Ручная установка данных в кэш

Для установки данных, без исполнения executor, используйте метод forceUpdate. При вызове все статусные флаги устанавливаются как success состояние

query.forceUpdate('foo');

InfiniteQuery

Существует необходимость постепенного запроса массивов данных, в постраничном режиме. Типичный пример, инфинити скролл, когда новая пачка данных запрашивается, в момент когда пользователь докрутил список до конца. Для удобства, мы создали специальный query, который содержит дополнительный метод fetchMore и при вызове оного, происходит запрос с увеличенными счетчиками. Данные ответа на этот запрос, будут сконкатенированы с уже имеющимся. В случае, если количество данных меньше, чем длина страницы, будет считаться что мы дошли до конца списка. В executor будет передан объект с offset - количество элементов отступа от начала списка, и count - количество элементов на одну страницу.

Значение count и увеличение offset регулируется опциональным параметром incrementCount при создании query. По умолчанию равен 30.

import { when } from 'mobx';

const query = mobxQuery.createInfiniteQuery(
    ['some cache key'],
    ({ offset, count }) => {
        // можно использовать "offset/count" для необходимых преобразований и последующего запроса к api
        return Promise.resolve(['foo'])
    },
    {
        incrementCount: 30, // опционально, по умолчанию 30
    }
);

await query.async();

console.log(query.data); // ['foo'] 

query.fetchMore();
await when(() => !query.isLoading); // ждем фоновой загрузки

console.log(query.data); // ['foo', 'foo'] 

isEndReached

Для определения того, что мы все таки достигли конца списка, присутствует флаг isEndReached.

const query = mobxQuery.createInfiniteQuery(
    ['some cache key'],
    () => Promise.resolve([]),
);

await query.async();

console.log(query.isEndReached); // true

Пример в sandbox

Mutation

Для изменения данных необходимо использовать mutation. Ответы Mutation не кэшируются.

const mutation = mobxQuery.createMutation(
    (params) => {
        console.log(params); // при необходмости, можем использовать опциональные параметры
        return Promise.resolve('foo');
    },
);

Пример в sandbox

async вариация

mutation
    .async('bar') // тут, по нашему примеру, увидим консоль 'bar'
    .then((data) => {
        console.log(data) // а тут уже 'foo'
    }); 

sync вариация

mutation.sync({
    params: 'bar',    
    onSuccess: (data) => {
        console.log(data) // а тут уже 'foo'
    }
}); // тут, по нашему примеру, увидим консоль 'bar'

fetchPolicy

const cacheFirstQuery = mobxQuery.createQuery(
    ['cache-first key'],
    () => {
        console.log('cache-first request');
        return Promise.resolve('foo');
    },
    {
        fetchPolicy: 'cache-first',
    }
);

const networkOnlyQuery = mobxQuery.createQuery(
    ['network-only key'],
    () => {
        console.log('network-only request');
        return Promise.resolve('bar');
    },
    {
        fetchPolicy: 'network-only',
    }
);

await cacheFirstQuery.async(); // увидим консоль 'cache-first request'
await networkOnlyQuery.async(); // увидим консоль 'network-only request'

await cacheFirstQuery.async(); // вызова executor не произойдет, и консоль не выведется
await networkOnlyQuery.async(); // вновь увидим консоль 'network-only request'

const duplicateCacheFirstQuery = mobxQuery.createQuery(
        ['cache-first key'], // использован тот же самый ключ, что и для cacheFirstQuery
        () => {
          console.log('duplicate cache-first request');
          return Promise.resolve('foo');
        }
);

await duplicateCacheFirstQuery.async(); // вызова executor не произойдет, и консоль не выведется

Пример в sandbox

Вспомогательные флаги и поля

Query, InfiniteQuery и Mutation имеют одинаковый набор вспомогательных флагов и полей, работающих по единому принципу.

isLoading

Boolean флаг, указывающий на процесс выполнения запроса

isSuccess

Boolean флаг, указывающий на успешное выполнение запроса

isError

Boolean флаг, указывающий на провалившийся запрос

isIdle

Boolean флаг, указывающий на простаивание query, первый же вызов запроса переключит его в false

error

Поле, содержащее информацию о последней ошибке

const query = mobxQuery.createQuery(
    ['some cache key'],
    () => Promise.reject('foo'),
);

await query
    .async()
    .catch((e) => {
        console.log(e); // 'foo'
    });

console.log(query.isError); // 'true'
console.log(query.error); // 'foo'

Режим фонового обновления

Query и InfiniteQuery имеют режим фонового обновления. Предполагается, что будет хорошо подходить для обновления данных через websocket.

В этом режиме, основные статусные флаги isSuccess, isLoading, isError, error будут изменяться до первого успешного запроса. Последующие запросы уже будут изменять статусные флаги под полем background

const query = mobxQuery.createQuery(
    ['some cache key'],
    () => Promise.resolve('foo'),
    { isBackground: true }
);

await query.async();
console.log(query.isLoading); // переключался в true на момент запроса
console.log(query.isSuccess); // true

query.invalidate();
await query.async();
console.log(query.isLoading); // не изменялся
console.log(query.isSuccess); // остался неизменным - true

console.log(query.background.isLoading); // переключался в true на момент обновления
console.log(query.background.isSuccess); // true

Тестирование

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

Исходный код

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

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

BookRepository - фасад для работы с данными, который использует MobxQuery:

export class BookRepository {
  constructor(private readonly mobxQuery: MobxQuery) {}

  public getBookListQuery = (params: BookRepositoryDTO.BookListInputDTO) =>
    this.mobxQuery.createQuery<BookRepositoryDTO.BookListDTO>(
      ['book-list', params],
      () =>
          apiHttpClient.get('/books', {
              params,
          }),
    );
}

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

class BooksListStore {
  public sort?: SortData;

  constructor(private readonly bookRepository: BookRepository) {
    makeAutoObservable(this);
  }

  private get listQuery() {
    return this.bookRepository.getBookListQuery(this.sort);
  }

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

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

Тест

import { when } from 'mobx';

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

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

    const bookRepositoryMock = mock<BookRepository>({
      // Подменяем реализацию метода для того, чтобы получить ожидаемый результат
      getBookListQuery: () =>
          // Создаем моковый Query, соответствующий интерфейсу BookRepository
          mobxQuery.createQuery(['id'], async () => fakeBookList),
    });

    const sut = new GoodsListStore(bookRepositoryMock);

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

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