Redux toolkit query mutation optimistic update - reactjs

RTKQ I'm creating a mutation. I'm trying to update it with the optimistic update method, but in the examples I've found, the scenario is always like this. postEdit mutation works. getPost request is made in onQueryStarted and it is finalized. In my scenario, instead of sending a getPost request, postEdit returns the current post value as a response if the statusCode is positive. In this case, I couldn't find how to update the store, do you have any ideas?

That's not the right way to use RTK Query or any other fetch library that handles revalidation for you like useSWR. You want to retrieve data from a query and perform revalidation after mutations on that data so the query gets automatically re-executed after each mutation and your UI is updated.
Let me give you this example I used in one of my applications:
addPost: builder.mutation({
query: (body) => {
console.log('ADDING POST', body)
return {
url: `add-post`,
method: 'POST',
body,
}
},
invalidatesTags: ['Posts'],
onQueryStarted(body, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
socialApi.util.updateQueryData(
'getPosts',
{ _id: body.profileOwner_id, limit: body.limit },
(draft) => {
console.log('POSTS ARRAY DRAFT', draft)
draft.unshift({
...body,
shareable: true,
title: 'published a new post.',
timestamp: new Date(),
likes: [],
})
}
)
)
queryFulfilled.catch(patchResult.undo)
},
}),
In this case I invalidate 'Posts' which triggers the query to User's Posts after the user adds a new Post. No need to add the new post manually in response to the mutation.
And as you can see, this way I can use the optimistic update util method .updateQueryData(), "draft" is the Posts list, I just add a new Post object to that array and that is automatically rendered as soon as the mutation is triggered.

Related

Why is queryClient.setQueryData taking so long in this example with react-query?

I'm updating cache with react-query using useQuery and setQueryData, the problem is setQueryData can take up to 2mins to update the data, possibly due to a loop of some sort. I'm mapping each page on the 'Styles' data, and updating the Styles, Groups and Ranges on the page that matches the pageIndex with data from the response. I have no idea why the very long update, an I using react-query wrong here? The data being updated is not huge at all by the way.
Thanks in advance.
export const useStyle = (styleId, pageIndex) => {
const queryClient = useQueryClient();
const { refetch } = useQuery('Style', () => fetchStyle(styleId), {
staleTime: Infinity,
refetchOnWindowFocus: false,
initialData: {},
onSuccess: (res) => {
queryClient.setQueryData('Styles', (oldData) => ({
pages: map(oldData.pages, (page, index) => ({
...page,
...(index === pageIndex
? {
Styles: {
...page.Styles,
...res.Styles,
},
Groups: {
...page.Groups,
...res.Groups,
},
Ranges: {
...page.Ranges,
...res.Ranges,
},
}
: {}),
})),
pagesParams: oldData.pageParams,
}));
},
});
return { refetchStyle: refetch };
};
I have solved this problem by using useMutation instead of useQuery and setQueryData, this is the better approach if a loop of onSuccess'es is caused by setQueryData.
I think you're creating an infinite loop here:
useQuery('Styles') subscribes to the key Styles, so this component will always update / re-render whenever something in the cache under that key changes
in onSuccess of this query, you update that very same key (Styles)
this update informs all observers, because they should be aware of that change
calling setQueryData also triggers onSuccess, because setQueryData is to be treated the same as if the data came from the backend via the queryFn.
this will trigger your onSuccess again and so on...
The better question would be: What problem are you trying to solve? By the looks of it, you want to perform some data transformation after each fetch. For this, you have multiple options, all of which I have outlined extensively here: https://tkdodo.eu/blog/react-query-data-transformations
But usually, merging data like that should not be necessary. The cache should best be a 1:1 mapping of the response you get from the server.

Why react useQuery() doesnt fetch the newest data after a mutation?

