We continue our series of notes: “Cooking mobx without documentation”. Last time we implemented the Subscriber primitive. It is very useful, but not everyone has WebSockets or similar data generators in their project. What everyone has is a REST API. Although the work with asynchronous operations is described in the documentation, it raises some suspicions and a lot of boilerplate code from the architectural side. There is a mobx-utils package that contains the fromPromise method, but it is very unpredictable if you call it outside of the reactive context and you can get a ton of requests to the server.

Let’s design the work with asynchronous API in mobx healthy person together, especially since we already have everything we need.

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 {....}

Let’s declare the Data interface for storing the server response, it will store the data/error and the response status. We will use our already familiar Subscriber. As getId we use our callback returning Promise. All reactive primitives that will be used inside load will restart its call. In subscribe we create a reactive container that we will use to store the response from the asynchronous operation and at the same time we will use it as the result of Subscriber. Finally, we will describe the methods of accessing the data and the status of the operation get data and 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) => {
			// create a reactive container to store the response
			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'; }
}