Apollo cache fetchMore & mutation - apollo-client

I'm trying to create a page with a load more and with a button that execute a mutation against a particular item of the list, this is the query:
const { data, fetchMore, networkStatus } =
useSearchItemsQuery({
notifyOnNetworkStatusChange: true,
variables,
});
this is the fetchMore:
fetchMore({
variables: {
offset: data.searchItems.nodes.length,
},
});
this is my apollo config:
return new ApolloClient({
...
cache: new InMemoryCache({
typePolicies: {
Item: {
keyFields: ["uuid"],
merge: true,
},
Query: {
fields: {
searchItems: {
keyArgs: false,
merge(existing = [], incoming, { args }) {
console.log("options", args);
return deepmerge(existing, incoming, {
arrayMerge: (destinationArray, sourceArray) => {
const refs = [...destinationArray, ...sourceArray].map(
(o) => o.__ref
);
const array = [...destinationArray, ...sourceArray].filter(
({ __ref }, index) => !refs.includes(__ref, index + 1)
);
console.log("array", array);
return array;
},
});
},
},
},
},
},
}),
});
};
the fetchMore works correctly but when I try to run a mutation I can see the changes on the screen but the searchItems query is been refetched and the list on the screen update with the initial state, before the fetchMore.
Example:
open the page and 1 item appear
click fetchMore
a new item appear
run a mutation against the 1st item or the 2nd item
the list now contains only the 1st item
I can see on the network tab the after the mutation the initial query it's been executed with the initial offset/limit

Related

Apollo Graphql fetchMore, updateQuery does not update state

I'm currently trying to implement pagination on my posts.
Using Apollo graphql here is my useQuery
const { data: postsData, fetchMore } = useQuery(POSTS_BY_USER_DRAFT, {
fetchPolicy: 'network-only',
variables: {
user: user.id,
start: 0,
limit: limit
},
onCompleted: () => {
setTotal(postsData[model].meta.pagination.total)
}})
and here is my onClick handler for fetching more posts
const loadMorePosts = async () => {
const nextStart = start + limit
setStart(nextStart);
await fetchMore({
variables: {
user: user.id,
offset: nextStart,
limit: limit,
},
updateQuery: (prevResult, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return prevResult
}
const prevData = prevResult[model].data
const moreData = fetchMoreResult[model].data
fetchMoreResult[model].data = [...prevData, ...moreData]
// fetchMoreResult[model].data = [...moreData]
return fetchMoreResult
},
})}
My queries are successful as I do get correctly the data, however postsData does not get updated
[NOTICED]: If I switch fetchMoreResult[model].data = [...prevData, ...moreData] for
fetchMoreResult[model].data = [...moreData] my postsData does get updated.
I have tried return { ...fetchMoreResult } and multiple ways of returning data fearing an immutability/comparaison issue but it does not seem to do the job.
I'm not sure why, but setting a fetchPolicy for Apollo will do the job
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache({
typePolicies: {
Publication: {
merge: true,
},
Post: {
merge: true,
},
},
}),
defaultOptions: defaultOptions,
})

Pagination Apollo Client 3 - Cache merges but does not render new results when paginating

