RTK Query failing to fetch new data after mutation - reactjs

I'm trying to use RTK query to fetch data from Firestore using the queryFn. The problem I'm having is that I'm developing a multiwizard, where the fields in the form are initialised with data straight from RTK Query.
Update an input field with a mutation in step 1
Go to step 2
Go back to step 1 (fetch is rejected with a ConditionError, please see attached log)
export const userApi = createApi({
reducerPath: 'userApi',
baseQuery: fakeBaseQuery(),
tagTypes: ['User'],
endpoints: builder => ({
getUser: builder.query<Customer, string>({
async queryFn(userId) {
try {
const docSnapshot = await getDoc(doc(db, 'users', userId));
const customer = docSnapshot.data() as Customer;
return { data: customer };
} catch (err) {
return { error: err };
}
},
providesTags: (result, error, arg) => [{type: 'User', id: result.id }]
}),
updateUser: builder.mutation<{ success: boolean }, { userId: string; user: Partial<UserInfo> }>({
async queryFn(payload) {
const { userId, user } = payload;
try {
// Note: Id is same for both. Have also tried replacing with 'LIST'
const res = await updateDoc(doc(db, 'users', userId), { ...user });
return { data: { success: true } };
} catch (err) {
return { error: err ? err : null };
}
},
invalidatesTags: (result, error, { userId }) => [{ type: 'User', id: userId}],
})
})
});
export const { useGetUserQuery, useUpdateUserMutation } = userApi;
I've also tried just using User for the providesTag/invalidatesTags.
I'm trying to figure out why the 2nd query, after going back to step 1 of the form fails to retrieve the data I've just updated.

I fixed this issue by updating rtk from 1.8.5 to 1.9.0
Seems like there was a bug
invalidateTags works correctly when dealing with persisted query
state.
https://github.com/reduxjs/redux-toolkit/releases/tag/v1.9.0

Related

RTK Query optimistic update returning empty Proxy object

I have two api slices, one is userApiSlice and one is teamsApiSlice. In userApiSlice I'm trying to change a user's team and using optimistic update for better ux in case of slow network or anything. here is my userApiSlice where I'm trying to make optimistic update:
changeUsersTeam: builder.mutation({
query: (data) => ({
url: `/user/change-team/${data.id}`,
method: "put",
body: data.body,
}),
invalidatesTags: ["Team"],
async onQueryStarted(data, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
teamsApiSlice.util.updateQueryData(
// #ts-ignore
"getTeamById",
data.id.toString(),
(draft) => {
console.log("test from api slice", draft);
if (data.body.team !== "substitudes") {
draft.subMembers = [];
draft.activeMembers.push(draft.studentId);
} else {
draft.activeMembers = [];
draft.subMembers.push(draft.studentId);
}
}
)
);
try {
await queryFulfilled;
} catch {
patchResult.undo();
}
},
}),
but in console log I'm getting this empty proxy object:
Proxy: { <target>:null,<handler>:null }
so the optimistic update is not working

RTK Query not Invalidating Data When App is Hosted, but works on Localhost

