Apollo Client cache - reactjs

I just started using apollo client on a React application and I'm stuck on caching.
I have a home page with a list of products where I do a query to get the id and name of those products and a product page where I do query for the ID, name, description and image.
I would like that if a user visits the home page fist then a specific product page to only do a query for that product's description and image, also display the name during the loading (since I should have cached it already).
I followed "Controlling the Store" part of the documentation (http://dev.apollodata.com/react/cache-updates.html) but still couldn't resolve it.
The query that is done when we go to the product page still asks for both the product's id and name whereas they should be cached since I already asked for them.
I think I'm missing something but I can't figure it out.
Here is a bit of the code:
// Create the apollo graphql client.
const apolloClient = new ApolloClient({
networkInterface: createNetworkInterface({
uri: `${process.env.GRAPHQL_ENDPOINT}`
}),
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) {
console.log(result.id, result.__typename); //can see this on console, seems okey
return result.__typename + result.id;
}
// Make sure to return null if this object doesn't have an ID
return null;
},
});
// home page query
// return an array of objects (Product)
export default graphql(gql`
query ProductsQuery {
products {
id, name
}
}
`)(Home);
//product page query
//return an object (Product)
export default graphql(gql`
query ProductQuery($productId: ID!) {
product(id: $productId) {
id, name, description, image
}
}
`,{
options: props => ({ variables: { productId: props.params.id } }),
props: ({ data: { loading, product } }) => ({
loading,
product,})
})(Product);
And my console output:

The answer to your question actually has two parts:
The client cannot actually tell for sure that these queries resolve to the same object in the cache, because they have a different path. One starts with products, the other with product. There's an open PR for client-side resolvers, which will let you give the client hints about where to find things in the cache, even if you haven't explicitly queried for them. I expect that we will publish that feature within a week or two.
Even with client-side resolvers, Apollo Client won't do exactly what you described above, because Apollo Client no longer does query diffing since version 0.5. Instead, all queries are fully static now. That means even if your query is in the cache partially, the full query will be sent to the server. This has a number of advantages that are laid out in this blog post.
You will still be able to display the part that's in the cache first, by setting returnPartialData: true in the options.

This question is quite old, however, there is a solution to map the query to the correct location using cacheRedirects
In my project, I have a projects query and a project query.
I can make a cacheRedirect like below:
const client = new ApolloClient({
uri: "http://localhost:3000/graphql",
request: async (operation) => {
const token = await localStorage.getItem('authToken');
operation.setContext({
headers: {
authorization: token
}
});
},
cacheRedirects: {
Query: {
project: (_, { id }, { getCacheKey }) => getCacheKey({ id, __typename: 'Project' })
}
}
});
Then when I load my dashboard, there is 1 query which gets projects. And then when navigating to a single project. No network request is made because it's reading from the cache πŸŽ‰
Read the full documentation on Cache Redirects

Related

Upload react-pdf dynamically generated file to Sanity using NextJS

