jamesplease / react-request

Declarative HTTP requests for React
MIT License
362 stars 21 forks source link

add the ability to "set" the cached value #176

Open ianstormtaylor opened 6 years ago

ianstormtaylor commented 6 years ago

It would be nice if there were an extra render prop that allowed you to "set" the cached value to a new value, in the case of updating the cache for a mutation. This way you could nest a patch inside a get and use the update method to update the cache without having to re-request. (Same goes for post/delete as well.)

jamesplease commented 6 years ago

:wave: Hi again @ianstormtaylor !

I've thought about this, but I have intentionally omitted a feature like this from the lib. Your other issue mentioned Apollo, so you are possibly accustomed to Apollo's system of writing updates to the store that works sort of like this?

If that is not the case, then the rest of this reply will not make any sense, and you can probably stop reading now and just start leaving a new reply 😉 .

But if that is the case, then I'm going to go ahead and write out why this feature is omitted here. It may be useful for future visitors, and also myself 😛 . The tl;dr is that React Request alone is not a full replacement for Apollo's Query component and InMemoryCache, so not all of the patterns that you use there apply if you are just using React Request.

The reason for this is that updating a cached response based on another response requires that there exists a relationship between the two requests. This is something that Apollo has, but React Request does not have.

When you are working with GraphQL, there are object types, unique identifiers for objects, and a well-defined response structure, which allows for you to build a normalized data store (InMemoryCache). When you update a normalized store over HTTP, all of the requests have a natural relationship, as they represent CRUD operations on the same collection of data. Naturally, then, providing something like update after a query to change another cached response makes some sense.

React Request does not have any notion of types, so there cannot be a normalized store, and consequently no relationships between requests. Each request is independent from one another by design.

Once you do have a normalized store where operations on the store occur over HTTP, the question then becomes: how do you manage a situation where one request affects the result of the other? There are different approaches to that, and I came up with an approach that is different than Apollo's, but one that solves some of the problems people have run into with update (from what I have read from the Apollo issue tracker). It also does not require updating the response cache directly. Because I think this pattern is preferable to Apollo's, I didn't provide the ability to modify the store so as not to encourage someone to go down the same path Apollo did, but maybe I should provide the option.

Also, I know that I was a little vague there; it has been some time since I've thought about it, but I can dig up my notes if you are curious to hear more about that topic.

Anyway, in a diagram, my understanding of the relevant pieces of Apollo is that it is kind of like this:

apollo-link-http  + <Query/>   => InMemoryCache

The description for what these do looks like this:

ApolloLinkHTTP: a wrapper around fetch that with other links gives you caching and deduplication

<Query/>: a nice way to use ApolloLinkHTTP in React

InMemoryCache: a normalized data store that introduces notions such as relationships between requests


The analogous React Request stack looks like this:

fetch-dedupe  + <Fetch/>   => ?

fetch-dedupe: a wrapper around fetch that gives you caching and deduplication

<Fetch/>: a nice way to use fetch-dedupe in React

?: A doesn't-yet-exist generic data store that introduces new problems such as relationships between requests

Note that the mapping between the two is not perfect. You can't use Query without a normalized store afaik, but <Fetch/> was designed to be standalone, but with "hooks" (beforeFetch and afterFetch) to sync it to any store.


I am still working on my own version of ?, and the plan was that it would not need a cache update feature like this, because I think there are better ways to do it. Although I have the concern that by adding it I may encourage Apollo's system of updates, but maybe it is better to provide the functionality and add docs?

Also, I admit that I may be completely wrong about the need for this feature. Maybe it is necessary for all types of normalized stores, and that it is the best way to manage updates. That's totally possible, and I'd love to chat more with ya if you are interested!

If you have made it this far, thanks for reading this giant tome 📖

ianstormtaylor commented 6 years ago

Hey @jamesplease, thank you for such a quick and detailed reply!

I totally agree with you about avoiding the complexity of Apollo's normalized store. I think actually even with all of the extra schema information GraphQL provides they're still going to have a hard time trying to do magic things with insert/delete operations, and it leads to a lot of craziness.

