React Apollo Client - query results mixing up in cache - reactjs

I use apollo client to fetch book graphs and use relay style pagination. Both of the following NEW_BOOKS query and ALL_BOOKS query works fine independently.
Currently, I am using NEW_BOOKS in the home page and ALL_BOOKS in a popup in the home page.
When the Homepage is opened NEW_BOOKS gets loaded fine.
When the popup is opened and ALL_BOOKS is fetched, newBooks become undefined or the result of ALL_BOOKS query.
Why is this happening?
const { loading, data: newBooks, fetchMore, networkStatus } = useQuery(NEW_BOOKS, {
variables: {
first: PAGE_SIZE,
after: endCursor
},
notifyOnNetworkStatusChange: true
});
const NEW_BOOKS = gql`query GetNewBooks($first:Int!, $after:String){
books(
first: $first, after: $after,
filters: [
{
path: "isNew",
value: true
}
]
) {
totalCount
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
node {
id
name
author {
id
name
}
}
}
}
}`;
-All books query filterable by name
const { loading, data: filteredBooks, fetchMore, networkStatus } = useQuery(ALL_BOOKS, {
variables: {
first: PAGE_SIZE,
after: endCursor,
name: nameFilter
},
notifyOnNetworkStatusChange: true
});
const ALL_BOOKS = gql`query GetAllBooks($first:Int!, $after:String, $name:String){
books(
first: $first, after: $after,
filters: [
{
path: "name",
value: $name,
type: "contains"
}
]
) {
totalCount
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
node {
id
name
copiesSold
author {
id
name
}
}
}
}
}`;
The cache being used looks like this,
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
books: relayStylePagination(),
},
}
},
});

We have to pass keyArgs explictly when relayStylePagination or similar pagination is used.
A keyArgs: ["type"] field policy configuration means type is the only argument the cache should consider (in addition to the field name and the identity of the enclosing object) when accessing values for this field. A keyArgs: false configuration disables the whole system of differentiating field values by arguments, so the field's value will be identified only by the field's name (within some StoreObject), without any serialized arguments appended to it.
KeyArgs documentation here.
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
books: relayStylePagination(["name"]),
},
}
},
});

Related

Relay-style pagination with Apollo Client

I am trying to paginate 2 relay-style fields posts_connection and comments_connection
For instance, the query is extremely simple,
const QUERY = gql `
PostsQuery($comments_first: Int, commments_after: String) {
posts_connection {
edges {
node {
...INFO_ABOUT_A_POST
comments_connection(first:$comments_first, after:$commments_after) {
edges {
node {
...INFO_ABOUT_A_COMMENT
}
}
}
}
}
}
}
`
To do this with Apollo client, we configure the cache using relayStylePagination(), as in https://www.apollographql.com/docs/react/pagination/cursor-based/#relay-style-cursor-pagination
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts_connection: relayStylePagination(),
},
},
posts: { // posts is the type of a single post node
fields: {
keyFields: ["postid"],
comments_connection: relayStylePagination()
}
},
comments: {
keyFields: ["commentid"],
},
}
}
My process is
Run the initial query,
data has a single initial comment, COMMENT1
fetchMore comments
fetchMore({variables : {comments_after:PREVIOUS_END_CURSOR} })
We fetch a new comment COMMENT2
The issue: data should contain [ COMMENT1, COMMENT2]
Instead, the new comment overwrites the old one, and data only contains COMMENT2
Why is this happening and how do I solve it?

Pagination doesn't work. I need to update from updateQuery to field policies

