-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Apollo Client integrated local state handling #4338
Conversation
Does this imply that opting out of |
@JoviDeCroock If you can use |
Correct, and if it matters, I think this is wise. It's really hard to innovate and move quickly when you have dependent code and plugins outside of the main source. If it helps, I believe that as the tech develops, you will be able to see the proper way to separate and package better. |
forceResolvers = | ||
node.arguments | ||
.filter(arg => ( | ||
arg.name.value === 'always' && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm totally open to changing the @client(always: true)
naming, if people don't think it makes sense. I originally had @client(force: true)
for example, but thought it wasn't super clear about the fact that adding this variable means we're telling the associated field resolver to always run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using always: true
crossed my mind too, and I like it! I think it’s a little clearer that you should never pass always: false
(because that’s clearly the default) whereas force: false
might conceivably trigger some non-default behavior.
Includes updates to the `apollo-utilities` package to better accommodate Apollo Client's local state handling capabilities. - Adds a new transform `buildQueryFromSelectionSet' function to help construct a GraphQL Query from a Mutation selection set. - Adds a new transform `removeClientSetsFromDocument` function to prune `@client` selection sets and fields from GraphQL documents'. - Adds a new directive based `hasClientExports` function to see if a GraphQL document is using a `@client @export` directive combination. - Adds a new `mergeDeep` utility function for deep cloning.
When using the `@client` directive, it might be desirable in some cases to want to write a selection set to the store, without having all of the selection set values available. This is because the `@client` field values might have already been written to the cache separately (e.g. via Apollo Cache's `writeData` capabilities). Because of this, we'll skip the missing field warning for fields with `@client` directives.
This is a temporary change, while working on the local state alpha changes.
This commit provides the bulk of the new Apollo Client local state handling capabilities. It replaces the need to use `apollo-link-state` in a link chain for local state management, by merging local state handling directly into the Apollo Client core. The majority of the new local state functionality can be found in the `LocalState` class, which is tied into the `QueryManager` to integrate with Queries, Mutations and Subscriptions. Key Changes: - Replaces the need to use `apollo-link-state`. - `apollo-link-state` defaults have been replaced with new initializer functions, which are a much more capable and flexible way to initialize the cache. - Initializers and local resolvers can be set through the `ApolloClient` constructor, or set separately using new public API functions. - A local schema can be set through `ApolloClient`, which can then be used by external tooling (like Apollo Client Devtools). - Mixing remote and local (`@client` based) data together in queries is supported. - Queries can now use the `@export` directive to export the result of a local resolver into a variable, that can then be automatically passed into a subsequent query. E.g. `@client @export(as: "someVar")` - Full SSR support. - Numerous bug fixes made to address the oustanding `apollo-link-state` issue backlog.
Various tests to cover Apollo Client local state handling.
Remove Boost's dependency on `apollo-link-state`, and wire it up with AC's local state functionality.
1574a72
to
2520751
Compare
By default, local resolvers are controlled by a query's `fetchPolicy`. If a query that has configured local resolvers is run with a fresh cache, and that query is using `ApolloClient.query`'s default `fetchPolicy` of `cache-and-network`, the local resolvers will be fired and the result will be stored in the AC cache. If the same query fires again however, since the query result already exists in the cache, it will be loaded and used instead of firing the query's local resolvers again. This functionality can be altered by using a `fetchPolicy` of `no-cache` for example, but setting a `fetchPolicy` impacts an entire query. So if mixing local and remote results and using a `fetchPolicy` of `no-cache`, local resolvers will always run, but so will the fetch to retrieve network based data. To address this, AC's local state functionality includes a new `resolverPolicy` approach. Setting a `resolverPolicy` of `resolver-always` makes sure local resolvers are always fired for a query, on every request. While this approach works, it is inflexible due to the fact that it does not provide a way to only always run certain local resolvers associated with a query, instead of running all of them. Apollo Client's `fetchPolicy` approach has also historically demonstrated that getting `fetchPolicy` settings right can be a bit tricky (especially for newcomers to the Apollo ecosystem), so adding another configurable policy based approach is not overly desirable. The changes in this commit remove the `resolverPolicy` functionality. They then ensure that fields marked with `@client(always: true)` always have their local resolvers run, on each request. This provides a way to control exactly which parts of a query should have its local resolvers always run, and which parts can continue to leverage the cache. Technical side note: when using `@client(always: true)`, the full query will be resolved from the cache first, before any local resolvers are run. So if data already exists in the cache for a field that's marked with `@client(always: true)`, it's loaded first as part of reading the fully executed query from the cache (including local and remote results). That data (if it exists) is then overwritten by the result from the associated local resolver, and returned as part of the query result. This load then override approach makes sure that the integrity of the cache is not affected by running local resolvers.
2520751
to
ff89ccd
Compare
aa8c7fa
to
e399ad8
Compare
Running tsc -p tsconfig.test.json --noEmit in the apollo-client/packages/apollo-client directory produced a number of warnings and errors before this commit (tsc version 3.2.2).
Thanks for all the great work on this! This feedback might be premature, but in case it influences the API design in any way, I think an important use case will be building additional abstractions on top of local state. For example, here's an idea I have for setting up mutations without as much boilerplate code: export const actions = createUpdaters(client, {
counter: {
increment: ({ count }) => ({
count: ++count
}),
decrement: ({ count }) => ({
count: --count
})
},
currentUser: {
changeName: (state, newName: string) => ({
name: newName
})
}
}) The initial state that goes along with this example looks like this: {
// counter doesn't need to be an object of course, since it only has one property;
// just making it an object for the sake of the example
counter: {
count: 0
},
currentUser: {
name: 'Guest'
}
}
I put together a quick POC here for anyone who's interested: One issue I ran into is that there seems to be no way to get a full object from the cache without knowing specifically which fields to request in advance. So for now I hacked it to access the private |
By relying on normal promise rejection, we can avoid needing the onError callback parameter. Removing await expressions in favor of explicit promises tends to save a small amount of bundle size. It's still a good idea to keep runResolvers async, because async functions reliably capture any exceptions thrown during the execution of the function.
Besides removing await expressions, this commit performs a single mergeDeepArray instead of multiple binary mergeDeep calls.
This type was already quite different from the GraphQLResolveInfo type used by the GraphQL specification, so I think we should limit it to just the necessary information while we still have that freedom: https://graphql.org/learn/execution/#root-fields-resolvers If a resolver needs them, the { isLeaf, resultKey, directives } data previously provided can all be derived from the FieldNode.
555b423
to
8af8875
Compare
On the surface, the idea of using initializers as a way to dynamically prep the cache seemed like a good idea. It was a more flexible way to initiliaze the cache than the `defaults` approach `apollo-link-state` uses. In practice however, everything `initializers` can do `cache.writeData` can do as well. Yes initializers are a little more user friendly to work with, but `cache.writeData` is already part of the cache API and is much more flexible in terms of being able to be called anywhere the cache is available, and it's explicit in what it's doing. Given this, we've decided to remove initializer support from Apollo Client, keeping the local state changes focused on the idea of running local resolvers. While prepping the cache is still an important peice of functionality, we'll update the local state docs to show how `cache.writeData` can be used to handle everything `initializers` were being used for, including re-initializing the cache after a store reset. Removing initializers also has the added benefit of reducing the Apollo Client bundle size by a good chunk.
The LocalState#runResolvers method now requires a remoteResult option of type ExecutionResult<TData> and returns a Promise<ExecutionResult<TData>>, and this contract is enforced (somewhat better) using generic types.
Since the new local state changes will be rolled out in a minor release, we have to make sure Boost local state support is backwards compatible. To do this, this commit adjusts the `clientState` parameter approach such that if `defaults` are passed in, they are automatically written to the cache using `cache.writeData`. Also, the Boost constructor has been updated to line up with the new `ApolloClient` constructor changes, meaning `resolvers`, `typeDefs`, and `fragmentMatcher` can now be passed into the Boost constructor. Other than making sure Boost doesn't break when the local state changes are rolled out, we won't be making other Boost changes. Apollo Boost is still slotted for deprecation in AC 3.0.
[field: string]: ( | ||
rootValue?: any, | ||
args?: any, | ||
context?: any, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't there be a some kind of context interface that provides cache
, getCacheKey
as per documentation?
We're going to avoid getters/setters for `typeDefs` for now, and instead recommend `typeDefs` are set via the `ApolloClient` constructor. We were originally exposing getters/setters for integrations like Apollo Client Devtools, but we'll adjust devtools to access the `typeDefs` in a different way. We will likely want to revisit this decision in the future, to accommodate the possibility that people want to add to their local schema at different points in their application, but we'll cross that bridge when we get there. For now keeping the changes to the public `ApolloClient` API minimal, is the goal.
I feel that local queries are rarely computationally expensive and that most of the reason to use caching is because of the network latency/bandwidth issues, as such wouldn't it make sense to default to |
@fpaboim What about when you call |
True, and those should be most use cases I guess... on the other side besides localStorage the only other thing I could think of is accessing some other state, like someone using redux/mobx/context for local state? |
In a nutshell, this PR migrates the local state handling capabilities provided by
apollo-link-state
, into the Apollo Client core.@client
directive handling is fully managed by Apollo Client (through theQueryManager
), without requiring a separate Apollo Link.While opinionated, these changes are being considered to help address several outstanding
apollo-link-state
issues that are difficult to address in a link chain, and open the door to future Apollo Client changes that integrate local state handling more closely with the Apollo Cache.The implementation outlined in this PR closely mirrors existing
apollo-link-state
functionality, with a few changes / additions to make handling local state with Apollo Client more flexible and easy to use.Key Changes
It is no longer necessary to add
apollo-link-state
to your link chain. Apollo Client includes all local state functionality out of the box.The idea of initializing your local cache with
apollo-link-state
's fixeddefaults
option has been replaced with a newinitializer
function approach. Initializers are simple functions, that can be run duringApolloClient
instantiation, or via theApolloClient.runInitializers
function. These functions are mapped against the name of the field you want to update in the cache, and the result of running the function is stored in the cache against that field. Initializers can be as flexible as you need them to be, and can either write the returned result directly to the cache, or be configured to skip writing to the cache, letting you handle that manually inside the initializer function yourself. Each initializer function is called with access to theApolloClient
instance, so the full AC API is available.Initializers do not check to see if they'll overwrite existing data when run. For now we're avoiding the extra cache hit of having initializers check to see if data already exists for a field, before overwriting it. This functionality can be overwritten by manually checking and updating the cache yourself in an initializer function.
Initializers are only run once (to help prevent overwriting data accidentally).
Local resolvers can be set through the
ApolloClient
resolvers
constructor param, or by calling theApolloClient.addResolvers
function.A local schema can be defined by passing it into the
ApolloClient
constructor via thetypeDefs
parameter.Mixing remote and local fields together in queries / selection sets (aka virtual fields) is still supported, along with some additional features.
@client ... @export(as: "someVar")
can now be used to pass the result of a locally resolved query in as a variable for a remote query, all in the same request. For example:Subscriptions now recognize the
@client
directive, meaning locally resolved data can be combined with incoming subscription data.Full SSR support.
Numerous bug fixes and changes made to address outstanding
apollo-link-state
issues (issues labelled asac-local-state-ready
are fixed by these changes).And more! 🙂
This PR is still a work in progress (and is not ready for review), but we're pushing hard to get it wrapped up soon.
TODO:
Documentation. This PR is already getting pretty large, so I'll submit the updated local state docs in a separate PR.