For context, I'm currently using react-refetch, but I'd prefer a solution that has render props as its first-class API. And have been experimenting with react-apollo since the API I'm using exposes both GraphQL and REST. I think react-refetch has a nice and simple "data updating" story, whereas react-apollo has a super complex one, which is one of the reasons I've decided not to switch to GraphQL.

Do you have an idea (or just a quick sketch) or what you think your API would look like?


From my perspective, if the request caches with a "cache key", then allowing the user to set/evict by that cache key is all the library needs to do. It doesn't need to try to get fancy like Apollo and do any automatic updates. Of course all of that automatic stuff could be built on top, but the bare minimum is to be able to modify the cache.

In a way though, if we're already caching by url+body+method+etc. then allow people to set/evict that cache isn't actually getting any "more opinionated" as far as the data goes. Does that make sense?

ianstormtaylor commented 6 years ago

More abstractly, what I'm arguing for is that the cache be able to...

And then each <Fetch> just creates a sort of "closured binding" to cache for a specific key, and exposes a set of get, set, remove operations for it. The get is implicit, since it's just the data value of the response. But the set/remove would be exposed as arguments to the children function.

This doesn't actually increase the "magic" of the library, since it's still only dealing with caching with unknown-to-it keys. But it does allow people to have full control to build more complex behaviors themselves.

jamesplease commented 6 years ago

First, a response to the original issue's question...

More abstractly, what I'm arguing for is that the cache be able to...

I get where you are coming from, and I'm open to adding this API for completeness. With that said, I would want to add some best practices docs around using it. I don't think that most users need those APIs, and I wouldn't want newcomers to think that they need to be writing some crazy cache management solution to get value out of this library 🙂


And now some other things...

Do you have an idea (or just a quick sketch) or what you think your API would look like?

Sure, I'll do my best! The use cases I can think of always involve a list of resources, such as a list of favorite books. Let's say that this list was returned by a network request.

When the user creates a new book, you typically want to display this new favorite book in the existing list of favorite books. Of course, you can always make another request to get the latest favorite books from the server, but sometimes developers want to update the app synchronously. In Apollo, you would use update to modify the query for fetching favorite books to now include the new favorite book.

Likewise, for deletes, you update the query for fetching favorite books to remove that favorite book.

The component that renders the list is then subscribed to updates to the fetching-favorite-books query. And because you are modifying the response of that query, the changes from the updates and deletes are always displayed to the user.

This API couples the the list itself (favorite books) with the HTTP request to retrieve the list (which is not an inherently bad thing; that is intended to just be a neutral description of the design of the API).

The alternative approach that I prefer is to introduce a first-order notion of "lists" in your store. Using lists, you can solve this situation and a number of other situations where users want lists of resources on the client (such as "selecting" resources) in a way that I find to be elegant.

So, in addition to the normalized section of the store where resources live, you also have a section with lists. Lists can be polymorphic, and you get a similar API for modifying lists.