I need to figure out how to update the onFetchMore method. I have a page, on this page I see 10 elements and, when I click on the "Show more" button, I have to add another 10 elements, etc. So I must add to the existing ones of the others.
When I click, the warning pops up in the console (The updateQuery callback for fetchMore is deprecated, and will be removed in the next major version of Apollo Client.)
const { loading, error, data, networkStatus, fetchMore } = useQuery(sezioneByUuid, {
variables: { slug: slug || sezione, limit: 10 },
errorPolicy: 'all',
fetchPolicy: 'network-only',
partialRefetch: true,
notifyOnNetworkStatusChange: true,
skip: false,
});
const onFetchMore = useCallback(() => {
const {
page: {
apertura,
block: {
set: {
pagedItems: { items },
},
},
},
} = data;
fetchMore({
variables: {
limit: 10,
exclude: [apertura.set.first.uuid].concat(
items.map((articolo) => {
const { uuid } = articolo;
return uuid;
}),
),
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
// prev
const {
mktg,
page: {
__typename: pageTypeName,
slug,
uuid,
section,
seo,
apertura,
block: {
page,
__typename: blockTypeName,
uuid: blockUuid,
set: {
__typename: setTypeName,
uuid: setUuid,
pagedItems: { uuid: pagedItemsUuid, __typename: pagedItemsTypeName, items: oldItems },
},
},
},
} = prev;
// fetch more contents
const { items: newItems, hasNext, excluded } = fetchMoreResult.page.block.set.pagedItems;
return {
page: {
uuid,
__typename: pageTypeName,
slug,
block: {
page,
uuid: blockUuid,
__typename: blockTypeName,
set: {
uuid: setUuid,
__typename: setTypeName,
pagedItems: {
uuid: pagedItemsUuid,
__typename: pagedItemsTypeName,
items: [...items, ...newItems],
hasNext,
excluded,
},
},
},
section,
seo,
apertura,
},
mktg,
social: prev.social,
};
},
});
}, [data, fetchMore]);
I'm trying to edit with the field policy, then eliminating updateQuery() but I get this message (Cache data may be lost when replacing the set field of a Block object.)
Can anyone help me?
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
sezione: {
//keyFields: ["sezione"],
// Don't cache separate results based on
// any of this field's arguments.
keyArgs: false,
// Concatenate the incoming list items with
// the existing list items.
merge(existing, incoming) {
if (!incoming) return existing;
if (!existing) return incoming; // existing will be empty the first time
const { items, ...rest } = incoming;
let result = rest;
result.items = [...existing.items, ...items]; // Merge existing items with the items from incoming
return result;
},
},
},
},
},
});
Add a dataIdFromObject function so that Apollo knows to key against your id field uuid on all objects, since this is different to the default id or _id that apollo looks for.
const cache = new InMemoryCache({
dataIdFromObject(responseObject) {
return `${responseObject.__typename}:${responseObject.uuid}`
},
typePolicies: {
Query: {
fields: {
sezione: {
//keyFields: ["sezione"],
// Don't cache separate results based on
// any of this field's arguments.
keyArgs: false,
// Concatenate the incoming list items with
// the existing list items.
merge(existing, incoming) {
if (!incoming) return existing;
if (!existing) return incoming; // existing will be empty the first time
const { items, ...rest } = incoming;
let result = rest;
result.items = [...existing.items, ...items]; // Merge existing items with the items from incoming
return result;
},
},
},
},
},
});
Note this assumes every object in your GQL schema has a UUID. If there's differences between types you need to do it the other way.
In my management I added the dataIdFromObject() function but that warning keeps coming out.
const dataIdFromObject = (object) => {
// eslint-disable-next-line no-underscore-dangle
const typename = object.__typename;
switch (typename) {
case 'Block':
return `${typename.toLowerCase()}:${object.page}-${object.uuid}`;
case 'ContentSet':
return defaultDataIdFromObject(object);
case 'Image':
return `${typename.toLowerCase()}:${object.src}`;
case 'Page':
return `${typename.toLowerCase()}:${object.slug}`;
case 'PagedContent':
return defaultDataIdFromObject(object);
case 'Leaf':
return defaultDataIdFromObject(object);
default:
if (object.uuid) {
return `${typename.toLowerCase()}:${object.uuid}`;
}
return defaultDataIdFromObject(object); // fall back to default handling
}
// }
};
const cache = new InMemoryCache({
dataIdFromObject,
typePolicies: {
Query: {
fields: {
sezione: {
// Don't cache separate results based on
// any of this field's arguments.
keyArgs: false,
// Concatenate the incoming list items with
// the existing list items.
merge(existing, incoming) {
if (!incoming) return existing;
if (!existing) return incoming; // existing will be empty the first time
const { items, ...rest } = incoming;
let result = rest;
result.items = [...existing.items, ...items]; // Merge existing items with the items from incoming
return result;
},
},
},
},
},
});

