1. 13
  1.  

  2. 2

    I can see what the author’s getting at here - RemoteData doesn’t capture more complex use cases, but I don’t think it’s intended to. RemoteData models a single loading event. Dropping it completely for more complex use cases strikes me a bit as throwing the baby out with the bathwater. I suspect a cleaner option would be to compose RemoteData with additional state and data structures to build up to the use cases he describes.

    For existing data and a refresh request, that might look something like:

    type RefreshableData
         = Refreshable a (RemoteData e a)
    

    For a large number of requests, you could do something like:

    type alias RequestGroup e a = List (RemoteData e a)
    

    And it becomes pretty easy to derive the states from the list:

    {- This can be optimized, but if you have enough data
        on the page for it to be an issue, you probably have bigger UX problems -}
    
    isPartialLoading requestGroup =
        (List.any RemoteData.isLoading requestGroup)
            && (not (List.all RemoteData.isLoading requestGroup)) 
    

    Of course, in these examples, the states aren’t represented as union types, so you lose come compiler checking that you’ve handled all states. That said, I’ve worked on some pretty complex interfaces, and I have never needed or wanted something that would validate that we had code to handle all of:

    • empty, general error, and request pending
    • empty, general error, and request pending for a subset of the data
    • empty, error for a subset of the data, and request pending
    • empty, error for a subset of the data, and request pending for a subset of the Data
    • data cached and general error
    • data cached and error for a subset of the data
    • data cached and request pending
    • data cached and request pending for a subset of the data
    • data cached, general error, and request pending
    • data cached, general error, and request pending for a subset of the data
    • data cached, error for a subset of the data, and request pending
    • data cached, error for a subset of the data, and request pending for a subset of the data

    That said, if you really wanted it, you could take put your composition of RemoteData with additional state into it’s own module, make it an opaque type and enforce correct transistions between states by limiting the exposed API.

    I think all of this would be clearer with a specific use case in mind. The exercise in the article strikes me as a case of premature generalization. It seems like it’s trying to solve all possible problems rather than anything specific.

    I also have questions about what kind of cache is being referenced in the article, as I have some fairly strong opinions about caching data in client-side applications. (TL;DR: Don’t, the browser can already do this for you.)

    1. 3

      I also have questions about what kind of cache is being referenced in the article, as I have some fairly strong opinions about caching data in client-side applications. (TL;DR: Don’t, the browser can already do this for you.)

      I would love to hear more about this, because two problems have me stuck with client-side JS data caches in my apps. And I love deleting code.

      1. Embedded documents. Every app I’ve worked on denormalizes the data we fetch to cut down on HTTP requests. Is it cheap enough with HTTP/2 to send lots and lots of requests? Even then, it would mean a bunch of sequential round-trips for each child document that depends on its parent.

      2. Consistency. If two parts of the app load the same data at different times, we can get different responses, and have weirdness in the UI. With a JS cache, when we update a document, we can have every dependent piece of UI re-render and be consistent.

      1. 3

        You should take this all with a big grain of salt, because I have not had the bandwidth to implement most of these ideas in practice, due to the usual constraints around priorities and limited time. I’ve just been increasingly bothered by the complexity of implementing caches in the client, or alternatively the ugly behavior that results when they’re implemented naively, and on reflection, I think it’s mostly unnecessary. I have probably missed some corner cases and I suspect there are many apps where some amount of specific, targeted caching might still be useful for a subset of APIs or pages.

        With all of that in mind, I will say that #1 is probably the best reason I’ve seen for having a client side cache. I think in that case it’s worth looking at usage patterns to be sure it’s really providing benefit. If the individual requests your app is making in between big de-normalized requests don’t overlap much with the de-normalized data, the client side cache isn’t going to buy you much, although neither is the browser cache. Or if you’re always making large de-normalized requests, you’re still probably not getting a caching win, unless you have a way of structuring those requests to specify what you already have data on.

        I think there’s a lot of promise with HTTP/2. The single TCP connection is nice on it’s own, but there’s also the potential to do interesting things like make a request for a de-normalized structure that only contains the relationships between resources, and then have the server push the actual data from the resources individually. That way they’re cached individually, and the browser will actually cancel the push for resources it has already cached. Running some experiments with that is somewhere medium-high on my TODO list.

        #2 is tricky. If you data doesn’t change often, it’s not as big of a deal, or if you don’t often/never show the same data twice on the same page. One thing you can do if it’s still a problem after taking both of those into consideration is to track ongoing requests at the API layer. If you’re using something like promises, that means when a request comes in while another is still outstanding, you should be able to return the promise from the first request to the second caller, and just share the API call. If the first request has completed already, the browser should have the data in it’s cache (assuming the data has a time-based cache rather than something like etags).

        1. 2

          This is awesome, thank you so much for taking the time to share your thoughts @mcheely!

          Solving the round-trip part of #1 with HTTP/2 server push seems like it could be so damn magical and cool. In my most common case of hard-to-cache embedding – “load a list of items” and then “load a detailed view of one item” – it seems like a drop-in solution.

          For #2, I actually hadn’t thought about races! I was thinking more about the case where data rendered on one part of the screen becomes stale, but there’s no way for the browser cache to tell that part of the UI to re-render, so it stays stale. I guess, since we hope it to be cached, maybe I just need to adjust my thinking to re-render more things more often. Cheap most of the time, since it’s cached, and expensive when it should be expensive anyway. Huh.

          (solving the races by having a client API layer managing promises across the whole app starts to feel like a dangerously tempting place to add new features like… caching :P )

          I think in that case it’s worth looking at usage patterns

          I think it all comes back to this, for me. Building an app usually feels like a process of discovery to me; top-down plans don’t survive long. The usage patterns can be pretty unstable, and it can get painful surprisingly quickly to be completely naïve about loading data.

          …It’s appealing to imagine that a client-side cache, made hopefully robust through explicit modeling of all the possible states of each piece of data, can provide a 90% solution in a general way. Coupled with things like graphQL or PostgREST, you just build stuff and it works reasonably well, for free-ish.

      2. 2

        Thanks for reading and thanks for the feedback.

        I think all of this would be clearer with a specific use case in mind. The exercise in the article strikes me as a case of premature generalization. It seems like it’s trying to solve all possible problems rather than anything specific.

        As I say in the post, “States, events, and transitions should reflect the needs of the application”. I listed those states as an example because the last four applications I’ve built have needed all of these states. I tried to use RemoteData for two of those applications and ran into the problems I describe in the post. These apps do not strike me as complex and three years of dealing with these states led me to assume they were common. One example is an app that lists financial transactions. The app periodically refreshes the list. A loading icon is shown next to the list header during each refresh. Error messages are shown above the list if a refresh fails. That’s half of the states in that list already. On top of that, the user can make inline edits to the transaction details. When the updates are committed, a loading icon displays next to the transaction title. Errors related to the update (e.g. network failure) are displayed under the transaction title. That’s all of the states in that list. But the point of the article is not to dictate states to the reader. Again, “States, events, and transitions should reflect the needs of the application”. The point is that you cannot oversimplify the problem just because the result of that oversimplification looks nice in a blog post or a tweet.

        RemoteData models a single loading event. Dropping it completely for more complex use cases strikes me a bit as throwing the baby out with the bathwater.

        I agree that these states map closely to the HTTP request/response lifecycle. As I said in the post, “RemoteData models a stateful cache of data in terms of a stateless transfer of data, REST.” The original RemoteData post clearly states that the pattern is intended to model the cache, not the request/response lifecycle. That is why that post starts by evaluating existing patterns for modeling cached data and then offers RemoteData as an alternative. Notice that these posts place RemoteData in the model and that the view functions consume RemoteData - cache state, not request state.