Apologies if this has been raised elsewhere, but I spent quite some time searching and couldn't find anything coming at this with quite the same angle I want to use.
I want to argue that deprecating both apollo-link-state and local resolvers from core simultaneously does serious damage to the powerful legacy API adoption story that Apollo and GQL has thus far been able to boast. By "legacy API adoption story" I mean the ability to start using Apollo without incurring tech debt in the client prior to a GQL server becoming available. This is such a valuable story because it empowers front end devs, who are often the main advocates for GQL, to start to demonstrate value to the entire cross-functional team without having immediate dependencies on back-end buy-in.
Noticing that local resolvers had been built into core in 2.5, I decided the time was right to adopt Apollo client as our primary state management solution. In part I was spurred on by this excellent blog post about successful adoption at Trello. I built our solution on a v3 RC. When v3 officially came out it was quite a shock to see the deprecation of the local resolvers feature, so soon after its promotion into core.
The reason I find local resolvers such a powerful and preferential solution is that it allows me to build my client (almost entirely) agnostic of where the graphql service exists. Today, it happens to be in the browser alongside the UI code consuming it, but at some point in the future we will have a real service running across the wire. When that point comes I expect to need to make almost no changes to my client in order to consume it (removing some @client
directives and adding the GQL service URL should be about it). Compare this with the disruption that would be caused if the client had originally been written to use the rest link (as pointed out in the above blog post) or the new type policies strategy being advocated.
(The potential to use most of the resolver code in a real node GQL service is also significant but incidental here.)
On type policies - I can kind of see how they could be used to get basically equivalent behavior, based on the suggestion here https://github.com/apollographql/apollo-client/issues/6852#issuecomment-674984235, but it's less clear to me how this would look at scale. That is, what are the patterns to follow to migrate a local resolver GQL service solution over to a new type policies solution? I appreciate that there is another issue open tracking outstanding documentation https://github.com/apollographql/apollo-client/issues/6711 - I do hope someone will write a blog post or doc chapter to illustrate this soon.
I'll finish with a few options that occur to me for ways forward, for consideration. The aim is to provide developers such as myself who have adopted Apollo on the client prior to the availability of a GQL server to continue to do so using version 3 and beyond without incurring technical debt in the client.
Thanks for all of your time, work and thoughts.
The reason I find local resolvers such a powerful and preferential solution is that it allows me to build my client (almost entirely) agnostic of where the graphql service exists.
I agree. My current (and still shaky) understanding of the new paradigm (ReactiveVar / TypePolicies) makes me bleed data access layer knowledge (in my case: apollo client) into the presentation layer.
Namely: My presentation layer needs to know if data is local or remote because the way to issue a mutation is different (gql-mutation vs function call).
@vigie If you're referring to ApolloLink
when you talk about the local link, I can reassure you that we have no plans to deprecate ApolloLink
. It's still the best way to hook your client up to arbitrary APIs (GraphQL or otherwise), which is a big part of the legacy API adoption story, as you put it.
Removing local resolvers is something we'd like to do at some point, but we realize we don't have a full replacement yet, so that deprecation and removal is still a ways off.
@vigie If you're referring to
ApolloLink
when you talk about the local link, I can reassure you that we have no plans to deprecateApolloLink
. It's still the best way to hook your client up to arbitrary APIs (GraphQL or otherwise), which is a big part of the legacy API adoption story, as you put it.Removing local resolvers is something we'd like to do at some point, but we realize we don't have a full replacement yet, so that deprecation and removal is still a ways off.
I was referring specifically to apollo-link-state (sorry, I think I called it "local link" in my original description, corrected). It is marked as deprecated - that's what I mean by this simultaneous deprecation leaving nowhere for this use case to live.
Let me try to boil this down a bit: after 2.5 the recommendation for everyone using local resolvers was to abandon newly deprecated apollo-link-state and use the functionality directly from core. Then in v3 local resolvers are deprecated from core, leaving no supported home for anyone who wants to stay with local resolvers for the reasons I give above.
So my concrete ask in this issue is for the Apollo team to, at a minimum, consider reversing the decision to drop support for/deprecate apollo-link-state
, in order to provide a supported home for this important use case.
@vigie have you tried to look into "Local-only fields" and replace local resolvers with them? Seems like interface more or less similar, should be replaceable. In case of mutations you have to use reactive variables.
Namely: My presentation layer needs to know if data is local or remote because the way to issue a mutation is different (gql-mutation vs function call).
Agreed! This is the biggest loss, in my opinion. Local resolvers made intuitive sense to me because my components could read and write data without caring whether data is local or remote.
@vigie have you tried to look into "Local-only fields" and replace local resolvers with them? Seems like interface more or less similar, should be replaceable. In case of mutations you have to use reactive variables.
I would argue that the synchronous nature of the cache means the interface is really rather different, but I'm still somewhat unfamiliar here and I've yet to see a concrete example of how such a rewrite would look, perhaps things aren't as bad as I fear.
But the broader point is that _any_ difference is unfortunate - the GQL service can no longer be moved across the network without disruption to the client.
With local resolvers, it was a lot easier to abstract whatever source of data you were using in them and combine them in a single source of truth for the UI, even you could transform graphql data to accommodate it for the UI and cache it in a very abstract way.
I have tried local-only fields but they are very limited in my opinion
Just going to link to a similar / related question: _Trying to understand AC3 vs. AC2.5 state management model_ #6994
I have been struggling for couple of weeks now to understand the reason behind reactive variable and field policy. I cannot have a clear unified architecture and my code is much less debuggable. Rather than having one source of truth and one way of query/mutate my state (local + remote) I have to have different logic in my code to deal with local state. https://github.com/apollographql/apollo-client/discussions/6994 summarize it well. Beside reactive variable are not persistable the same way as the state and are a pain to debug. Direct change to the cache is generally harder to debug. This is why having an abstracted layer with local mutation/query through resolvers was a good thing. Would be nice to have some insight about the reason from the team. Especially about reactive variable and why they exist in the first place. I haven't found any explanation about this in the official doc
I'll condense a bit my thoughts from #6994. Following is how I currently play with these reactive variables and policies.
So, I come from a perspective where I migrated a work project specifically to Apollo 3 to play with these field policies and reactive variables. My use case was simple (as far as I currently use it):
useFragment
. (We have a rather large tree of objects that is given by the backend, which does correspond to the frontend. But it would be cumbersome to pass in all the data subselections down layers and layers of components. I tried using {children}
at some point, but that does not cleanly separate components)So I looked into reactive variables. Well, gladly there's an example for this selection use case. So it turns out that it's relatively straight forward to use this reactive variable. I could just use filter or findIndex to imitate the old Redux selection logic (essentially creating a "selected" field with an index where -1 means unselected. Yes some legacy feature, could have been a boolean). Now, I did look into the code for these reactive variables, and they seem to be cache aware. So if they get called inside a read function, they will "bind" to this read function, triggering it to update each time it changes. This is good to know as I was wondering how it would work when things change, as on the surface, it simply looks as if you call the value once. But this is why reactive variables are part of the in-memory cache, they are in-memory cache aware.
Considering this. I could essentially implement the first 3 options. Great! And with the help of toReference
, I could also enhance the selection to return the actual selected objects. Which is the better part because there are some different ways selected objects need to be handled. So now it becomes possible to query the selection and use fragments to extract the proper data.
Now for the last point. Apparently this use case is also already given. So it was as simple as making a field for the Query type. Looking at the variables given and return a reference to the correct real object. Something akin to:
someObject(_, {args, toReference}) {
return toReference({
__typename: 'SomeObject',
id: args.id,
}
}
Now this is useful, because in a component I can depend on a query with a simple id. If the cache has this object (which it does because a huge tree is fetched first time), then it will simply get a subset of the data. If things change, only the relevant parts will get an update. This was one of my main points. Now, granted, I would have preferred a solution like useFragment
which would essentially do the same: you select bits of information you need based on an id, much like read/writeFragment. But that doesn't exist (yet).
Considering this approach, I feel like the reactive variables are here to help with pure local state, much like the policies. Policies help the client to shape its local usages (the server can (and probably should) provide the "fetch one object", but if the client then spams this query over and over because a lot of objects suddenly want this data, that's bad. With this example policy, the client can reuse the data from the larger query and possibly refetch later if time comes to do so.) From some design decisions over time, I also see this as the original idea behind local resolvers. I don't think they were ever intended to be used for custom fetching. Just looking at the need to put @client
to get to local resolvers points to "this is client side only". So I feel like some users may have used an API in a less than ideal manner, at least in the eyes of apollo devs, and now start to see the mistake. Now, I can see the appeal for developing locally first with client side only and using GraphQL from scratch and have no real opinion on whether this should or should not be easily possible. Though personally, I find setting up a server easy enough not to bother development without a server.
I hope this may shed some light. Although I'm not an apollo dev, though I tend to seek info and reason about their api to get most out of it, which I believe I've done quite well thus far.
Most helpful comment
Let me try to boil this down a bit: after 2.5 the recommendation for everyone using local resolvers was to abandon newly deprecated apollo-link-state and use the functionality directly from core. Then in v3 local resolvers are deprecated from core, leaving no supported home for anyone who wants to stay with local resolvers for the reasons I give above.
So my concrete ask in this issue is for the Apollo team to, at a minimum, consider reversing the decision to drop support for/deprecate
apollo-link-state
, in order to provide a supported home for this important use case.