A coworker has suggested a similar shape to @hoichi’s solution:
type state<'r, 'e> =
| Ready
| Pending
| Fulfilled('response)
| Refreshing('response)
| RefreshingFailed('response, 'error)
| Rejected('error)
It looks clean, but I can’t help but feel it makes too many assumptions about the relationship of the data. For example, not every UI may want to hold onto old data, while some will and I think it makes sense to make it flexible to support on a case-by-case basis so that each UI can determine how it wants to treat subsequent requests.
I’m still rooting for a more general shape:
{
state: Ready | Pending | Fulfilled | Rejected
context: {
error: None,
response: None
}
}
Realistically, I don’t think there is an illegal state. Having an error and a response is not unreasonable to me.
Below is a quick demo of different UI scenarios which this solution seems more than capable of representing.
@react.component
let make = () => {
let (listResourceReq, fetchlistResource) = RequestStateMachine.useRequest(Transport.listResource.fetchAll)
React.useEffect1(() => {
fetchlistResource()
}, [fetchlistResource])
React.useEffect1(() => {
Js.log(`
This side effect will only fire when request state changes,
which might be more reliable given state is serializable and cheap to compare?
`)
}, [listResourceReq.state])
//////////////////////////////////////////////////////////////////////////////
// Basic results, error, or empty no helpers
switch (listResourceReq.state, listResourceReq.error, listResourceReq.data) {
| (Fulfilled, None, Some(listResource)) => <listResourceList listResource />
| (Error, Some(error), None) => <RequestError error />
| (Ready | Pending, _, _) => <Spinner text="Fetching data now" size="2rem" />
}
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// Basic results but with some helpers for conciseness
open RequestHelpers
switch listResourceReq.state {
| Fulfilled => <listResourceList listResource={listResourceReq->getResponse} />
| Error => <RequestError error={listResourceReq->getError} />
| Ready
| Pending =>
<Spinner text="Loading..." size="2rem" />
}
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// Display the error along with the last data we have
switch listResourceReq.data {
| Some(listResource) => <listResourceList listResource />
| None =>
if isPendingOrReady(listResourceReq.state) {
<Spinner text="Loading..." size="2rem" />
} else {
React.null
}
}
// Error is conditionally displayed below UI
switch listResourceReq.error {
| Some(error) => <ErrorBox message={error.message} />
| None => React.null
}
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// Multiple requests, with helpers for conciseness
let (reqA, fetchA) = RequestStateMachine.useRequest(Transport.ResourceA.fetchAll)
let (reqB, fetchB) = RequestStateMachine.useRequest(Transport.ResourceB.fetchAll)
let (reqC, fetchC) = RequestStateMachine.useRequest(Transport.ResourceC.fetchAll)
// Use effect to fetch all three resources ...
if allFulfilled([reqA.state, reqB.state, reqC.state]) {
<SomePage a={reqA->getResponse} b={reqB->getResponse} c={reqC->getResponse} />
} else if anyPendingOrReady([reqA.state, reqB.state, reqC.state]) {
<Spinner text="Loading..." size="2rem" />
} else {
// Could also be a helper function
let errors = [reqA.error, reqB.error, reqC.error]->Array.keepMap(error => error)
<RequestErrors errors />
}
//////////////////////////////////////////////////////////////////////////////
}
While it’s definitely not as clean, I would wager the flexibility is more important given that in a medium sized application I don’t want to over-engineer how requests can behave.
Anyone want to weigh in on which solution strategy you would go with and why?