Isomorphic handling of promises in libraries like react.js, vue.js, angular, svelte etc.

Published 9/13/2020

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.

Check out my e-book!

Learn to simplify day-to-day code and the balance between over- and under-engineering.

It works by first defining your promise like this:

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.

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

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.