-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Refreshing
constructor to RemoteData
?
#9
Comments
So...just thinking aloud about this. I could add a No conclusions yet, but here's the table of what that would look like. It seems to me that this reveals useful information... 🤔
|
Thanks for taking the time to reply :-) I like the idea to represent the data reload as a transition between two state, and not a state.
I agree. But one could argue that this could be solved by a I'm gonna try implementing this idea in my application then we'll see if some patterns emerge. |
I had considered using a pair of One way to solve them seems to be by writing a selection of helper functions, such as:
Basically shifting the approach from "What is the current value for the remote data?" to "Do I have any data to work with and/or do I have any error to display?" Any thoughts on this approach? Some of the considerations are:
Edit: And the missing piece is some kind of "merging" function. Something like:
|
I am afraid that the transition from one RemoteData state to the next would not solve my issue. I directly save the What is missing for my use case is a discrete state (no transition), in which the data is there, but the app is loading (so I can also show my spinner). Could an alternative solution be to change the signature of to this?
That way, one only needs to add the underscore to the loading state, if one does not care whether the data is there:
And the helper to get the data would be relatively simple:
|
@wintvelt If you use a pair of
The main issue with If you want, why not try a custom type like:
|
@robertdp I get that, and I appreciate the upside of non-breaking changes by adding extra helpers. But I still think that saving previous + new state in the model is an overly complicated solution. Changing a from a static state model to a transition state model brings all kinds of nastiness with it: e.g. if a user clicks Load twice, they will still lose their data, unless of course we catch that in our code, and do not update the previous The current |
@wintvelt Do you have an example of a situation that is not handled by the helpers above?
I can see that a |
How about something like:
which would get used like
This allows easy pattern matching on the |
Yeah, that might work. Perhaps a separation is needed between the request status, i.e. the current And the state of the data, which could simply be there, or not. Regardless of the current state of the Request perhaps. Under the hood, this could be solved with an (opaque) type of any kind, possibly with a tuple of 2 |
To combine the proposals, what about: type RemoteData e a =
NotAsked
| Loading (RemoteData e a) -- previous state
| Failure e
| Success a |
@ericgj That would make the following a valid value: The main point is that I suppose Let me just make it clear that I also want the behaviour of If you want to track the last
@krisajenkins I'd be interested to hear your thoughts at this point. |
Good points @robertdp, I retract my suggestion. My first thought was just what you said -- wrap it instead of changing RemoteData itself. |
@ericgj That makes you smarter than me. My first thought was the same as @krisajenkins -- use a pair of |
After re-reading the thread I agree that What would solve it is maybe not some tuple of new + current request, but simply a way for data to persist, like:
Either in the library with helper getters and setters, or home-made. One drawback I see in this approach, is that the actual data is duplicated if the latest request was successful. There are actually 2 distinct requirements in the thread and the desire for reloading.
|
Or just manage it separately in your model. type alias Model =
{ pageData : WebData PageData
, isRefreshing : Bool
}
view : Model -> Html Msg
view model =
model.pageData
|> RemoteData.map (viewPage model.isRefreshing)
|> withNoSuccess viewPageNotAsked viewPageLoading viewPageError
viewPage : Bool -> PageData -> Html Msg
viewPage isRefreshing data =
-- render spinner if isRefreshing, then the rest of the view
-- helper for easier pipelining
withNoSuccess : a -> a -> (x -> a) -> RemoteData x a -> a
withNoSuccess notasked loading failure data =
case data of
NotAsked ->
notasked
Loading ->
loading
Failure x ->
failure x
Success a->
a
Not ideal since it does allow some meaningless states, but there's probably just 3 places in the code where you'd need to touch |
Meaningless states should not be allowed. Especially not after Richard Feldman's talk (I heard that he once clotheslined a nun for allowing invalid states -- true story). And wrapping it up in a new type lets us use encapsulation to hid all the gritty details away in a module somewhere, making certain that inquisitive developers can't take it to bits and put it back together the wrong way. |
Agreed that a type is needed which meets Richard Feldman's "making impossible states impossible" test. But it should at the same time also fulfil requirements. I am not so sure hiding the implementation behind an opaque type is ideal. There is merit in allowing others to do case statements on branches, if they are designed well. How about:
I think this makes impossible states impossible.. In this setup, In view functions, you could ignore the |
@wintvelt Both of those types are essentially the same, with the exception of the Does this accomplish something practical that the |
Sure, but as I'm sure you know, there's no absolutes in programming, just deadlines ;) It's always a tradeoff. Not to say we shouldn't keep looking, as library developers. But as an application developer it's a question of what are we actually talking about. Spinner state I can't really justify hours of design discussion on, as interesting as it is. I'd much rather "control the valid states in the app until something better comes along" than wait for the perfect abstraction, knowing full well it may mean quite a bit of changes later. Or push back on the feature: use (Not to quell this very fruitful discussion, but I thought it needs to be said from a practical point of view, the quest to make impossible states impossible does not have to block). |
The trade-off between the different models is: either have more than 1 constructor for loading and failure states, or allow the type to hold the same data twice.
Agreed. |
I really like the solution with the But isn't What about adding a section in the documentation explaining both approaches: using Just for the record in an app I work on I re-created type LoadingStatus a
= Loading (Maybe a)
| Loaded a
type WebData a
= NotAsked
| Failure Http.Error
| Data (LoadingStatus a)
getData : WebData a -> Maybe a
getData webData =
case webData of
NotAsked ->
Nothing
Failure _ ->
Nothing
Data loadingStatus ->
getLoadingStatusData loadingStatus
getLoadingStatusData : LoadingStatus a -> Maybe a
getLoadingStatusData loadingStatus =
case loadingStatus of
Loading data ->
data
Loaded data ->
Just data Now I think that both types |
I'd love to hear how people are getting on with the various solutions in this thread. What works and what doesn't work? In the interim, I'm adding a |
I'd be interested to hear the thoughts of @krisajenkins on this. Is it likely that 'refreshing' will make it into this package? |
For what it's worth, I created my own package for a similar purpose to this: elm-remote-data It has three "types" of remote data:
I successfully used this package in one fairly large project, but I'm still not quite happy with it. At this point, it seems like every project needs its own slightly different All the variations share similar properties but if they're all different union types, they all need their own set of practically identical utility functions. Also, I'm not very happy with the names I came up with, they're pretty long. I might look into doing this with extensible records... |
I wrote a little module that wraps RemoteData to fit the following requirements:
module RemoteData.Refresh exposing (RefreshableRemoteData, isRefreshReady, refresh, refreshSend)
import Http
import RemoteData
type alias RefreshableRemoteData data =
{ current : RemoteData.WebData data
, upcoming : RemoteData.WebData data
}
-- are we already refreshing?
isRefreshReady : RefreshableRemoteData data -> Bool
isRefreshReady rrd =
not (RemoteData.isLoading rrd.upcoming)
-- only update if the new response is not a failure when we have a previous success
refresh : RefreshableRemoteData data -> RemoteData.WebData data -> RefreshableRemoteData data
refresh rrd upcoming =
case rrd.current of
RemoteData.Success _ ->
if not (RemoteData.isFailure upcoming) then
{ rrd | current = upcoming, upcoming = RemoteData.NotAsked }
else
{ rrd | upcoming = RemoteData.NotAsked }
_ ->
{ rrd | current = upcoming, upcoming = RemoteData.NotAsked }
-- make sure we're not overloading the backend, if query is already loading, skip scheduling a new one
refreshSend rrd msg call =
if isRefreshReady rrd then
( { rrd | upcoming = RemoteData.Loading }, Http.send msg call )
else
( rrd, Cmd.none )
|
It might be worth to check out an answer by @raveclassic (author of
|
I went back and forth with trying to make a nice implementation of this, though my use case was regarding infinite scrolling where I needed two extra branches: I think in Elm my preferred option is to build my own type that wraps |
A solution that's been working well for me, and I don't think I've seen put forth in the thread so far, is collapsing the module Api.Data exposing
( Data
, Value(..)
, notAsked
, loading
, succeed
, fail
-- ...
)
type Data a
= Data (Internals a)
type alias Internals a =
{ value : Value a
, isLoading : Bool
}
type Value a
= Empty
| HttpError Http.Error
| Success a
-- CONSTRUCTORS
notAsked : Data a
notAsked =
Data { value = Empty, isLoading = False }
loading : Data a
loading =
Data { value = Empty, isLoading = True }
succeed : a -> Data a
succeed a =
Data { value = Success a, isLoading = False }
fail : Http.Error -> Data a
fail error =
Data { value = HttpError error, isLoading = False }
-- ...
|
After @krisajenkins article How Elm Slays a UI Antipattern I read a discussion talking about adding a
Refreshing a
constructor to theRemoteData
type.I need this too! I'd like to display a "spinner" load indicator while data is refreshing (data was already loaded at least once) but continue to display the view.
I have the motivation to implement it, but @krisajenkins I'd just like to know it it's suited.
Thanks for this good library ^^
The text was updated successfully, but these errors were encountered: