This content originally appeared on Level Up Coding - Medium and was authored by Thomas Juster
A developer’s oath: keep my promises … with React
A Promise handling proposal for React developers
NB: That proposal can be transposed to other tools, which I already did for lit-html a while ago here: lit-html-promise.

1/3 − Introduction
Some promises you hold, some others you don’t; it’s fine. As long as both cases are dealt with 😌. Spoiler alert: there’s a code sandbox at the end.
In React projects (and others), I have the sentiment that we keep on de-structuring promises as quickly as possible − usually at component mount − by keeping the result in a state. Like this:
Now I’m wondering:
1. Why rewriting these useEffect or componentDidMount all the time (although some parts could be factorized) ?
2. What’s the urge of unwrapping the value of a promise ?
What if we worked directly with promises in templates 🤔 ? (Which is in itself a factorization 🤫. And would actually be way more functional-programming friendly, although that’s debatable.)
2/3 − Promise folding & mapping
There are two use cases to create promises:
1. When the async action is triggered straight away, like data fetching.
2. When the async action is triggered on demand, like form submission.
There are two use cases to use a promise value:
1. Exhaustively: handling all the states (not asked, pending, failure & success) in one place ; in particular to make sure the error is handled 😒.
2. Not exhaustively: for instance when you need a button to be disabled only when the submission is pending.
To be ergonomic, our component API should address those 2 × 2 use cases.
Side note: I’ve come to a conclusion that a border (among others) between ~Trump and Mexicans~ junior and senior developers is error handling 🤐
1. Async action triggered straight away − data fetching
And for testing purposes, we can test all the promise states by injecting :
- Pending: getUser: (id) => new Promise() never resolving promise
- Success: getUser: (id) => Promise.resolve(…)
- Failure: getUser: (id) => Promise.reject(new Error('Oops'))
2. Async action triggered on demand − form submission
OK, that’s nice. But what if I have more complex needs, like combining promises ?
Promise composition
Well fortunately, Promise is an awesome data type that allows you to transform the success data and the error data. Which means we are free to create promises from other promises indefinitely without triggering more requests. To illustrate that:
const user = fetch(…)
const userName = user.then((user) => user.name)
const userNameWithBetterError = userName.catch((error) => {
if (error.message.length > 10) throw error
throw new Error('some clearer error')
})
And then (🤭) at some point we can de-structure their value exactly and only where we need it.
Here, if the user promise changes, the posts promise becomes immediately pending.
In terms of performance, it might have an amazing impact:
Before: The whole component re-renders 2+ times per promise.
After: The component re-renders once per promise, and only subparts of the template get re-rendered when the promise resolves or rejects.
How awesome is that ?
Now there are probably some caveats I’m currently unaware of.
Thinking about it, I wonder if this approach shares an RxJS caveat: props drilling.
If you pass along promises deep in the component tree and create new promises deriving from props ones on the way down, I don’t know how clear errors are and how easy finding the initial buggy promise is. This could not help tracking bug and tracing problems.
On the other hand, promises stack traces are quite good, and if you don’t pass promises as props you’ll be fine, so … I don’t know. Maybe an ESLint rule to disallow passing promises as props does the trick ?
Thoughts?
3/3 − Feedback time !
- What do you think of this pattern ?
- Is this a true good idea or a false good idea ?
- Would you use this pattern in your company or projects ?
- Am I missing out something that should discourage this idea ?
- Last but not least: should I make a very small lib out of this ? (if so I would add a stand-alone hook as well 😏)
If you are interested by how the promise is represented under the hood, I used a common data type RemoteData I met with Elm package remotedata-http, and translated it to JavaScript/TypeScript:
type RemoteData<Error, Value> =
| { kind: 'NotAsked' }
| { kind: 'Pending', stale: Value | undefined }
| { kind: 'Failure', error: Error }
| { kind: 'Success', value: Value }
I also discovered recently an equivalent type in fp-ts library:TaskEither.
Conclusion
Anyway, if you’ve come this far, thank you very much for reading, I hope it lead − at least − to some questioning of any sort. I added some notes on prior art at the end.
Stay safe 😷, hopefully the pandemic will be soon a (very) bad memory.
Cheers 🤗, and so long 👋 !
By the way, I never mentioned it:
I’m currently living in the east of Paris (and work as freelancer), if you want to discuss stuff over a drink or a project, you can drop me a comment 😊.
And since I keep my promises 🙃, the promised sandbox:
Prior art
The closest react prior arts I found on NPM are use-remote-data (which does too much stuff) and react-use-promise-matcher, in both cases I don’t really like the idea of needing a hook and a component. IMHO it should be a hook or a component. Libraries should remain as simple as possible to allow being efficiently integrated together.
For instance, here since I’m only dealing with plain ol’ promises, you can use other tools that works with promises too, like ts-retry to take care of retry strategies. It’s none of my lib’s business to know what you do with your promises !
Other prior arts − which are good libraries but don’t suit how I approach library usage now:
- react-async-hook: buggy data structure allowing incorrect states. For instance, { loading: true, error: new Error(…), result: '…' } is a possible state, which has no place in the real world 🤷.
- react-query: Same issue, plus it does too much stuff while closely binding them to react. In my opinion, features implementations like caching do not require React and in general those features should be decoupled from other tools/libraries.
- use-remote-data: No exhaustiveness in state mapping (no error handling), too much stuff taken care of and tightly coupled to React as well.
- swr: Same. But different. But still the same. 🕵️
A developer’s oath: keep my promises … with React was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Thomas Juster

Thomas Juster | Sciencx (2022-02-13T15:22:18+00:00) A developer’s oath: keep my promises … with React. Retrieved from https://www.scien.cx/2022/02/13/a-developers-oath-keep-my-promises-with-react/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.