Skip to main content

Reducing Boilerplate in the API Layer

When working with APIs, there's often a need to describe API requests, handle errors and loading states. The MobX ecosystem doesn't have an equivalent to react-query, but basic functionality can be built using the official mobx-utils package.

Installation

npm i mobx-utils

Usage

import { fromPromise, IPromiseBasedObservable } from 'mobx-utils'
import { makeAutoObservable } from 'mobx'
import { observer } from 'mobx-react-lite'
import { useEffect } from 'react'

type Todo = {
id: number;
title: string;
}

const getTodos = async (): Promise<Todo[]> => {
await new Promise((resolve) => setTimeout(resolve, 500))
return [
{ id: 1, title: 'Clean bathroom' },
{ id: 2, title: 'Feed the cat '},
]
}

class TodoStore {
data?: IPromiseBasedObservable<Todo[]>

constructor() {
makeAutoObservable(this)
}

loadData() {
this.data = fromPromise(getTodos())
}
}

Now the data property contains an observable object with state and value properties. state takes values 'pending' | 'rejected' | 'fulfilled'. value takes values T | undefined, where T is a generic type that TypeScript automatically infers based on what was passed to the fromPromise function.

In the component, you can display this data in different ways:

const store = new TodoStore();

const TodoList = observer(() => {
useEffect(() => {
store.loadData();
}, [])

if (store.data?.state === 'pending') {
return <div>Loading...</div>
}

if (store.data?.state === "rejected") {
return <div>Error</div>
}

return <ul>
{store.data?.value.map((todo, i) => {
return <li key={i}>{todo.title}</li>
})}
</ul>
})

Or through the case method:

const store = new TodoStore();

const TodoList = observer(() => {
useEffect(() => {
store.loadData();
}, [])

// Without this TS complains
if (!store.data) {
return null;
}

return store.data.case({
pending: () => <div>Loading...</div>,
rejected: () => <div>Error</div>,
fulfilled: (value) => <ul>
{value.map((todo, i) => {
return <li key={i}>{todo.title}</li>
})}
</ul>
});
})

Note that in all examples, TypeScript's type inference for the Todo type works correctly.

Conclusion

Libraries like react-query provide a lot of additional functionality, such as caching and refetching on focus. But in simple scenarios, fromPromise can reduce boilerplate.