I am working on this app that is so data intensive. I have implemented RTK Query and my issue is with the invalidation of tags after mutation changes via API call. It works well on localhost where all the tags are invalidated as needed, but when I host the app, no invalidation happens even after an API call is successful and data has been changed on the server. Hard-refreshing the app doesn't help, until I have to clear the browser cache for the changes to reflect on the UI. I also notice that the network API calls are being fired, but updating the stale data on the cache does not take place. I will add here all the necessary code that may help to debug this issue.
store.js
import { configureStore } from "#reduxjs/toolkit";
import { apiSlice } from '../api/apiSlice';
import authReducer from "./auth/authSlice";
export const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer,
auth: authReducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(apiSlice.middleware)
});
apiSlice.js
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react';
import { Mutex } from 'async-mutex';
import { logout, setCredentials } from '../features/auth/authSlice';
import { baseUrlDev, baseUrlPro } from './baseUrl';
const baseURL =
window.location.hostname.includes("dev") || window.location.hostname.includes("localhost")
? baseUrlDev
: baseUrlPro;
const mutex = new Mutex();
const baseQuery = fetchBaseQuery({
baseUrl: baseURL,
credentials: 'include',
timeout: 15000,
prepareHeaders: (headers, {getState}) => {
const token = getState().auth.token || JSON.parse(localStorage.getItem("authenticatedUser"))?.accessToken;
if (token) {
headers.set("Authorization", `Bearer ${token}`)
}
return headers;
}
});
const baseQueryWithReauth = async (args, api, extraOptions) => {
await mutex.waitForUnlock();
let result = await baseQuery(args, api, extraOptions)
if (result?.error?.originalStatus === 403) {
if (!mutex.isLocked()) {
const release = await mutex.acquire();
try {
console.log('sending refresh token');
// send refresh token to get a new access token
const refreshResult = await baseQuery('/auth/refresh', api, extraOptions);
// console.log(refreshResult);
if(refreshResult?.data) {
const email = api.getState().auth.email || JSON.parse(localStorage.getItem("authenticatedUser"))?.email;
const role = api.getState().auth.role || JSON.parse(localStorage.getItem("authenticatedUser"))?.role;
const name = api.getState().auth.name || JSON.parse(localStorage.getItem("authenticatedUser"))?.name;
// store the new token
api.dispatch(setCredentials({
accessToken: refreshResult.data.accessToken,
email,
role,
name,
branch: refreshResult.data.branch,
company: refreshResult.data.company
}));
// retry the original query with new access token
result = await baseQuery(args, api, extraOptions);
} else {
await baseQuery('/auth/logout', api, extraOptions);
api.dispatch(logout());
}
} finally {
release();
}
} else {
await mutex.waitForUnlock();
result = await baseQuery(args, api, extraOptions);
}
}
return result;
}
export const apiSlice = createApi({
baseQuery: baseQueryWithReauth,
tagTypes: [
'Branch', 'Company', 'Customer', 'Driver', 'Parcel', 'ParcelTransaction',
'ParcelType', 'Staff', 'Town', 'TransactionChannel', 'User', 'VehicleOwner',
'Vehicle', 'VehicleType'
],
refetchOnMountOrArgChange: 5,
refetchOnFocus: true,
endpoints: builder => ({})
})
I also set the refetchOnFocus to be true on the baseQuery but it doesn't work at all. I was thinking this would help, but the cache is persistent, even if the system remains dominant for more than 30 mins. I mean it should even refetch data on the minimum, but it continues to use the stale cache data.
parcelSlice.js
import { createSelector, createEntityAdapter } from "#reduxjs/toolkit";
import { apiSlice } from '../../api/apiSlice';
const SLICE_URL = '/parcels';
const parcelsAdapter = createEntityAdapter({
sortComparer: (a, b) => b.parcelCode.localeCompare(a.parcelCode)
})
const initialState = parcelsAdapter.getInitialState()
export const parcelApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getParcels: builder.query({
query: () => `${SLICE_URL}`,
transformResponse: responseData => {
return parcelsAdapter.setAll(initialState, responseData)
},
providesTags: (result, error, id) => ['Parcel', 'ParcelTransaction']
}),
getParcel: builder.query({
query: (id) => `${SLICE_URL}/${id}`,
providesTags: (result, error, id) => ['Parcel', 'ParcelTransaction'],
}),
getParcelsAvailableForDispatch: builder.query({
query: () => `${SLICE_URL}/available-for-dispatch`,
providesTags: (result, error, id) => ['Parcel', 'ParcelTransaction'],
}),
getParcelsAssignedToVehicle: builder.query({
query: (vehicleID) => `${SLICE_URL}/assigned-to-vehicle/?vehicleID=${vehicleID}`,
providesTags: (result, error, id) => ['Parcel', 'ParcelTransaction'],
}),
getParcelsAwaitingRecipients: builder.query({
query: () => `${SLICE_URL}/parcels-awaiting-recipients`,
providesTags: (result, error, id) => ['Parcel', 'ParcelTransaction'],
}),
addParcel: builder.mutation({
query: parcelData => ({
url: `${SLICE_URL}`,
method: 'POST',
body: {
...parcelData
}
}),
invalidatesTags: ['Parcel', 'ParcelTransaction']
}),
issueParcel: builder.mutation({
query: parcelID => ({
url: `${SLICE_URL}/issue-parcel/${parcelID}`,
method: 'PATCH',
body: {
id: parcelID
}
}),
invalidatesTags: ['Parcel', 'ParcelTransaction']
}),
updateParcel: builder.mutation({
query: ({id, parcelData}) => ({
url: `${SLICE_URL}/${id}`,
method: 'PATCH',
body: {
...parcelData
}
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Parcel', id: arg.id }
]
}),
})
});
export const {
useGetParcelsQuery,
useGetParcelQuery,
useGetParcelsAvailableForDispatchQuery,
useGetParcelsAssignedToVehicleQuery,
useGetParcelsAwaitingRecipientsQuery,
useAddParcelMutation,
useIssueParcelMutation,
useUpdateParcelMutation,
} = parcelApiSlice;
// returns the query result object
export const selectParcelsResult = parcelApiSlice.endpoints.getParcels.select();
// Creates memoized selector
const selectParcelsData = createSelector(
selectParcelsResult,
parcelsResult => parcelsResult.data // normalized state object with ids & entities
);
//getSelectors creates these selectors and we rename them with aliases using destructuring
export const {
selectAll: selectAllParcels,
selectById: selectParcelById,
selectIds: selectParcelIds
// Pass in a selector that returns the parcels slice of state
} = parcelsAdapter.getSelectors(state => selectParcelsData(state) ?? initialState)
An example use case is when I want to issue a parcel to a customer using the issueParcel mutation on the parcelSlice, the thing is, the current parcel status should change the status to delivered and update the UI by refetching data once the API mutation call has been made and the mutation is successful. However, this only happens in locahost, but does not happen when I host the app in the server. This is part of the code that I am using on the parcel's details component.
parcelDetails.jsx
const ParcelDetails = () => {
const { id } = useParams();
const navigate = useNavigate();
const theme = useTheme();
const { data: parcelDetails, isLoading, isError, error, refetch } = useGetParcelQuery(id);
const [issueParcel] = useIssueParcelMutation()
const authenticatedUser = JSON.parse(localStorage.getItem("authenticatedUser"));
const breadcrumbs = [
{ name: "Parcel", path: "/parcel" },
{ name: parcelDetails?.parcelCode }
];
const staffName = parcelDetails?.staff?.ownuser?.firstName + " " + parcelDetails?.staff?.ownuser?.lastName;
const handleIssueParcel = (parcelID) => {
Swal.fire({
title: 'Are you sure you want to issue this parcel?',
html: '<p>ParcelCode: ' + parcelDetails.parcelCode + '</p> <br />',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, issue Parcel!'
}).then(async (result) => {
if (result.isConfirmed) {
try {
Swal.fire({
title: "Issuing Parcel",
html: "Please wait..."
})
Swal.showLoading()
await issueParcel(parcelID);
refetch();
Swal.hideLoading()
Swal.fire(`parcel ${parcelDetails.parcelCode} issued successfully!`, '', 'success');
} catch (error) {
console.log(error);
}
} else if (result.isDenied) {
Swal.fire('Parcel not issued.', '', 'info');
}
})
}
return (<>Parcel Display UI</>)
}
export default ParcelDetails
I even tried to force refetch() of the data after every API call is successful, but this does not work when the app is hosted on the server.
I believe it is something small that I am missing out. I will appreciate your review and advice on the same.
I finally solved this by adding this to the baseQuery. Credits to this question that was facing a similar problem as mine.
const baseQuery = fetchBaseQuery({
baseUrl: baseURL,
credentials: 'include',
timeout: 15000,
prepareHeaders: (headers, {getState}) => {
headers.set('Accept', 'application/json');
headers.set('Cache-Control', 'no-cache');
headers.set('Pragma', 'no-cache');
headers.set('Expires', '0');
const token = getState().auth.token || JSON.parse(localStorage.getItem("authenticatedUser"))?.accessToken;
if (token) {
headers.set("Authorization", `Bearer ${token}`)
}
return headers;
}
});

