Isomorphic handling of promises in libraries like react.js, vue.js, angular, svelte etc.
Tags: javascript, vue, react
Published: 2020-09-15 11:30:25

If you are working on a SPA that connects to an API somewhere, you are going to need to write a lot of fetch requests.

Now it's not as simple as fetching something and putting the result on the page.

What about an indication for the user that the request is currently pending? What when there was an error fetching the resource? What if the result is empty? What if you need to be able to cancel requests? ...

Handling all of that introduces a lot of boilerplate. Now imagine having two or even more API requests in a component...

So here's an approach I've been using for quite a while now. I mainly developed this for a SPA I wrote in vue.js but since realized that it pretty much works with every single UI library as well as plain Vanilla JS.

I extracted it to a library called [promistate](https://github.com/MZanggl/promistate).

It works by first defining your promise like this:

```javascript
import promistate from 'promistate'

const userPromise = promistate(async function callback(id) {
    return fetch(`/api/users/${id}`).then(res => res.json())
})
```

This won't execute the callback right away but `userPromise` already holds a lot of useful properties for us. For example, we can say `userPromise.value` to get the resolved value (currently null), `userPromise.isPending` to know if the promise is pending, and `userPromise.error` to see if there was an error fetching the resource. There are a couple of more useful properties...

Now how do we actually fetch? We simply do `userPromise.load(1)`. This will now set `isPending` to true, and after the promise settled, it will mutate `userPromise.value` if successful, or `userPromise.error` if an error was thrown.

Now let's see it in action in a Vue component.

```vue
<template>
  <div>
    <button @click="todosPromise.load()">load</button>
    <button @click="todosPromise.reset()">reset</button>

    <div v-if="todosPromise.error">Whoops!</div>
    <div v-else-if="todosPromise.isPending">Pending...</div>
    <div v-else-if="todosPromise.isEmpty">empty...</div>
    <div v-else>
      <div v-for="todo in todosPromise.value" :key="todo.title">{{ todo.title }}</div>
    </div>
  </div>
</template>

<script>
import promistate from "promistate";

export default {
  data() {
    const todosPromise = promistate(() =>
      fetch("https://jsonplaceholder.typicode.com/todos").then(res =>
        res.json()
      )
    );

    return { todosPromise };
  }
};
</script>
```

Alright, what about react? This requires the use of the usePromistate hook.

```jsx
import React from "react";
import { usePromistate } from "promistate/lib/react";

const api = "https://jsonplaceholder.typicode.com/todos";

export default function App() {
  const [todosPromise, actions] = usePromistate(
    () => fetch(api).then(res => res.json()),
    { defaultValue: [] }
  );

  return (
    <div className="App">
      <button onClick={actions.load}>load</button>
      <button onClick={actions.reset}>reset</button>

      {todosPromise.isPending && <div>pending...</div>}
      {todosPromise.isEmpty && <div>no results...</div>}
      {todosPromise.value.map(todo => (
        <div key={todo.id}>{todo.title}</div>
      ))}
    </div>
  );
}
```

In the docs, I have an entire list of examples in different libraries including React.js, Vue.js, Angular, Svelte, Alpine.js, and Vanilla JS.