Undo action in RTK query for optimistic update - reactjs

I am using React with Redux Toolkit and RTK Query. For optimistic updates, I would need possibility to undo any dispatched action. RTK Query allows this using undo() method on dispatch result.
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query'
const api = createApi({
endpoints: (build) => ({
// Action for getting the list of all items.
getItems: build.query({ query: () => 'items' }),
// Action for adding new item.
addItem: build.mutation({
query: (body) => ({ url: 'item', method: 'POST', body }),
onQueryStarted({ id, ...body }, { dispatch, queryFulfilled }) {
const patch = dispatch(api.util.updateQueryData('getItems', undefined, (items) => [...items, body]))
queryFulfillled.catch(patch.undo) // On fail, undo the action (so remove the added item)
}
})
})
})
However, the problem is when I have an array of items, to which some action adds new item. How can I revert it? undo method just set array to the state in which it was before dispatching the action. But what if I dispatch the action multiple times at same time? All items will be removed, even when just 1 fails.
// items: []
dispatch(addItem({ id: '1' }))
// items: [{ id: '1' }]
dispatch(addItem({ id: '2' }))
// items: [{ id: '1' }, { id: '2' }]
// Now, first dispatch asynchronously fail, so undo the first action
// items: [], but should be [{ id: '2' }]
Of course, I can make undo myself and manually modify the list of items. But is there any way how to automate undo action for arrays? If adding of some items fail, undo (remove) only this item, not the others.

Related

Infinite scrolling and search with RTK query duplicate results

