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