With lists, the above situation looks like this:

  1. user fetches the array of resources with a network request, and sets them on a list (lets call it favoriteBooks
  2. when the user adds a new favorite book, you add it to the list
  3. when the user deletes the book, you remove it from the list

The UI is subscribed to changes to the list rather than from the initial request to get the lists.

There are some changes to how you structure your network requests to account for this, but we have been doing this pattern on a number of apps at Netflix for over a year now, and it has worked out really well for us.

I recognize that naming things can be difficult, and when you use lists, you need to come up with a name for the list. In my experience, the lists of resources that folks work with in apps usually have a natural name that the developer is already using in their head (favorite books, new books, user's books, "search results," and so on).

In my opinion, the benefits to this approach (which I have been light on in this response, but I can elaborate on further if you are interested) outweigh the cost, but I can totally understand why someone may disagree 🙂


By the way, the normalized store that I am building, Standard Resource, is still a WIP, but the docs are basically ready for the first release. The store includes a separate section for lists ("groups" in Standard Resource parlance), which is why I am mentioning it. You can see the docs here if you are interested in reading more about it.

The design goals of Standard Resource are:

  1. small size, so that the impact on load times is low (less than 5kb gzipped)
  2. a small API surface area, so that learning the library is straightforward
  3. flexible to support specifications such as GraphQL or JSON API, but able to handle inconsistent APIs as well (like bad REST APIs, for instance)

My intention is ultimately to get an Apollo-like developer experience that lets you work with any data transmission format. In my opinion, very few of Apollo's most useful features require GraphQL, and I think it is a shame that they didn't make it more generic!


Oh, and I should note that React Request is being developed separately from Standard Resource. I never want to merge the two, or make them dependent on one another, or anything like that 😅

ianstormtaylor commented 6 years ago

The alternative approach that I prefer is to introduce a first-order notion of "lists" in your store. Using lists, you can solve this situation and a number of other situations where users want lists of resources on the client (such as "selecting" resources) in a way that I find to be elegant.

This sounds very interesting. You could even have the lists be able to define a sort comparator at creation time that would be insanely helpful for automating the adding aspect. This does sound promising, and like it could be the start of the better abstraction that solves lots of the Apollo issues around adding/removing.

In my opinion, very few of Apollo's most useful features require GraphQL, and I think it is a shame that they didn't make it more generic!

I agree with this over 100%. After doing another round of research into GraphQL I kind of feel like this about a majority of the ecosystem's benefits, not even just Apollo.

Oh, and I should note that React Request is being developed separately from Standard Resource. I never want to merge the two, or make them dependent on one another, or anything like that 😅

My intention is ultimately to get an Apollo-like developer experience that lets you work with any data transmission format.

Very fair. It sounds like it will be interesting. I guess maybe what I'm looking for is too opinionated for react-request, and almost not opinionated enough for standard-resource, so maybe it needs to be its own new thing instead. Although maybe I can just keep using Apollo, but heavily restrict how it's used to get some of the benefits.

jamesplease commented 6 years ago

This sounds very interesting. You could even have the lists be able to define a sort comparator at creation time that would be insanely helpful for automating the adding aspect. This does sound promising, and like it could be the start of the better abstraction that solves lots of the Apollo issues around adding/removing.

I agree that this would be useful. For now, to keep the API simple, I opted for users needing to sort manually at the moment, which has been working fine for us (we are using something similar to Standard Resource on some teams at Netflix).

I agree with this over 100%. After doing another round of research into GraphQL I kind of feel like this about a majority of the ecosystem's benefits, not even just Apollo.

:+1:

I guess maybe what I'm looking for is too opinionated for react-request, and almost not opinionated enough for standard-resource, so maybe it needs to be its own new thing instead.

That may be true. With that said, I am totally open to adding the additional cache methods to this library. I know that I've written a lot of words in this issue that are a little tangential to your original question, so I just want to make sure that I communicated that 😅

If you think that would give you the API that you need to build the application you want to build (or at least to be a building block for it), then that is another option for ya'. If you have the time to open up a PR, I'd be happy to review it.

But if you decide to stick with Apollo, then no worries at all! I appreciate you stopping by this project and sharing your thoughts :v:

jamesplease commented 6 years ago

Just poppin in here to summarize the huge walls of text above:

I am open to this change, but I don't have the time to implement anything myself. PRs are welcome. Thanks!

ilijaNL commented 6 years ago

An example could be very helpful in this situation:

With lists, the above situation looks like this:

user fetches the array of resources with a network request, and sets them on a list (lets call it favoriteBooks when the user adds a new favorite book, you add it to the list when the user deletes the book, you remove it from the list The UI is subscribed to changes to the list rather than from the initial request to get the lists.

There are some changes to how you structure your network requests to account for this, but we have been doing this pattern on a number of apps at Netflix for over a year now, and it has worked out really well for us.

As far I understood, you mean to have your own internal store, whenever you fetch new data, insert that data to the store and when you mutating it, manually updating it?

I wonder how @jamesplease inserting the initial data to the store after the requests has been made since there is no way to know if data comes from the cache or internet.

Edit: Found the documentation: https://github.com/jamesplease/react-request/blob/master/docs/guides/integration-with-other-technologies.md

Any examples how to integrate it with Redux Resource ?

jamesplease commented 6 years ago

Hi @Rusfighter , thanks for the reaching out! I'm happy to help, but I don't think this issue is the right place for this conversation. Would you mind opening another issue here, or on the Redux Resource issue tracker?

Thanks!