I'm trying to implement infinite scroll page of posts with search. It works with Reddit JSON API.
There is no pagination in common sense. Reddit api returns listings. Listing JSON responses contain after field which is equivalent to the "next" buttons on the site and in combination with count can be used to page through the listing. So the first request is made without after param and all the following ones contain 'after' with if from the previous server response.
When I change search request, I clean the after state.
This is my rtk api:
export const postsAPI = createApi({
reducerPath: 'posts/api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://www.reddit.com/',
}),
endpoints: build => ({
getPosts: build.query<ITransformedPosts, { limit?: number, searchParam?: string, after?: string }>({
query: ({ limit = 8, after, searchParam }) => ({
url: searchParam ? 'search.json' : '.json',
params: {
limit,
...(after && { after }),
...(searchParam && { q: searchParam })
}
}),
serializeQueryArgs: ({ endpointName, queryArgs }) => { return queryArgs.searchParam || endpointName },
merge: (currentCache, newItems) => {
currentCache.posts.push(...newItems.posts)
currentCache.after = newItems.after
currentCache.hasMore = newItems.hasMore
},
forceRefetch({ currentArg, previousArg }) {
return currentArg !== previousArg
},
transformResponse: (response: IPosts) => ({ posts: response.data.children,
after: response.data.after })
}),
It works fine, except one bug. If I make search request and make the same afterwards, new data are concated to the previous ones. So there are doubled posts.
Are there any chance to say rtk: "Hey rtk, if there are data on the particular request, first return the existing data. And concat new data only when I sent the same request with new 'after' param.

How to refetch/fetch after a series of mutation in graphql/apollo/react

Right now I have a use case to use two useMutations to create/update database. So the second one is depends on the success of the first one. And also the second mutation needs to be called in a loop, just think about that I have a array and I need loop through the array and apply the second mutation.
After all these mutation finished I have to refetch another api to update caches, because the cache would be impacted by the two mutations.
I am really struggling with how to achieve this.
From another post: Apollo Client - refetchQueries after multiple updates
I can do probably like
const [creatEnrollment] = useMutation(mut1)
const [updateEnrollment] = useMutation(mut2)
const [toFetch, {loading, error, data}] = useLazyQuery(UsersDocument)
await Promise.all([creatEnrollment(), updateEnrollment()])
const result = () => toFetch({
variables: {name: 'i'}
})
but the problem is 1. I need to execute second mutations after the first one; 2, I need to have an array that applied to second mutations one by one.
I also saw
here How can I wait for mutation execution in React Query?
we can use onSuccess
const mutate1 = useMutation((data) => axios.post('/something', { data }))
const mutate2 = useMutation(somethingResult) => axios.put('/somethingElse', { somethingResult })
<button onClick={() => {
mutate1.mutate('data', {
onSuccess: mutate2.mutate
})
}} />
But still 1. how to loop thru mutate2.mutate? and how to fetch after mutate2 finished
do like this????:
<button onClick={() => {
mutate1.mutate('data', {
onSuccess: mutate2.mutate
})
mutate2.mutate('data', {
onSuccess: query
})
}} />
Thank you for helping!!
You can have a function for useMutation and onSuccess the data which use get on success use other mutation
const mutationFuntion = (id) => {
// this is first mutation
return useMutation(
(newTitle) => axios
.patch(`/posts/${id}`, { title: newTitle })
.then(response => response.data),
{
// 💡 response of the mutation is passed to onSuccess
onSuccess: (data) => {
// call the api which will get all the latest update
},
}
)
}
Get the Data of first mutation
const [addTodo, { data, loading, error }] = mutationFuntion(//send data);
This is consecutive mutation I found it from this https://react-query-v3.tanstack.com/guides/mutations#consecutive-mutations doc
useMutation(addTodo, {
onSuccess: (data, error, variables, context) => {
// Will be called 3 times
},
})
['Todo 1', 'Todo 2', 'Todo 3'].forEach((todo) => {
mutate(todo, {
onSuccess: (data, error, variables, context) => {
// Will execute only once, for the last mutation (Todo 3),
// regardless which mutation resolves first
},
})
})
For handle the promise of every mutation call
const mutation = useMutation(addTodo)
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
Please you need to verify on what kind of object you want to call mutation in loop it array or some thing alse.

AWS AppSync mutation rerendering the view on mutation

I have test app where I'm subscribing to a list of todos. But my add new todo mutation also optimistically updates the UI.
The problem is it's now refreshing the entire react view and I have no idea why.
This is the culprit, it happens when I run this mutation:
export default graphql(addItem, {
options: {
fetchPolicy: 'cache-and-network'
},
props: props => ({
onAdd: item =>
props.mutate({
variables: item,
optimisticResponse: {
__typename: 'Mutation',
addItem: { ...item, __typename: 'Item' }
},
update: (proxy, { data: { addItem } }) => {
let data = proxy.readQuery({ query: listItems });
data.listItems.items.push(addItem);
proxy.writeQuery({ query: listItems, data });
}
})
})
})(AddItem);
It turns out that mutating items locally without all the queried fields (in my case "created" is added by the server) causes the entire query mutation to fail but silently. There's a fix on the way: https://github.com/apollographql/apollo-client/issues/3267
Hopefully this helps others starting out!

How to enhance the response in apollo client with react only once

I make a GraphQL query where I get back a lot of data and than I calculate the min and max values. As the calculating is quite time consuming I would like to only do it when I receive the value. Unfortunately the props method is called every time the component is rerendered even there was no new call, and the data comes from the store. How can I limit the calculation to the points where I really get new data
graphql(DataQuery, {
options: ({ id }) => ({
variables: {
id,
},
}),
props: ({ data: { result} }) => ({
data: result,
min: getMin(result),
max: getMax(result),
}),
})
This problem is similar to the problem in redux where mapStateToProps() will be called again every time the store updates repeating costy calculations.
You can solve that by using memorized selectors:
import { createSelector } from 'reselect'
const getMinMax = createSelector(
result => result,
result => ({
min: getMin(result),
max: getMax(result),
})
)
graphql(DataQuery, {
options: ({ id }) => ({
variables: {
id,
},
}),
props: ({ data: {result} }) => ({
data: result,
...getMinMax(result), // will only re-calculate if result changed
}),
})
Memorized selectors remember the last result of a call and keep returning that on subsequent calls as long as the input does not change.

Redux mockStore's expected action is only returning first element of list

const middlewares = [ thunk ];
const mockStore = configureStore(middlewares);
const originals = {
Actions: {}
};
const mocks = {
Actions: {
addPasswordResetRequest: spy(() =>{
return [
{type: REQUEST_ADD_PASSWORD_RESET_REQUEST},
{type: REQUEST_ADD_PASSWORD_RESET_REQUEST_SUCCESS}
];
})
}
}
//(have functions to mock actions)
...
it('should create an action to request a password reset', (done) => {
nock('http://localhost:8080/')
.post('/password-reset-requests')
.reply(200);
var email = "test#email.com";
const expectedActions= [
{type: REQUEST_ADD_PASSWORD_RESET_REQUEST},
{type: REQUEST_ADD_PASSWORD_RESET_REQUEST_SUCCESS}
];
const store = mockStore({}, [
{type: REQUEST_ADD_PASSWORD_RESET_REQUEST},
{type: REQUEST_ADD_PASSWORD_RESET_REQUEST_SUCCESS}
], done);
store.dispatch(Actions.addPasswordResetRequest(email));
unMockActions();
});
Response I get:
Error: Expected [ { type: 'REQUEST_ADD_PASSWORD_RESET_REQUEST' },
{type: 'REQUEST_ADD_PASSWORD_RESET_REQUEST_SUCCESS' } ] to equal
{type: 'REQUEST_ADD_PASSWORD_RESET_REQUEST' }
When I run this code, I only get the first action returned, not the second one. And it's not a list, it's only one action. I need to test the sequence of the actions, not whether one action is called.
I experienced this when trying to test a reducer that had more than one action type in it. With Mocha you can only test 1 action at a time. Ive not tested any 'thunked' action creators yet, but i would expect that the mock action creator would test for the second action and that should be captured in a test for that action creator.

Resources