How to create a dynamic slug in Strapi v4?

I want to create a slug for the url as soon as the user adds an event from the frontend. The slug is based on the name of the event. How to do that in V4 as the old method does not work now?
Slug creation link - old version
By following the article, it seems that you are trying to add lifecycle events to a model. You would need to make the following modifications to the article to make it work for v4.
After the creation of the article model via the admin dashboard, instead of adding the following file:
./api/article/models/Article.js
add:
./src/api/article/content-types/article/lifecycles.js
With the following:
const slugify = require('slugify');
module.exports = {
async beforeCreate(event) {
if (event.params.data.title) {
event.params.data.slug = slugify(event.params.data.title, {lower: true});
}
},
async beforeUpdate(event) {
if (event.params.data.title) {
event.params.data.slug = slugify(event.params.data.title, {lower: true});
}
},
};
Also the api endpoint changed in v4 so you would need to use:
GET /api/articles?filters[slug]=my-article-slug
This seem to work for me
Settings > Roles > Public > Slugify (checkbox findSlug)
config/plugins.js
module.exports = ({ env }) => ({
slugify: {
enabled: true,
config: {
contentTypes: {
page: {
field: "slug",
references: "name",
},
post: {
field: "slug",
references: "name",
},
category: {
field: "slug",
references: "name",
},
},
},
},
});
graphql
const POSTS = gql`
query GetPosts {
posts {
... on PostEntityResponseCollection {
data {
__typename
id
attributes {
__typename
name
slug
content
featuredImage {
data {
__typename
id
attributes {
url
alternativeText
caption
}
}
}
createdAt
}
}
}
}
}
`;
const POST = gql`
query GetPost($slug: String!) {
findSlug(modelName: "post", slug: $slug, publicationState: "live") {
... on PostEntityResponse {
data {
__typename
id
attributes {
createdAt
name
slug
content
seo {
__typename
id
title
description
blockSearchIndexing
}
categories {
__typename
data {
__typename
id
attributes {
__typename
name
slug
}
}
}
}
}
}
}
}
`;

Apollo Client delete Item from cache

