Order of cache items after update mutation using Apollo Client - reactjs

I have a question concerning Apollo Client's behaviour when dispatching an update mutation.
I have a little application that fetches data and allows you to modify it. After the modification, an update mutation is sent to graphQL. The changes can be seen instantly on the UI since the update of a single item triggers an automatic cache update by Apollo.
However, I noticed that when I refresh the page after an update, the order of the items I previously fetched is changed with the recently updated item going at the end of the list.
I was just wondering if this is the normal behaviour to expect and if there was a way to force the cache to keep the same order after an update?
Edit: Here's the code for my resolver, mutation and useMutation call.
Resolver:
async UpdateUser(parent, args, ctx, info) {
const { id, input } = args;
const updatedUser = await ctx.prisma.user.update({
where: {
id,
},
data: {
...input,
},
});
return updatedUser;
}
Mutation:
export const UPDATE_USER_MUTATION = gql`
mutation UpdateUser($id: String, $input: CreateUserInput) {
UpdateUser(id: $id, input: $input) {
id
name
email
}
}
`;
useMutation:
UpdateField({
variables: {
id: data.fieldID,
input: {
[data.fieldName]: value,
},
},
});
Edit 2: Here's a gif what's going on..
Thank you!

Mutation update option can update (or insert) properly list/array (query cached result) following BE sorting ...
... but it will fail on longer datasets, paginated results - on list query refetch record can be removed from current [page] list/array. It will be more confusing behavior.
IMHO fighting is not worth the effort, it's already acceptable behavior (mutation of indexed/ordering field).
Possible solutions:
don't refetch list query, only update the query list cache (mutation update - it will update the list view);
for small datasets, use FE sortable table component (BE order doesn't matter).

Related

Use useEffect or not

Is it bad practice to use useEffect?
And should useEffect be avoided if possible due to re-renders?
This question arised yesterday when a colleague asked for a code review and we had different opinions on how to solve this.
We are creating an app that shows some kind of documentation which could be sorted in chronological or reversed chronological order. This is decided by a button in the apps top bar with a default value of chronological order, this value is stored in a global redux state and will be used in every call to fetch documentation.
In this example we update sortOrder on button click and as an effect of that we fetch data.
If I understand this correctly, we render once when sortOrder state change, and once after data is fetched.
Pseudo code ish
interface AppState = {
sortOrder: SortOrder:
documentation: Documentation[];
}
reducer(){
case toggleSortOrder:
const order = state.sortOrder === 'asc' ? 'desc' : 'asc';
return {
....state,
sortOrder: order;
}
}
const AppBar = () => {
const dispatch = useDispatch();
return <div><button onClick={dispatch(toggleSortOrder)}>Change sort order</button>
</div>;
}
const DocumentationList = (type: DocumentationType) => {
const dispatch = useDispatch();
const sortOrder = useSelector((state) => state.appState.sortOrder);
const documentation = useSelector((state) => state.appState.documentation);
useEffect(() => {
// action is caught by redux-saga and a call to docApi is made through axios
dispatch(getDocumentation.request(type, sortOrder)
},[sortOrder]);
return documentation.map((doc) => <Documentation data={doc} />);
}
Is this bad practice?
Should we avoid useEffect and fetch data on click and update sortOrder in saga instead?
Reading docs and blogs I mostly see examples of how en when to use them.
In my opinion, I would go with solution more-less like yours, with splitting responsibilities between element which externally changes query params, and element which is displaying data based on current query params. If you decide to put all logic in button click handler then you are kinda coupling too much list and button, because in order to delegate all work to button click you must dig into DocumentationList fetch-data implementation and to copy it to another place(button related saga or in button click handler) in order to fetch data from another place in the app, and not just from the DocumentationList itslef.
From my perspective, only DocumentationList should be responsible to fetch data, and noone else. But you should provide way to subscribe, from documentation list, to some external query params(sort, filters etc) and when they change(if they exist) data should be loaded.
Right now you only have sort, but in case when you can potentially have more params that can be externally modified, then I would dedicate more complex redux part to query params, something like documentationQueryParams: { sort: "asc", filters: { name: "doc a", type: "type b" } }, and then inside DocumentationList I would use custom hook, for example const queryParms = useDocumentationQueryParams(); which will return standardized query params, and in useEffect I would subscribe to those queryParms change - whenever they change I will easily fetch new data since you know what is the structure of the queryParms(they must be standardized is some way). Like this you coupled them but in very flexible way, whenever you need new param you will update only filter/query-related component, because in DocumentationList you relay on standardized hook output and you can easily create generic mechanism to output query string, or body data, in order to make new request and to fetch new data.
In terms of performance, there is really no difference between hooks-based approach and moving all to click handlers, because in DocumentationList your render part should rerender only when list change, no matter how list is being changed.

RTK Query sustain `isLoading` for auto-refetching after cache invalidation

Having a hard time finding a good answer/ solution for this problem.
I have a POSTS list component that allows to delete individual POST rows.
I'm using a run of the mill queryMutation for deletion purposes:
IMPLEMENTATION
deletePostById: build.mutation<{ success: boolean; id: number }, number>({
query(id) {
return {
url: `post/${id}`,
method: 'DELETE',
}
},
invalidatesTags: (result, error, id) => [{ type: 'Posts', id }],
})
USAGE
const [deletePost, { isLoading: isDeleting, error: deletionError }] = useDeletePostByIdMutation();
PROBLEM DESCRIPTION
Within the list component the individual row has an icon to delete that Post - while isDeleting I'm showing a spinner, which is also showing just fine - however, when the Post is deleted the auto-refetching of RTKQ kicks in to get the now updated Posts from the server - isDeleting is no longer true though.
This leaves a small time window in which the Post row is not showing any spinner but also is not removed yet from the Posts list.
Once the refetched data of all Posts has successfully returned the deleted Post row gets removed from the list.
How can I sustain the spinner animation from deleting the individual Post till the removal after the automatic refetching of RTKQ has finished?
Thanks
You can use isFetching on the list query instead of isLoading, which is only true for the initial request, not refetches.

Unable to fetch data with useQuery hook for a remote query that has some #client variables

Given below is the query that fetches a list of products and stores it in the apollo-client local cache. It retrieves a list of products and the list of images for every product
const GET_PRODUCTS_QUERY = gql`
query Products($cursor: String, $query: String, $imageCount: Int = 100) {
products(first: 9, after: $cursor, query: $query) {
edges {
cursor
node {
id
selected #client
title
description
images(first: $imageCount) {
edges {
node {
id
originalSrc
selected #client
}
}
}
}
}
}
}
`;
I have a bunch of products with selected: true and each of this products will have one or more images with selected: true.
The GET_PRODUCTS_QUERY is being used by a ProductsList component that fetches the products and renders it. This is working fine. When the user selects some product, the selected is marked as true in the local cache and the ProductsList component rerenders. So far so good.
I have another component where I need to use the same GET_PRODUCTS_QUERY to fetch the products but only from the cache. This component should listen to the list of products in the cache and whenever something changes in the cache, it should rerender. I was trying the following to no avail
export default () => {
const { data: productsData = {} } = useQuery(GET_PRODUCTS_QUERY, { fetchPolicy: "cache-only" });
const { selectedProducts = [] } = productsData;
//Render selected products
}
During initial render, the query is executed and nothing is returned as expected.
After ProductsList fetched the list of products and populated the cache, I expected the render function to be called as I was listening on the query but it did not happen
I manually affected a render by changing some other props so that the useQuery will be triggered. But I still did not get any products from the cache inspite of having it there.
Can anyone please explain where I've gone wrong here?
You're fetching two different queries -- you would need to ensure that not only the query is the same but that the variables passed to both hooks are the same as well.
Think of it this way. When you fetch, for example, 20 products
products(first: 20)
Apollo normalizes the result when storing it in the cache. Each product is stored individually and assigned a key. Then the products field is associated with those keys. However, imagine if you then queried for 21 products:
products(first: 21)
If Apollo ignored the argument values passed to the field, it would treat this as a cache hit and just return the 20 products associated with the earlier query. Luckily, it doesn't do that. Each list of keys is associated not just with the field but also with the arguments provided to the field. If the arguments change, and that combination of arguments isn't already in the cache, then Apollo knows it needs to fetch the data from the server.

GraphQL Automatic refetch on empty responses

I want to randomize movies from theMovieDB API. First I send a request to access the ID of the latest entry:
const { loading: loadingLatest, error: errorLatest, data: latestData, refetch: refetchLatest } = useQuery(
LATEST_MOVIE_QUERY
);
Then I want to fetch data from a randomly selected ID between 1 and the number of the latest id. Using a variable dependant on the first query seems to break the app, so for now I'm just using the same movie every time upon mounting the component:
const [
movieState,
setMovieState
] = useState(120);
const { loading, error, data, refetch } = useQuery(ONE_MOVIE_BY_ID_QUERY, {
variables : { movieId: movieState },
skip : !latestData
});
I want to press a button to fetch a new random movie, but the problem is that many of the IDs in the API lead to deleted entries and then I get an error back. I want to keep refetching until I get a good response back but I have no idea to implement it. Right now my randomize function just looks like this:
const randomizeClick = () => {
let mostRecentID = latestData.latestMovie.id;
setMovieState(Math.floor(Math.random() * mostRecentID));
};
I'd be grateful if someone can help me how to implement this.
I think what you needs is the "useLazyQuery" functionality of Apollo. You can find more information about it here: https://www.apollographql.com/docs/react/data/queries/#executing-queries-manually
With useLazyQuery you can change your variables easily and this is meant to be fired after a certain event (click or something similar). The useQuery functionality will be loaded when the component is mounted.

React-Apollo - unclear usage of readQuery/writeQuery in mutation's update function

I'm not clear at all about how readQuery and writeQuery work in the context of the update function passed to a mutation function in react-apollo.
It seems that readQuery returns a reference to the cache, mutating the returned object directly triggers a rerender across my components with the relevant changes, why then do I need to call writeQuery?
Example directly from the docs (https://www.apollographql.com/docs/react/features/optimistic-ui.html) of an update function passed into mutate :
update: (proxy, { data: { submitComment } }) => {
// Read the data from our cache for this query.
const data = proxy.readQuery({ query: CommentAppQuery });
// Add our comment from the mutation to the end.
data.comments.push(submitComment);
// Write our data back to the cache.
proxy.writeQuery({ query: CommentAppQuery, data });
}
now if I comment out the last line :
update: (proxy, { data: { submitComment } }) => {
// Read the data from our cache for this query.
const data = proxy.readQuery({ query: CommentAppQuery });
// Add our comment from the mutation to the end.
data.comments.push(submitComment);
// Write our data back to the cache.
// proxy.writeQuery({ query: CommentAppQuery, data });
}
My component and UI would change as soon as the data returned from proxy.readQuery is mutated. What added benefit is there to calling proxy.writeQuery?
Initially I thought proxy.writeQuery would be necessary to notify React of the changes and trigger a re-render to my components, but evidently this is not the case.

Resources