Unable to delete or add data in firestore using RTK-Query in react

I am trying to achieve delete and add functionality in react using RTK-Query with firestore. It sound might be weird that I am using RTK Query to perform firestore operation in React. So, I have written service API file to delete and add operation with firestore. So, whenever I try to delete or add data in firestore with RTK Query, so I am getting some weird error in my console. However, when I refresh the application then I am seeing the updated data on my app after performing the add/delete operation. For some reason, initially it's not providing me correct result due to below error but after page refresh I am getting the updated value from firestore in my react app.
Here is code for service API
import { createApi, fakeBaseQuery } from "#reduxjs/toolkit/query/react";
import {
addDoc,
collection,
deleteDoc,
doc,
getDocs,
onSnapshot,
serverTimestamp,
} from "firebase/firestore";
import { db } from "../firebase";
export const contactsApi = createApi({
reducerPath: "api",
baseQuery: fakeBaseQuery(),
tagTypes: ["Contact"],
endpoints: (builder) => ({
contacts: builder.query({
async queryFn() {
try {
const userRef = collection(db, "users");
const querySnapshot = await getDocs(userRef);
let usersData = [];
querySnapshot?.forEach((doc) => {
usersData.push({
id: doc.id,
...doc.data(),
});
});
return { data: usersData };
} catch (err) {
console.log("err", err);
return { error: err };
}
},
providesTags: ["Contact"],
}),
addContact: builder.mutation({
async queryFn(contact) {
try {
await addDoc(collection(db, "users"), {
...contact,
timestamp: serverTimestamp(),
});
} catch (err) {
return { error: err ? err : null };
}
},
invalidatesTags: ["Contact"],
}),
deleteContact: builder.mutation({
async queryFn(id) {
try {
await deleteDoc(doc(db, "users", id));
} catch (err) {
if (err) {
return { error: err };
}
}
},
invalidatesTags: ["Contact"],
}),
}),
});
export const {
useContactsQuery,
useAddContactMutation,
useDeleteContactMutation,
} = contactsApi;
store.js file
import { configureStore } from "#reduxjs/toolkit";
import { contactsApi } from "./services/contactsApi";
import { setupListeners } from "#reduxjs/toolkit/query";
export const store = configureStore({
reducer: {
[contactsApi.reducerPath]: contactsApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}).concat(contactsApi.middleware),
});
setupListeners(store.dispatch);
A queryFn has to always return an object with either a data property or an error property - in your case you only do that in the error case.
Try adding a return { data: 'ok' } if you don't have any better idea.