I have a code like this
const [inputComment, setInputComment] = useState('');
const [
commentPost,
{ data: data4, loading: loading4, errorCreate4 },
] = useMutation(COMMENT_POST);
const { error: error2, loading: loading2, data: data2 } = useQuery(
GET_POST_BY_ID,
{
variables: {
postid: item.id,
},
},
);
const doComment = () => {
commentPost({
variables: {
postId: item.id,
userEmail: email,
comment: inputComment,
},
})
.then(({ data }) => {
setInputComment('');
console.log('success');
})
.catch((e) => {
console.log('not success');
});
};
This is supposed to get the data, and when I do comment then it runs the mutation and re-render everything.
My problem is, it re-render alright BUT the data that the useQuery fetch is not the newest data a.k.a the data before I add a new comment.
Does anyone know how to fix this problem??
Please help :(
Your mutation modifies data on the server side.
Once your mutation is done, you should refetch your data in order to get the modified version in your local cache on the client side.
By guessing how your mutation and query actually work, here is how it would look like:
const [
commentPost,
{ data: data4, loading: loading4, errorCreate4 },
] = useMutation(COMMENT_POST, {
refetchQueries: [
{ query: GET_POST_BY_ID, variables: { postid: item.id } }
]
});
Otherwise, intead of refetching from the server, you could update the local cache directly.
More info can be found here in the official documentation.
I assume commentPost is an insert operation, not an update of a single record. In this case, Apollo useMutation will not update the cache for you. You need to modify the cache yourself. The official Apollo documentation has covered this use case with an example. You may want to revise the usage of writeFragment as well.
Below are directly from apollo docs on cache update for list fields.
In most cases, a mutation response should include any object(s) the
mutation modified. This enables Apollo Client to normalize those
objects and cache them according to their __typename and id fields (by
default).
...
When a mutation's response is insufficient to update all modified
fields in your cache (such as certain list fields), you can define an
update function to apply manual changes to your cached data after a
mutation.
const [addTodo] = useMutation(ADD_TODO, {
update(cache, { data: { addTodo } }) {
cache.modify({
fields: {
todos(existingTodos = []) {
const newTodoRef = cache.writeFragment({
data: addTodo,
fragment: gql`
fragment NewTodo on Todo {
id
type
}
`
});
return [...existingTodos, newTodoRef];
}
}
});
}
});
EDIT
I noticed another answer suggests using refetch, which is not a bad option for starters. However, updating the cache is the recommended approach over refetch. You can refer to the Apollo blog article When To Use Refetch Queries in Apollo Client.
Below are some quotes you should note from this article.
If you’re just getting started with GraphQL, I think the mental model of passing in the queries that you’d like to re-run after a mutation is an easy one to wrap your head around.
...
The advantage here is that this approach is straightforward. The disadvantage is that we’re fetching the entire list of data again when we might not need to.
...
For a more efficient use of bandwidth and network round-trips, we can rely on cache normalization and update functions.

When to use Redux to fetch data from api call

I adopted Redux in my project for state control, and also Axios for fetching api in action.
But I wonder when should I fetch data using API call through Redux (in action), when should I directly make the api call in component.
Is it depending on, whether I need to store the response data in Redux (for sharing among different components)? May I know any best practice for it?
API call through Redux
export const fetchOptions = () => {
return async (dispatch, getState) => {
const request = await client.query({
query: gqlQueries.getQuery,
});
const options = await request;
dispatch({
type: types.FETCH_DATA_END,
options: options
});
}
}
Directly make API call in component:
const axios = require("axios");
useEffect(() => {
axios({
url: 'http://localhost/graphql',
method: 'post',
data: {
query: `
query PostsForAuthor {
author(id: 1) {
firstName
posts {
title
votes
}
}
}
`
}
}).then((result) => {
console.log(result.data)
});
}, []);
If multiple components are using the same data, redux shines there. API calls in components are preferred when you do not want any stale data to show, therefore you call api every time component mounts and your data is always in sync with your back end. There might be some other criteria but these two help me decide , where to keep the state.

Apollo Mutation - UI not updated after useMutation update

Background: always able to make Apollo store cache updated, but not UI
Question:
what's the reason that makes the UI updated or not?
what's the right way to pass the data object in update?
"react": "~16.9.0"
"#apollo/react-hooks": "^3.1.3"
Both UI and cache updated codes in my project:
update: (store, { data: { newPhoto } }) => {
const { bookInfo } = store.readQuery({ query: GET_BOOK_BY_ID, variables: { bookId } });
bookInfo.photos = [...bookInfo.photos, newPhoto];
store.writeQuery({
query: GET_BOOK_BY_ID,
variables: { bookId },
data: {
bookInfo
}
});
}
In this line: bookInfo.photos = [...bookInfo.photos, newPhoto];, the bookInfo object is amended directly and just passed back to writeQuery's data
This doesn't look okay to me as I saw people saying it needs to be "immutable" or "passing new object", etc.
if you experience the same thing, please check following list:
go to check out https://github.com/apollographql/apollo-client/pull/4543. By applying the freezeResults & assumeImmutableResults into the ApolloClient will help detect the issue. For my case, the issue actually occurs inside the parent component, which I mutated the Apollo store objects, instead of the component that calling client.writeQuery, which is generally hard for others to notice too, in my opinion.
const client = new ApolloClient({
link: ...,
cache: new InMemoryCache({
freezeResults: true, // new
}),
assumeImmutableResults: true, // new
});
Ensure you mutate the data in an immutable fashion. (i.e. Apollo store object hasn't been changed until the end of update) https://github.com/immerjs/immer definitely helps to keep your change in immutable fashion. I used this to mutate my nested object and it's working so well.
Try to use client returned from the useMutation, then you get client.writeQuery to do the update. Although I'm not sure about this point, a lot of people spreading this message, probably help in some cases.
import { useMutation } from '#apollo/react-hooks';
import produce from "immer";
const [mutate, { client }] = useMutation(MUTATION_GQL);
const submit = () => {
mutate({
variables: { inputs },
update: (store, { data: { response }) => {
// get existing cache returned from the query
const cache = store.readQuery({ query: QUERY_GQL, variables: { id } });
// manipulate the cache in immutable fashion
const data = produce(cache, draftCache => {
draftCache.title = "new title";
draftCache.approval = response;
});
// write the cache back to that query
// REMEMBER the variables inside writeQuery too!
client.writeQuery({
query: QUERY_GQL,
variables: { id },
data,
});
}
})
}
Try to use useQuery to read the data from ApolloClient instead of readQuery so you will get the updated cache from Apollo's store

UseApolloClient query won't return fetchMore

I am working on project with Apollo on client side. I am using react-apollo-hooks on my client side. And I have a problem with useApolloClient.
When i fire query with my client I got in useApolloClient I don't get back all data I need. FetchMore is missing. If I use regular query (useQuery) I get that. But problem is I need to fire that query on click and i need to use one provided with apollo client.
I have this function for fetching data on click:
const bulkSearch = async data => {
setContent(<Spinner />);
showModal();
try {
const response = await client.query({
query: BULK_SEARCH_PRODUCTS,
variables: { data }
});
if (!response.loading) {
setContent(
<ProductsListDisplay
products={response.data.bulkSearch.products}
fetchMore={response.fetchMore}
count={{ total: 10 }}
/>
);
return 200;
}
} catch (err) {
return 400;
}
};
And response doesn't contain fetchMore.
On the other way classic query returns fetchMore.
const newdata = useQuery(BULK_SEARCH_PRODUCTS, {
variables: { data: { ids: ["536003", "513010"] } }
});
Some help ? Thank you!
According to the apollo-client docs, ApolloClient.query returns a Promise that resolves to an ApolloQueryResult, which is a simpler object that has only data, errors, loading, networkStatus, and stale as properties.
On the other hand, the render prop argument of react-apollo's Query component gets fed a much richer object, with fetchMore being one of its additional properties. If you want to do something similar using the raw ApolloClient object, you would have to use ApolloClient.watchQuery, which returns an ObservableQuery that you can subscribe to consume results. The benefit of this is that you have access to more methods, such as ObservableQuery.fetchMore.
Note this approach is fundamentally different than using ApolloClient.query, since that function requests one query and returns the result as a Promise, while ApolloClient.watchQuery consistently monitors your cache and pushes updated results to your subscribe method when the cache store changes, so it's a more complicated lifecycle. In general, if you're already using react-apollo or one of the #apollo/react-X packages, you probably want to stay away from ApolloClient.watchQuery, since the functionality from those libraries builds directly on top of it and is designed to be easier to consume.
Hope this helps!
You have to create your own FetchMore method for this. This has to be handled by you that's the safer you to go.
In my case I needed
fetchMore
Adding Infinite loading and should event deal with loading state as well.
Problem with default loading state is that it will be always false as return of promise.
When you use await client.query.
In our query we have cursor based pagination.
read this
Create Function that will trigger on scroll ( end of page )
Check on value of after and update it with state management
Loading as well as data also needs to be in state.
Code:
const fetchMoreFilteredData = async (after) => {
try {
setFilteredLoading(true); // set this state in order to show loading indicator
const { data, loading } = await client.query({
query: QUERY,
variables: {
after: after,
...all variables,
},
fetchPolicy: "network-only",
notifyOnNetworkStatusChange: true,
});
const {
query: {
pageInfo: { hasNextPage, endCursor },
},
} = data;
setFilteredData({
// update your data ...filteredData,
});
setHasNextPage(hasNextPage); // check if there is next page
setEndCursor(endCursor); // set end cursor for next page this will guide the query to fetch next page
setFilteredLoading(loading); // set loading state to false
} catch (error) {
error.graphQLErrors.map((error) => {
console.log("error", error.message);
});
setFilteredLoading(false);
} };
const handleLoadMore = () => {
hasNextPage && fetchMoreFilteredData(_endCursor);
};

Resources