I am trying to use Apollo cache for local state management to store the state of a form so it can be returned to without clearing.
I am experiencing a problem where the cache is being updated but subsequent queries to the cache are returning stale data. I have experienced this problem in React components using the useQuery hook, and also in Apollo DevTools which I will use to demonstrate it below:
I have this mutation and query set in my resolvers (I am using Typescript):
const resolvers = {
Mutation: {
storeLetterDraft: (_root, args: { type: string, details: LetterSending }, { client, getCacheKey }) => {
const id = getCacheKey({
__typename: "LetterDraft",
id: args.type,
});
const data = { ...args.details };
client.writeFragment({
data,
id,
fragment: LETTER_SENDING_FRAGMENT,
});
},
},
Query: {
letterDraft: (_root, args: { type: string }, { client, getCacheKey }) => {
// I HAVE TRIED A DEBUGGER STATEMENT HERE
const id = getCacheKey({
__typename: "LetterDraft",
id: args.type,
});
return client.readFragment({
id,
fragment: LETTER_SENDING_FRAGMENT,
});
},
},
}
My fragment is:
export const LETTER_SENDING_FRAGMENT = gql`
fragment DraftLetterSending on LetterDraft {
date
firstName
lastName
addressLine1
addressLine2
addressTown
addressCounty
addressPostcode
}
`;
I am initialising my cache with:
cache.writeData({
data: {
letterDrafts: [{
__typename: "LetterDraft",
id: "CREATE",
addressCounty: "Northamptonshire",
addressLine1: "1 Watkin Terrace",
addressLine2: "",
addressPostcode: "NN1 3ER",
addressTown: "Northampton",
date: "2019-11-01",
firstName: "d",
lastName: "d",
}],
},
});
My mutation looks like:
export const storeCreateLetterSendingMutation = gql`
mutation StoreCreateLetterSending($details: LetterSending!) {
storeLetterDraft(type: "CREATE", details: $details) #client
}
`;
Before mutation, the cache in Apollo DevTools looks as expected:
And a query returns as expected:
After the mutation is performed, the cache updates:
However, running the query again results in the stale data:
Interestingly if I put a debugger statement in the part above (I HAVE TRIED A DEBUGGER STATEMENT HERE), then it seems the query resolver is run the first time, but not the second time, so it appears the query is being cached - even though it is the cache I am updating! Therefore I think the issue is with the query not running the resolver subsequently.
I had missed this from the documentation (there are various places on the Apollo website detailing the local cache and #client.
https://www.apollographql.com/docs/react/essentials/local-state/#forcing-resolvers-with-clientalways-true
While leveraging the cache for both local and remote results can be super helpful in a lot of cases, it's not always the best fit. We might want to use a local resolver to calculate a dynamic value that needs to be refreshed on every request, while at the same time continue to use the cache for the network based parts of our query. To support this use case, Apollo Client's #client directive accepts an always argument, that when set to true will ensure that the associated local resolver is run on every request.
Related
I am using apollo server in the azure function. I want to set cookies from apollo server azure functions. But it's not working. It doesn't throw any kind of errors.
How do I set cookies in apollo server azure functions? I tried this way but it's not working.
Here is my code
import { ApolloServer, gql } from "apollo-server-azure-functions";
import { ApolloServerPluginLandingPageLocalDefault } from "apollo-server-core";
import { serialize, parse } from "cookie";
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
user: User
}
type User {
id: ID!
name: String!
email: String!
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
user: (parents, args, { request, context }, info) => {
const cookie = serialize("token", "123", {
expires: new Date(Date.now() + 900000),
httpOnly: true,
});
context.res.setHeader("Set-Cookie", cookie);
return {
id: "1",
name: "John Doe",
email: "john#example.com",
};
},
},
};
// #ts-ignore
const server = new ApolloServer({
typeDefs,
resolvers,
debug: true,
plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })],
context: (context) => {
return context;
},
});
export default server.createHandler({
cors: {
origin: ["*", "https://studio.apollographql.com"],
methods: ["GET", "POST", "OPTIONS"],
allowedHeaders: [
"access-control-allow-header",
"access-control-allow-credentials",
"access-control-allow-origin",
"content-type",
],
},
});
There is no documentation available for apollo server azure functions.
Official repository from apollo server azure functions: https://github.com/Azure-Samples/js-e2e-azure-function-graphql-hello.git
Sharing the discussion with the team internal and posting the update as updated here.
After looking at the issue, the infrastructure, and the announcement from Apollo for this package, I believe Apollo is the correct organization to post this issue because Apollo is providing the server in this sample. It just happens to be running on an Azure Function. Additionally, when I look for a solution on Apollo, it looks like the ApolloServer dependency needs to be swapped out for an Apollo server for express dependency in order to successfully set the cookie.
None of this is great news. I apologize for this.
I believe the sample works in this repo without cookes and doesn't currently include cookies in the code. Moving forward with the Apollo dependency, we will re-evaluate its use based on this feedback.
I'm working on a vue3 project using #vue/apollo-composable and #graphql-codegen.
My index page does a search query. Each result from that query has a tile made on the page. I'm expecting the tile queries will be answered by the cache, but instead, they always miss.
At the page level I do this query:
query getTokens($limit: Int!) {
tokens(limit: $limit) {
...tokenInfo
}
}
Inside of the tile component I execute:
query getToken($id: uuid!){
token(id: $id) {
...tokenInfo
}
}
The fragment looks like this:
fragment tokenInfo on token {
id
name
}
Expectation: The cache would handle 100% of the queries inside the tile components. (I'm hoping to avoid the downfalls of serializing this data to vuex).
Reality: I get n+1 backend calls. I've tried a bunch of permutations including getting rid of the fragment. If I send the getToken call with fetchPolicy: 'cache-only' no data is returned.
The apollo client configuration is very basic:
const cache = new InMemoryCache();
const defaultClient = new ApolloClient({
uri: 'http://localhost:8080/v1/graphql',
cache: cache,
connectToDevTools: true,
});
const app = createApp(App)
.use(Store, StateKey)
.use(router)
.provide(DefaultApolloClient, defaultClient);
I'm also attaching a screenshot of my apollo dev tools. It appears that the cache is in fact getting populated with normalized data:
Any help would be greatly appreciated! :)
I've gotten this worked out thanks to #xadm's comment as well as some feedback I received on the Vue discord. Really my confusion is down to me being new to so many of these tools. Deciding to live on the edge and be a vue3 early adopter (which I love in many ways) made it even easier for me to be confused with the variance in documentation qualities right now.
That said, here is what I've got as a solution.
Problem: The actual problem is that, as configured, Apollo has no way to know that getTokens and getToken return the same type (token).
Solution: The minimum configuration I've found that resolves this is as follows:
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
token(_, { args, toReference }) {
return toReference({
__typename: 'token',
id: args?.id,
});
},
},
},
},
});
However, the feels.... kinda gross to me. Ideally, I'd love to see a way to just point apollo at a copy of my schema, or a schema introspection, and have it figure this out for me. If someone is aware of a better way to do that please let me know.
Better(?) Solution: In the short term here what I feel is a slightly more scalable solution:
type CacheRedirects = Record<string, FieldReadFunction>;
function generateCacheRedirects(types: string[]): CacheRedirects {
const redirects: CacheRedirects = {};
for (const type of types) {
redirects[type] = (_, { args, toReference }) => {
return toReference({
__typename: type,
id: args?.id,
});
};
}
return redirects;
}
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
...generateCacheRedirects(['token']),
},
},
},
});
If anyone has any improvements on these, please add a comment/solution! :)
Suppose I have the following GraphQL types:
type User {
id: String!
posts: [Post!]!
}
type Post {
id: String!
text: String,
}
And here is a mutation that returns the updated post:
mutation addNewPost(
$userId: String!
$text: String!
) {
addNewPost(userId: $userId, text: $text) {
id
text
}
}
After running this mutation my cache contains a new entry of a post. How do I add it to the user's posts array? I have tried cache.writeQuery and cache.modify but I cannot figure it out.
We do push the item into array inside the update function, which is one of the options of useMutation.
I'm writing the whole mutation so that you can get the idea 💡, let have a look at example:
By Update function:
const [createPost, { data, loading, error }] = useMutation(CREATE_POST, {
update(cache, response) {
// Here we write the update
// Step 1: Read/Fetch the data 👈
const data = client.readQuery({
query: FETCH_POSTS_QUERY,
});
// Step 2: Update the cache by immutable way 👈
client.writeQuery({
query: FETCH_POSTS_QUERY,
data: {
getPosts: [response.data.createPost, ...data.getPosts],
},
});
},
variables: formValues,
});
By refetchQueries:
That's really shortcut 🔥 way to update the cache, by using DocumentNode object parsed with the gql function
const [createPost, { data, loading, error }] = useMutation(CREATE_POST, {
refetchQueries: [ 👈
FETCH_POSTS_QUERY
'fetchPosts`' // OR Query name
],
variables: formValues,
});
You're going to want to directly write to the Apollo cache in order to update the other entities that your mutation has modified.
Have a look at the docs https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates here for specifics (you're going to want to use cache.modify and cache.writeFragment)
I've got a query that looks like this:
export const GET_PROJECT = gql`
query GetProject($id: String!) {
homework {
getProject(id: $id) {
...ProjectFields
}
}
}
${ProjectFieldsFragment}
`;
My InMemoryCache looks like this:
const cache = new InMemoryCache({
dataIdFromObject: ({ id }) => id,
cacheRedirects: {
Query: {
getProject: (_, args, obj) => {
console.log('Hello world');
},
},
}
});
The above cache redirect is never hit. However, if I modify it to look like:
const cache = new InMemoryCache({
dataIdFromObject: ({ id }) => id,
cacheRedirects: {
Query: {
homework: (_, args, obj) => {
console.log('Hello world');
},
},
}
});
It does get hit, however I don't have any of the arguments that are passed in the nested getProject query. What's also confusing is that this cache redirect function is hit for queries that it seemingly shouldn't get hit for, like:
export const SESSION = gql`
query Session {
session {
user {
id
fullName
email
}
organizations {
name
id
}
}
}
`;
So what is going on? I've resorted to just using readFragment in the places where I want the cache to redirect, but I'd like for that logic to become centralized.
It's hard to say for sure with these kinds of issues, but I'm betting that, since you say
What's also confusing is that this cache redirect function is hit for queries that it seemingly shouldn't get hit for
the issue might be with your dataIdFromObject function.
This function is ultimately what decides if data is read from the cache or not. You should only override this if you have a very specific reason to. For example:
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory';
// ...
export default new ApolloClient({
link,
cache: new InMemoryCache({
dataIdFromObject(object) {
switch (object.__typename) {
case 'ModifierScale':
case 'ModifierGroup':
return [
object.__typename,
object.id,
...object.defaults
.map((defaultModifier) => defaultModifier.id)
.join(''),
].join('');
default:
return defaultDataIdFromObject(object); // fall back to default handling
}
},
}),
});
The point of this setting is to allow you to customize the key that gets put into the cache when you are loading the data.
If this doesn't solve your issue, I would definitely go into the Apollo tab in the chrome dev tools (you need the Apollo dev tools chrome extension to do this) and look at the cache section. It should show you the data in the cache and the key that the data is stored in.
Using this mutation:
import produce from 'immer
const [createItem] = useMutation(CREATE_ITEM, {
update (client, { data: { createItem } }) {
const queryResults = client.readQuery({
query: GET_LATEST_ORDER,
variables: { orderDepth: 1 }
})
client.writeQuery({
query: GET_LATEST_ORDER,
variables: { orderDepth: 1 },
data: produce(queryResults, draft => {
draft.orders[0].items.push(createItem)
})
})
}
})
I am unable to get
const { loading, data, refetch } = useQuery(GET_LATEST_ORDER, {
variables: { orderDepth: 1 }
})
to show updated data after the mutation.
The apollo cache is updated correctly. But data on the useQuery does not change.
The issue ended up being the returned object from the mutation was not exactly the same. It was missing an #client field.
While obvious in hindsight no where I searched described this as a reason except for a comment I saw mentioning perhaps missing the __typename.
This would have been obvious had apollo thrown an error. However, no error was thrown, nor existed on the useQuery.