Add a RemoteData/AsyncData type to Belt

I find myself adding such a data-structure on all of my projects, so that my React component states can hold some request current status:

type 'a t  =
  | NotAsked
  | Loading
  | Done 'a

val mapU: 'a t -> ('a -> 'b [@bs]) -> 'b t
val map: 'a t -> ('a -> 'b) -> 'b t

val flatMapU: 'a t -> ('a -> 'b t [@bs]) -> 'b t
val flatMap: 'a t -> ('a -> 'b t) -> 'b t

val getWithDefault: 'a t -> 'a -> 'a

val isLoading: 'a t -> bool
val isDone: 'a t -> bool
val isNotAsked: 'a t -> bool

val eqU : 'a t -> 'b t -> ('a -> 'b -> bool [@bs]) -> bool
val eq : 'a t -> 'b t -> ('a -> 'b -> bool) -> bool

val cmpU : 'a t -> 'b t -> ('a -> 'b -> int [@bs]) -> int
val cmp : 'a t -> 'b t -> ('a -> 'b -> int) -> int

Would you find it interesting to have that in Belt under a module named AsyncData or RemoteData (like Elm)?

3 Likes

I would definitely be in favor of this, that would make interfacing with different libraries much easier as it’s a pretty common need.

Allow attaching a callback to this, and you have a home-grown promise data type :slight_smile:

However I think it needs an error case as well:

type ('a, 'e) t =
  | NotAsked
  | Loading
  | Done of 'a
  | Err of 'e

…or similar.

Not sure types like this are not dependent on how things are done in your app. E.g., you might handle errors in the API (so that it never reaches your components), or you might want a Reloading of 'a variant, besides Loading. Having this module in a standard library is rather opinionated.

What I do is that I combine that with the result type: t<result<'a, 'e>>

1 Like

The error handling can be managed with AsyncData.t<result<'a, 'b>>.

The reloading part can be expressed fairly simply:

type state = {
  data: AsyncData.t<'a>,
  reloadData: AsyncData.t<'a> 
}

let firstLoad = {data: Loading, reloadData: NotAsked}
let subsequentLoad = {data: Done(x), reloadData: Loading}
let subsequentLoadDone = {data: Done(x2), reloadData: NotAsked}

Yeah, but:

  1. reloadData is actually never Done(_), so the type is a bit of a lie.
  2. Not sure if I pattern matching on 2 variants reads better than on a single variant (but maybe it’s no big deal).
  3. If I have to jump through hoops to customize the standard AsyncData, why not just roll my very own AsyncData. The set of helpers you propose are not that hard to write—and you’d probably have to write them for the state record anyway.
  1. reloadData is actually never Done(_), so the type is a bit of a lie.

It could, if for instance you want to highlight the diff between the previous and new data :slightly_smiling_face:

  1. If I have to jump through hoops to customize the standard AsyncData, why not just roll my very own AsyncData. The set of helpers you propose are not that hard to write—and you’d probably have to write them for the state record anyway.

My idea would be to have that data-type a common building block that works as-is for the most common cases, can be shared across libraries, and allows you to build abstractions upon (e.g. reload, pagination…) with little work.

One option to allow wide reuse across libraries would be to use a polymorphic variant type instead of a normal variant. Then users could add more cases like `Reloading.

1 Like

That’d be an option, but the utility functions wouldn’t be general enough IMO

This comes from a userland Elm library right?

Btw, I’m guessing from the lack of canceled state here that you’re using a promise-like mechanism in your React component. In case you are: don’t forget to use a cancelable paradigm instead; otherwise it causes leaks and bugs.

Regarding this data type itself: like the above comments said, I also am not too sure about this extra wrapping when

  1. Such loading itself is simple enough to be your own app’s little util
  2. There’s differing opinions on what states to have and not have (I don’t think using poly variant cuts it here though, and yes, reloading makes these apis more misleading here and there).
  3. It’s not much of a ROI vs having a dedicated variant inside your app.
  4. The various data fetching libraries likely won’t build on top of this (but a product could), because it’s a little abstraction. They’ll likely just expose their own data type. Not much reuse for an extra indirection I think.

We also wanna encourage putting extra care into the UX, e.g. progress indicator and data-specific loading UX. This is a little bit much of a pull toward not doing those.

2 Likes

If using the AsyncData.t<result<'a, 'b>> type as @bloodyowl mentioned, then a possible state could be Done(Error(`Cancelled)) or something like that.

Exactly, or you can go back to NotAsked depending on the cancellation nature/reason.

Uff, even in Elm the RemoteData package is a third party dep, isn’t it?

Yeah. I wanted to have a built in (so that it doesn’t make an extra dependency) way to represent requests status (mostly in React components), but if that seems inappropriate I’ll just release it on my own :smile:

Hm, I kinda understand, but on the other hand I don’t see why adding a dependency should be a problem?

Absolutely! It would definitely be easier to decide on stdlib additions when it’s based on a heavily used third party library or something.

Maybe i find time to finally get our ReScript package index on rescript-lang.org up and running, so it’s easier to find those packages.

1 Like

That’s 3 wrapping and 2 concepts to express something that’s actually not too clear from reading and raises other questions (is cancelled an error? Does it count as done? Why does it look different? What other composition is there? etc).

But like Patrick said, just like for Elm’s RemoteData, I think we’re better having this in userland (at least for now).

1 Like

This is very tricky to make part of a stdlib. Over the course of several projects I went through three iterations of the same variant and eventually ended up with a completely different design.
Being able to move on and learn from previous designs is a lot easier in your own software or a userland library.

1 Like

For those interested: https://github.com/bloodyowl/rescript-asyncdata

5 Likes

Also added some React hooks helpers for reload and pagination that let’s you configure through a merge parameter how you like to reconcile the current and next data https://github.com/bloodyowl/rescript-asyncdata#react-hooks :slightly_smiling_face: