Продолжаем нашу серию заметок: “Готовим mobx в обход документации”. В прошлый раз мы реализовали примитив Subscriber
. Он очень полезный, но не у каждого в проекте есть вебсокеты или подобные генераторы данных.
Что есть у каждого, так это REST API. Работа с асинхронными операциями в документации, хоть и описана, но с архитектурной стороны вызывает одни подозрения и много boilerplate кода. Есть пакет mobx-utils
, который содержит метод fromPromise
, но очень непредсказуемо, если обратиться к нему вне реактивного контекста и то можно получить тонну запросов на сервер.
Давайте вместе спроектируем работу с асихронным API в mobx здорового человека
, тем более все необходимое у нас уже есть.
const id = observable.box(1);
const model = new PromiseModel(() => fetch(`/api/data/${id.get()}`).then(r => r.json()));
autorun(() => console.log('DATA:', model.status, model.value));
// DATA: pending undefined
// DATA: fulfilled {....}
runInAction(() => id.set(2));
// DATA: pending undefined
// DATA: fulfilled {....}
Обьявим interface Data
для хранения ответа сервера, в нем будут храниться данные/ошибка и статус ответа. Будем использовать наш уже знакомый Subscriber
. В качестве getId
мы используем наш коллбек возращающий Promise
. Все реактивные примитивы, которые будут использованы внутри load
будут перезапускать его вызов. В subscribe
мы создаем реактивный контейнер, который будем использовать для хранения ответа от асинхронной операции и одновременно будем использовать его в качестве результата Subscriber
. В завершении опишем методы доступа к данным и статусу операции get data
и get status
.
interface Data<T> {
value: T | Error;
status: 'fulfilled' | 'rejected';
}
class PromisedModel<T> {
constructor(private load: () => Promise<T>) {
makeObservable(this, {
data: computed.struct,
status: computed.struct,
});
}
private subscription = new Subscriber<
Promise<T>,
IObservableValue<Data<T> | undefined>
>({
getId: () => this.load(),
subscribe: (promise, push) => {
// создаем реактивный контейнер для хранения ответа
const $box = observable.box<undefined | Data<T>>(undefined, { deep: false });
push($box);
promise.then(
value => runInAction(() =>
$box.set({ value, status: 'fulfilled' }))
.then(
err => runInAction(() =>
$box.set({ value: new Error(err),
status: 'rejected'})));
return () => {};
}
})
get data() { return this.subscription.data?.get()?.value; }
get status() { return this.subscription.data?.get()?.status ?? 'pending'; }
}