Call function after refetchQueries

I am receiving data from an api call, taking that data and restructuring it to properly display in a table. When a user clicks a button I am trying to create a copy of that record. I've got it all working, its just not updating the table with the appended, or removed (for delete) data. until after i refresh the page through the browser.
Is it possible to call a function after refetchQueries?
const {
loading: appLoading,
data: applicationsData,
refetch: refetchApplicationsData,
} = useQuery(applications.operations.GET_APPLICATIONS_BY_COMPANY, {
client: applications.client,
variables: { companyId: userDetails.companyId },
})
const [
CloneApplication,
{ loading: cloneLoading, data: cloneData, error: cloneError },
] = useMutation(applications.operations.CLONE_APPLICATION_BY_COMPANY, {
client: applications.client,
onCompleted: () => {
refetchApplicationsData
},
})
useEffect(() => {
if (applicationsData && templatesList) {
const newFinalData = getFinalData({
applicationsList: applicationsData.getApplicationsByCompany,
templatesList: templatesList,
})
console.log('oldFinalData: ', finalData)
console.log('newFinalData: ', newFinalData)
setFinalData(newFinalData)
console.log('updatedFinalData: ', finalData)
}
}, [applicationsData, templatesList])
const cloneAndRefresh = (applicationId, companyId, ucId) => {
CloneApplication({
variables: {
applicationId: applicationId,
companyId: companyId,
ucId: ucId,
},
}).then(({ data: responseData }) => {
if (responseData) {
console.log('response data: ', responseData)
console.log('applications: ', applicationsData)
}
})
}
the function to restructure data:
export function getFinalData(request: {
templatesList: GetAllTemplate[]
applicationsList: GetApplicationsByCompany[]
}): FinalDataResponse[] {
const templates = request.templatesList.map((template) => {
const applicationsForTemplate = request.applicationsList.filter(
(app) => app.templateId === template.templateId
)
return { ...template, applications: applicationsForTemplate }
})
const groupedData = _.chain(templates)
.groupBy('templateId')
.map((value, key) => {
const templateName = _.chain(value)
.groupBy('templateName')
.map((value, key) => key)
.value()
const createdDate = _.chain(value)
.groupBy('dateCreated')
.map((value, key) => dayjs(key).format('ll'))
.value()
const lastModified = _.chain(value)
.groupBy('lastModified')
.map((value, key) => dayjs(key).format('ll'))
.value()
return {
templateId: key,
templateName: templateName[0],
createdDate: createdDate[0],
lastModified: lastModified[0],
applications: value[0].applications,
}
})
.value()
const finalData = groupedData.map((object, index) => {
return {
...object,
totalApplications: object.applications.length,
}
})
console.log('returning final data: ', finalData)
return finalData
}
I guess im trying to rerun getFinalData after the refetchquery then save it to state and it should re-render the table?
EDIT: I've updated my queries with new code, though it didnt quite work. If its possible to get the data from the refetched query I think i could make it work. I assume that refetching the query would update applicationsData as a result but i dont think it did?
By default, the useQuery hook checks the Apollo Client cache to see if all the data you requested is already available locally. If all data is available locally, useQuery returns that data and doesn't query your GraphQL server. This cache-first policy is Apollo Client's default fetch policy. If you say that you will call handleRefresh() after mutation the below code will work fine.
here read fetch policy
const {
loading: appLoading,
data: applicationsData,
refetch: refetchApplicationsData,
} = useQuery(applications.operations.GET_APPLICATIONS_BY_COMPANY, {
client: applications.client,
variables: { companyId: userDetails.companyId },
fetchPolicy: "network-only",
})