I'm working on an e-commerce app built on NextJS and Sanity, so far I've made some mock products with all the necessary requirements, a user login system and checkout. I've been trying to make an invoice system so that when the user confirms an order 3 things must happen:
send all the order data to a react-pdf component and generate the invoice(working)
post the invoice file to the sanity schema so that the user has access to it when he goes to his order history page(not working)
email both the company and the client about the order(not implemented yet but I can do it)
ReactPDF allows me to access the pdf through a hook that returns me the blob of the file and the URL. I've tried to POST both of them but the url returned 404 and the blob didn't upload at all.
Searched the docs of both ReactPDF and Sanity and I couldn't find anything, although I think it has to do something with this endpoint from Sanity:
myProjectId.api.sanity.io/v2021-06-07/assets/files/myDataset
This is how I POST the order to my sanity studio
const { data } = await axios.post(
'/api/orders',
{
user: userInfo,
invoice_id: orders.length + 1,
orderItems: cartItems.map((item) => ({
...item,
slug: undefined
})),
billingData,
paymentMethod,
itemsPrice,
taxPrice,
totalPrice
},
{
headers: {
authorization: `Bearer ${userInfo.token}`
}
}
);
I've tried making 2 POST requests, one for the invoice_file alone, trying to post the blob or the url but none did work. The schema for invoice file was updated for the type of post each time so I'm 99% sure that wasn't the issue, anyway here's how the schema for invoice_file looks as for file:
{
name: 'invoice_file',
title: 'Invoice',
type: 'file',
options: {
storeOriginalFilename: true
}
},
If there would be any other code snippets relevant please let me know.
I really don't know how to find the solution for this as it's the first time trying to do such thing, so help would be much appreciated.
Thanks in advance!
I apologies as I'm not really active here but it's hard to pass on your question especially as I'm working on something similar. There's probably other ways to do this but I suggest you work use the official Sanity client. There's a specific section in the README that tells us how to do the file uploads or here.
So here's kinda the very small snippet:
import {
Document,
pdf,
} from "#react-pdf/renderer";
const doc = <Document />;
const asPdf = pdf([]); // {} is important, throws without an argument
asPdf.updateContainer(doc);
const blob = await asPdf.toBlob();
// `blob` here is coming from your react-pdf blob
const fileName = "customfilename.pdf";
client.assets.upload("file", blob, { filename: fileName }).then((fileAsset) => {
console.log(fileAsset", fileAsset);
// you can then use the fileAsset to set and reference the file that we just uploaded to our document
client.patch("document-id-here").set({
invoice_file: {
_type: "file",
asset: {
_type: "reference",
_ref: fileAsset._id,
},
},
}).commit();
});

Attaching data from model to mutation with GraphQL

I'm currently working on a social media site using react, graphQL and Apollo.
I have an issue retaining a field from a User when making a post. With this being graphQL, there comes a host of areas that issues can arise from (typedefs, resolvers, mutations, queries, how they're called in the component, etc) and when trying to make this post I have had it become about 3 miles long as I cut and paste all of the various code from all of those files to try and get as much detail as I can. However, that ends up being an incredible amount of code to dig through to try to find the exact error, so I'm going to skip all of that and ask a simple question in the hopes that an answer can help me crack this without someone having to read a novels worth of text.
When a User makes a post, the username is saved in the post through the resolver like such:
addPost resolver
addPost: async (parent, args, context) => {
if (context.user) {
const post = await Post.create({ ...args, username: context.user.username });
await User.findByIdAndUpdate(
{ _id: context.user._id },
{ $push: { posts: post._id } },
{ new: true }
);
return post;
}
throw new AuthenticationError('You need to be logged in!');
},
I recently finished up adding profile picture functionality to the site. A users profile image is saved to their model. I have updated the Post model to include a userImage field alongside the username field.
I attempted to retain the users profile image to attach to the post like so:
addPost resolver including image
addPost: async (parent, args, context) => {
if (context.user) {
const post = await Post.create({ ...args, username: context.user.username, userImage: context.user.image });
await User.findByIdAndUpdate(
{ _id: context.user._id },
{ $push: { posts: post._id } },
{ new: true }
);
return post;
}
throw new AuthenticationError('You need to be logged in!');
},
However, when making a post, the userImage field comes back as null. Is there somewhere else to call that value in the resolver to retain that? If I drop the "username: context.user.username" from the resolver, the post returns an error saying the username is required. In testing, I made the userImage field a required field so that a post would not post without a userImage, and I receive the same error saying the userImage field is required, even with that updated resolver including the "userImage: context.user.image."
The image field on the Post model is "userImage" and on the user model it is just "image", so I believe the way it is set up in the resolver should work, but for some reason it will not take.
Main question: How can I retain this field?
Questions based on this not working: Am I only able to call one field in the resolver (that field being username)?
When I list the userImage field as not required, the post will post, and the userImage field in the response from graphQL just says "null".
I hope this is enough for someone to point me in the right direction. I will keep an eye on this post and would be happy to add any relevant code that anyone may need to assist. I have updated everything on the typedefs, mutations.js, and queries.js files to include images and userImages respectively.
Thank you to anyone who takes a look, I'm really trying to understand what may need to be done here!

Apollo client v3 not caching query results with useQuery

I am using apollo v3 with a create-react app. I fire a query with useQuery and expect results to be cached, but they are not.
In my App.jsx, I have:
const client = new ApolloClient({
uri: `${api}/graphql`,
cache: new InMemoryCache()
})
I wrap my app with the ApolloProvider.
I have a provider that uses this graphql query to fetch a list of users:
const USERS_QUERY = gql`
query GetUsers {
users {
id
email
fullName
}
}
`
The query works, when I inspect the apollo tab in chrome devtools, I see nothing in the cache section.
My questions:
Why are the results not cached if I am using the useQuery from #apollo/client?
const { loading, error, data } = useQuery(USERS_QUERY)
I thought results should be cached automatically.
I also tried to add a type policy:
cache: new InMemoryCache({
typePolicies: {
Users: {
keyFields: ['id']
}
}
})
But I think I use this if I want to normalise with a different key the cache, or if I want to decide how to merge new data by myself. But first I need to have the data in my cache
As far as I know, this is because your query does not have any input arguments / variables, so the cache does not know what item to get from the cache when the query is called again. From what I understand, the cache is only used when a specific piece of data is looked for with an ID; otherwise, if it is a generic query, the data might have changed and so is not cached.

How to force Apollo Client to use cached data for detail view page

I have a paginated cursor based query TODOS and detail page with query TODO to get data by ID.
Whenever I go to detail view and use useQuery with TODO query (Which contains exactly same data as TODOS query result, it still tries to get data from server not from cache. How can I achieve to not get data from server (Because it already exists), I thought Apollo detect by id and return from the cache but no. Any suggestions ?
Similar issue as no this post, but I don't think thats a right approach, there should be better solution. (I hope)
This is TODOS query:
query TODOS(
$paginationOptions: PaginationOptionsInput
) {
todos(paginationOptions: $paginationOptions) {
pagination {
minCursor
maxCursor
sortOrder
limit
hasMoreResults
}
result {
id
...SomeTodoFields
}
}
And on detail page I have second query TODO
query (
$todoId: String!
) {
todo(todoId: $todoId) {
id
...SomeTodoFields
}
}
Since I am using Apollo-client < 3.0 for me cacheRedirect worked fine, you can have a look farther here. Read every note carefully it is really important! My code example:
cache: new InMemoryCache({
fragmentMatcher,
cacheRedirects: {
Query: {
todo: (_, args, { getCacheKey }) => {
return getCacheKey({ __typename: 'TodoType', id: args.todoId })
}
}
}
})
})
Found some good relevant article as well, which you might want to check.
This worked for me, hope it helps to someone else as well. :)

React Apollo updating client cache after mutation

I am trying to update my chache after succesfully executing a mutation. Here is my query and mutation:
export const Dojo_QUERY = gql`
query Dojo($id: Int!){
dojo(id: $id){
id,
name,
logoUrl,
location {
id,
city,
country
},
members{
id
},
disziplines{
id,
name
}
}
}`;
export const addDiszipline_MUTATION = gql`
mutation createDisziplin($input:DisziplineInput!,$dojoId:Int!){
createDisziplin(input:$input,dojoId:$dojoId){
disziplin{
name,
id
}
}
}`;
and my mutation call:
const [createDisziplin] = useMutation(Constants.addDiszipline_MUTATION,
{
update(cache, { data: { createDisziplin } }) {
console.log(cache)
const { disziplines } = cache.readQuery({ query: Constants.Dojo_QUERY,variables: {id}});
console.log(disziplines)
cache.writeQuery({
...some update logic (craches in line above)
});
}
}
);
when i execute this mutation i get the error
Invariant Violation: "Can't find field dojo({"id":1}) on object {
"dojo({\"id\":\"1\"})": {
"type": "id",
"generated": false,
"id": "DojoType:1",
"typename": "DojoType"
}
}."
In my client cache i can see
data{data{DojoType {...WITH ALL DATA INSIDE APPART FROM THE NEW DISZIPLINE}}
and
data{data{DisziplineType {THE NEW OBJECT}}
There seems to be a lot of confusion around the client cache around the web. Somehow none of the posed solutions helped, or made any sense to me. Any help would be greatly appreciated.
EDIT 1:
Maybe this can help?
ROOT_QUERY: {…}
"dojo({\"id\":\"1\"})": {…}​​​​​
generated: false​​​​​
id: "DojoType:1"​​​​​
type: "id"​​​​​
typename: "DojoType"​​​​​
<prototype>: Object { … }​​​​
<prototype>: Object { … }
Edit 2
I have taken Herku advice and started using fragment. however it still seems to not quite work.
My udated code:
const [createDisziplin] = useMutation(Constants.addDiszipline_MUTATION,
{
update(cache, { data: { createDisziplin } }) {
console.log(cache)
const { dojo } = cache.readFragment(
{ fragment: Constants.Diszilines_FRAGMENT,
id:"DojoType:"+id.toString()});
console.log(dojo)
}
}
);
with
export const Diszilines_FRAGMENT=gql`
fragment currentDojo on Dojo{
id,
name,
disziplines{
id,
name
}
}
`;
however the result from console.log(dojo) is still undefined.Any advice?
So I think your actual error is that you have to supply the ID as as a string: variables: {id: id.toString()}. You can see that these two lines are different:
dojo({\"id\":1})
dojo({\"id\":\"1\"})
But I would highly suggest to use readFragment instead of readQuery and update the dojo with the ID supplied. This should update the query as well and all other occurrences of the dojo in all your queries. You can find documentation on readFragment here.
Another trick is as well to simply return the whole dojo in the response of the mutation. I would say people should be less afraid of that and not do to much cache updates because cache updates are implicit behaviour of your API that is nowhere in your type system. That the new disziplin can be found in the disziplins field is now encoded in your frontend. Imagine you want to add another step here where new disziplins have to be approved first before they end up in there. If the mutation returns the whole dojo a simple backend change would do the job and your clients don't have to be aware of that behaviour.

Resources