Hy I'm using the Apollo Client with React. I query the posts with many different variables. So I have one post in different "caches". Now I want to delete a post. So I need to delete this specific post from all "caches".
const client = new ApolloClient({
link: errorLink.concat(authLink.concat(httpLink)),
cache: new InMemoryCache()
});
Postquery:
export const POSTS = gql`
query posts(
$after: String
$orderBy: PostOrderByInput
$tags: JSONObject
$search: String
$orderByTime: Int
) {
posts(
after: $after
orderBy: $orderBy
tags: $tags
search: $search
orderByTime: $orderByTime
) {
id
title
...
}
}
`;
I tried it with the cache.modify(), which is undefined in my mutation([https://www.apollographql.com/docs/react/caching/cache-interaction/#cachemodify][1])
const [deletePost] = useMutation(DELETE_POST, {
onError: (er) => {
console.log(er);
},
update(cache, data) {
console.log(cache.modify())//UNDEFINED!!!
cache.modify({
id: cache.identify(thread), //identify is UNDEFINED + what is thread
fields: {
posts(existingPosts = []) {
return existingPosts.filter(
postRef => idToRemove !== readField('id', postRef)
);
}
}
})
}
});
I also used the useApolloClient() with the same result.
THX for any help.
Instead of using cache.modify you can use cache.evict, which makes the code much shorter:
deletePost({
variables: { id },
update(cache) {
const normalizedId = cache.identify({ id, __typename: 'Post' });
cache.evict({ id: normalizedId });
cache.gc();
}
});
this option worked for me
const GET_TASKS = gql`
query tasks($listID: String!) {
tasks(listID: $listID) {
_id
title
sort
}
}
`;
const REMOVE_TASK = gql`
mutation removeTask($_id: String) {
removeTask(_id: $_id) {
_id
}
}
`;
const Tasks = () => {
const { loading, error, data } = useQuery(GET_TASKS, {
variables: { listID: '1' },
});
сonst [removeTask] = useMutation(REMOVE_TASK);
const handleRemoveItem = _id => {
removeTask({
variables: { _id },
update(cache) {
cache.modify({
fields: {
tasks(existingTaskRefs, { readField }) {
return existingTaskRefs.filter(
taskRef => _id !== readField('_id', taskRef),
);
},
},
});
},
});
};
return (...);
};
You can pass your updater to the useMutation or to the deletePost. It should be easier with deletePost since it probably knows what it tries to delete:
deletePost({
variables: { idToRemove },
update(cache) {
cache.modify({
fields: {
posts(existingPosts = []) {
return existingPosts.filter(
postRef => idToRemove !== readField('id', postRef)
);
},
},
});
},
});
You should change variables to match your mutation. This should work since posts is at top level of your query. With deeper fields you'll need a way to get the id of the parent object. readQuery or a chain of readField from the top might help you with that.

Handling Graphql Mutation update, cache read and writeQuery, if the query is dynamic?

Doing nightlife app on freecodecamp https://learn.freecodecamp.org/coding-interview-prep/take-home-projects/build-a-nightlife-coordination-app/
I am trying to implement 'Go' button, similarly 'Like' button on Youtube or Instagram. Users click the button the number(counting how many users go) goes up meaning users will go there and click again, it revokes, the number decreases, users will not go there.
It seems like working well except the issue, I have to refresh the page and then, the number has increased or decreased and throws the error like below so:
Invariant Violation: Can't find field getBars({}) on object {
"getBars({\"location\":\"vancouver\"})": [
{
"type": "id",
"generated": false,
"id": "Bar:uNgTjA9ADe_6LWby20Af8g",
"typename": "Bar"
},
{
"type": "id",
"generated": false,
"id": "Bar:CwL5jwXhImT_7K5IB7mOvA",
"typename": "Bar"
},
{
"type": "id",
"generated": false,
"id": "Bar:mdt1tLbkZcOS2CsEbVF9Xg",
"typename": "Bar"
},
.
.
.
I am assuming handling update function will fix this issue but unlike the example from Apollo documentation:
// GET_TODOS is not dynamic query
// nothing to pass as variables to fetch TODO list
<Mutation
mutation={ADD_TODO}
update={(cache, { data: { addTodo } }) => {
const { todos } = cache.readQuery({ query: GET_TODOS });
cache.writeQuery({
query: GET_TODOS,
data: { todos: todos.concat([addTodo]) },
});
}}
>
My query is dynamic:
// I have to pass location variable, otherwise it won't fetch anything.
const GET_BARS_QUERY = gql`
query getBars($location: String!) {
getBars(location: $location) {
id
name
url
rating
price
image_url
goings {
username
}
goingCount
}
}
`;
I believe I might need to handle to provide location using readQuery and writeQury but not too sure what I should do.
Here's my code:
const GoButton = ({ user, bar }) => {
const { token } = user;
const { id, goings, goingCount } = bar;
const [userGoes] = useMutation(GO_MUTATION, {
variables: { yelp_id: id },
update(proxy, result) {
const data = proxy.readQuery({
query: GET_BARS_QUERY
});
data.getBars = [result.userGoes, ...data.getBars];
proxy.writeQuery({ query: GET_BARS_QUERY, data });
}
});
return (
<Button onClick={userGoes}>
Go {goingCount}
</Button>
);
};
const GO_MUTATION = gql`
mutation go($yelp_id: String!) {
go(yelp_id: $yelp_id) {
id
goings {
id
username
}
goingCount
}
}
`;
export default GoButton;
Full code here https://github.com/footlessbird/Nightlife-Coordination-App
when you read/write the getBars query, you need to pass the location as a variable
const [userGoes] = useMutation(GO_MUTATION, {
variables: { yelp_id: id },
update(proxy, result) {
const data = proxy.readQuery({
query: GET_BARS_QUERY,
variables: {
location: 'New York'
}
});
data.getBars = [result.userGoes, ...data.getBars];
proxy.writeQuery({ query: GET_BARS_QUERY, data,
variables: {
location: 'New York'
}
});
}
});

Resources