the first request doesn't send to the database in working with RTK query

In my project I'd like to increase and decrease amount of the foods listed . but after push the "More" button in the first request doesn't change anything in database but after push it again in second request just increse it once (after this refresh the page you'll figure it out). actually I don't know why the first request doesn't counted .
Demo :https://laughing-jang-201dc1.netlify.app/ .
Github : https://github.com/alibidjandy/burger
this is the main RTK Query configuration :
https://github.com/alibidjandy/burger/blob/main/src/Api/apiSlice.ts
import { current } from "#reduxjs/toolkit";
import { createApi, fetchBaseQuery } from "#reduxjs/toolkit/query/react";
import {
AllBurgerType,
BurgerType,
IngredientsType,
} from "../components/Layouts/burger/burgerSlice/burgerSlice";
export const burgerApi = createApi({
reducerPath: "burgerApi",
tagTypes: ["Ingredients"],
baseQuery: fetchBaseQuery({
baseUrl:
"https://burger-order-brown-default-rtdb.europe-west1.firebasedatabase.app",
}),
endpoints: (builder) => ({
getIngredients: builder.query<BurgerType, undefined>({
// query: () => `/.json`,
query: () => {
return { url: "/.json", method: "GET" };
},
providesTags: ["Ingredients"],
}),
editIngredients: builder.mutation({
query: (initialIngredients) => {
return { url: "/.json", method: "PATCH", body: initialIngredients };
},
invalidatesTags: ["Ingredients"],
}),
increase: builder.mutation({
query: ({ ing }) => {
return {
url: `/ingredients/${ing.id}/.json`,
method: "PATCH",
body: ing,
};
},
async onQueryStarted({ ing }, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
burgerApi.util.updateQueryData(
"getIngredients",
undefined,
(draft) => {
console.log("increase");
// debugger;
const ingredient = draft.ingredients.find(
(ingredient) => ingredient.title === ing.title
);
if (ingredient) {
console.log(current(ingredient));
ingredient.Qty!++;
}
}
)
);
try {
await queryFulfilled;
} catch {
console.log("crashed");
patchResult.undo();
}
},
}),
decrease: builder.mutation({
query: ({ ing, indexIng }) => {
return {
url: `/ingredients/${indexIng}/.json`,
method: "POST",
body: ing,
};
},
async onQueryStarted({ ing, indexIng }, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
burgerApi.util.updateQueryData(
"getIngredients",
undefined,
(draft) => {
console.log("decrese");
const ingredient = draft.ingredients[indexIng];
if (ingredient.Qty !== undefined && ingredient.Qty > 0) {
draft.totalCost = +(draft.totalCost -=
ingredient.cost!).toFixed(2);
ingredient.Qty--;
}
}
)
);
try {
await queryFulfilled;
} catch {
patchResult.undo();
}
},
}),
}),
});
export const { editIngredients, getIngredients } = burgerApi.endpoints;
export const {
useGetIngredientsQuery,
useEditIngredientsMutation,
useIncreaseMutation,
useDecreaseMutation,
} = burgerApi;
Wait... I'm just re-reading your code. I think you might have a very wrong idea on what onQueryStarted and updateQueryData do. onQueryStarted runs after your request has been sent to the server. updateQueryData only updates your data on the client.
So you send the old client-side data to the server and then update it on the client side.
Yes, no wonder your data is always one step behind.
You need to send updated data to the server in the first place.
You would need to do something like
increase: builder.mutation({
query: ({ ing }) => {
return {
url: `/ingredients/${ing.id}/.json`,
method: "PATCH",
body: { ...ing, Qty: ing.Qty + 1 }
};
},
and no onQueryStarted at all.
Just use invalidatesTags to have the correct data refetched from the server afterwards.
Also, as I stumbled over that somewhere else in your code: you are generating an id using nanoid() and using that as key. Never do that. A key has to be stable, not be a different one on every render. That will always cause React to destroy the whole child DOM on every render and throw away the whole local state of all child components as well.

Resources