I'd love to implement nested pagination within my application. I have been reading the docs and looking at several other examples but I just can't get this to work - any help is appreciated! Thanks!
React component:
I am clicking the button to run the fetchMore function provided by the useQuery hook (apollo). The network request is going through and the new products are merged into the cache... but no new products render on the page.
export const FilterableKit = () => {
const selectedKitId = useReactiveVar(selectedKitIdVar);
const [
getKitProducts,
{ data: getKitProductsData, loading: getKitProductsLoading, fetchMore },
] = useGetKitProductsLazyQuery();
useEffect(() => {
if (selectedKitId) {
getKitProducts({
variables: {
getKitsInput: {
_id: {
string: selectedKitId,
filterBy: "OBJECTID" as StringFilterByEnum,
},
},
getProductsInput: {
config: {
pagination: {
reverse: true,
limit: 3,
},
},
},
},
});
}
}, [getKitProducts, selectedKitId]);
const kitProducts = getKitProductsData?.getKits.data?.find(
(kit) => kit?._id === selectedKitId
)?.products.data;
const handleLoadMore = () => {
if (kitProducts && kitProducts?.length > 0) {
const remaining =
getKitProductsData?.getKits.data[0]?.products.stats?.remaining;
if (remaining && remaining > 0) {
const cursor =
kitProducts[kitProducts.length - 1] &&
kitProducts[kitProducts.length - 1]?.createdAt;
fetchMore({
variables: {
getProductsInput: {
config: {
pagination: {
reverse: true,
createdAt: cursor,
},
},
},
},
});
}
}
};
return (
<CContainer>
<KitItemCards products={kitProducts} loading={getKitProductsLoading} />
<CContainer className="d-flex justify-content-center my-3">
<CButton color="primary" className="w-100" onClick={handleLoadMore}>
Load More
</CButton>
</CContainer>
</CContainer>
);
};
Type Policies: I define the "Kit" typePolicy to merge products into the correct field.
export const cache: InMemoryCache = new InMemoryCache({
typePolicies: {
Kit: {
fields: {
products: {
keyArgs: false,
merge(existing = [] as Product[], incoming: GetProductsResponse) {
if (!incoming) return existing;
if (!existing) return incoming;
const { data: products, ...rest } = incoming;
let result: any = rest;
result = [...existing, ...(products ?? [])];
return result;
},
},
},
},
});
Thanks for any pointers in the right direction! Let me know if there is something else you'd like to see.

Optimistic response not working when adding items to list

My data model is a list with items. Very simple:
{
_id: 1,
name: "List 1",
items: [
{ _id: 2, text: "Item text 1" },
{ _id: 3, text: "Item text 2" }
]
}
Adding a new list with optimistic response works perfectly:
const [addListMutation] = useAddListMutation({
update: (cache, { data }) => {
const cachedLists =
(cache.readQuery<GetAllListsQuery>({
query: GetAllListsDocument,
})?.lists as TList[]) ?? [];
if (data) {
cache.writeQuery({
query: GetAllListsDocument,
data: {
lists: [...cachedLists, data?.list as TList],
},
});
}
},
});
const addList = async (name: string) => {
const list = {
_id: ..new id here,
name,
items: [],
};
const variables: AddListMutationVariables = {
data: list,
};
await addListMutation({
variables,
optimisticResponse: {
list,
},
});
};
This gets reflected immediately in my component using const { loading, data } = useGetAllListsQuery();. data is updated twice; first with the optimistic response and then after the mutation is done. Just like expected.
Now I'm trying to add an item to the list this way:
const [updateListMutation] = useUpdateListMutation({
update: (cache, { data }) => {
const cachedLists =
(cache.readQuery<GetAllListsQuery>(
{
query: GetAllListsDocument,
},
)?.lists as TList[]) ?? [];
if (data?.list) {
// Find existing list to update
const updatedList = data?.list as TList;
const updatedListIndex = cachedLists.findIndex(
(list: TList) => list._id === updatedList._id,
);
// Create a copy of cached lists and replace entire list
// with new list from { data }.
const updatedLists = [...cachedLists];
updatedLists[updatedListIndex] = { ...updatedList };
cache.writeQuery({
query: GetAllListsDocument,
data: {
lists: updatedLists,
},
});
}
}
});
const updateList = async (updatedList: TList) => {
const variables: UpdateListMutationVariables = {
query: {
_id: updatedList._id,
},
set: updatedList,
};
await updateListMutation({
variables,
optimisticResponse: {
list: updatedList,
},
});
};
const addListItem = async (list: TList, text: string) => {
const updatedList = R.clone(list);
updatedList.items.push({
_id: ...new item id here,
text: 'My new list item',
});
await updateList(updatedList);
};
The problem is is in my component and the const { loading, data } = useGetAllListsQuery(); not returning what I expect. When data first changes with the optimistic response it contains an empty list item:
{
_id: 1,
name: "List 1",
items: [{}]
}
And only after the mutation response returns, it populates the items array with the item with text 'My new list item'. So my component first updates when the mutation is finished and not with the optimistic response because it can't figure out to update the array. Don't know why?
(and I have checked that the updatedLists array in writeQuery correctly contains the new item with text 'My new list item' so I'm trying to write the correct data).
Please let me know if you have any hints or solutions.
I've tried playing around with the cache (right now it's just initialized default like new InMemoryCache({})). I can see the cache is normalized with a bunch of List:1, List:2, ... and ListItem:3, ListItem:4, ...
Tried to disable normalization so I only have List:{id} entries. Didn't help. Also tried to add __typename: 'ListItem' to item added, but that only caused the { data } in the update: ... for the optimistic response to be undefined. I have used hours on this now. It should be a fairly simple and common use case what I'm trying to do :).
package.json
"#apollo/client": "^3.3.4",
"graphql": "^15.4.0",
"#graphql-codegen/typescript": "^1.19.0",

React reducer question on changing a returned payload

I am trying to either add a new field into a returned payload or add a new field copying the contents of another field in the returned payload object. Here is my reducer code...
[actionTypes.GET_PAYTYPE_CONTRIBUTORS]: (state, action) => {
return {...state, paytypeContributors: { ...action.payload }, loadingPaytypeContributors: false, }
},
For each entry in action.payload.Items, I need to either change the field name ID to Id or add Id to the payload Items array with the same contents as the ID field has.
Here is where I tried to do this...
[actionTypes.GET_PAYTYPE_CONTRIBUTORS]: (state, action) => ({...state, paytypeContributors: { ...action.payload, Id: action.payload.Items.ID }, loadingPaytypeContributors: false}),
The payload returns an object and then Items inside of the object is an array and ID is a field in the array. Any ideas on how to do this?
If I understood correctly, that would be my take on this:
const actionFunc = (state, action) => ({ ...state,
paytypeContributors: { ...action.payload,
Items: action.payload.Items.map(Item => {
const newItem = { ...Item,
Id: Item.ID
};
delete newItem['ID'];
return newItem;
})
},
loadingPaytypeContributors: false
});
const state = {};
const action = {
payload: {
Items: [{
ID: 1
},
{
ID: 2
},
{
ID: 3
},
{
ID: 4
}
]
}
}
console.log(actionFunc(state, action));

How to redirect after a deleting mutation in Apollo?

After I delete a post, I want to update the cache and redirect to the post index page.
deletePost() {
this.$apollo.mutate({
mutation: DELETE_POST,
variables: {
postId: this.postId
},
update: (cache, { data: { deletePost } }) => {
const query = {
query: GET_PAGINATED_POSTS,
variables: {
page: 0,
pageSize: 10
},
};
const data = cache.readQuery({ ...query });
data.postsPage = data.postsPage.filter(post => post._id != this.postId)
cache.writeQuery({ ...query, data })
}
})
// redirect
this.$router.push({ name: 'IndexPosts' })
}
The above works, but since I'm not doing an optimisticResponse, there's a bit of a delay between the time the index page shows and the time the cache update takes place. How can I solve this? I was trying to do an optimisticResponse but I don't know how to get the list of paginated posts without doing another query.
this.$apollo.mutate(...) returns a promise.
Try something like:
this.$apollo.mutate(...)
.then(({ data: { deletePost } }) => {
this.$router.push({ name: 'IndexPosts' })
})

Resources