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;
}
});
I have a RTK query that's not refreshing it's core content after a delete mutation. Could anyone explain why ? It's not clear to me where the problem lies as there is not refresh request made at any point.
The code looks fine and it's pretty much the same I use in another API that's working. And on this second API I pass the same data (a list of items) and it's refreshing fine after a delete ; here's the code:
:
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react';
import { Auth, API, Storage } from 'aws-amplify';
// Define a service using a base URL and expected endpoints
export const researchApi = createApi({
reducerPath: 'researchApi',
tagTypes: ['Research'],
baseQuery: fetchBaseQuery({
baseUrl: process.env.NEXT_PUBLIC_API_RESEARCH,
prepareHeaders: async (headers, { getState }) => {
const token = (await Auth.currentSession()).getIdToken().getJwtToken();
headers.set('Authorization', `${token}`);
headers.set('Content-Type', 'application/json');
return headers;
}
}),
endpoints: (builder) => ({
getResearch: builder.query({
query: () => `research`,
providesTags: ['Research']
}),
getResults: builder.query({
query: (id) => `results?searchid=${id}`,
}),
addResearch: builder.mutation({
query(keywords) {
const data = {
keywords: keywords
}
return {
url: `research`,
method: 'POST',
body: data
}
},
invalidatesTags: ['Research']
}),
deleteResults: builder.mutation({
query(results) {
// send array
let sanitized;
sanitized = results.filter(item => item);
const data = {
items: sanitized
}
//console.log('data: ', data);
return {
url: `removeresult`,
method: 'DELETE',
body: data
}
},
invalidatesTags: ['Research']
}),
}),
})
// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetResearchQuery, useGetResultsQuery, useAddResearchMutation, useDeleteResultsMutation } = researchApi
I'm calling the query like this :
const router = useRouter()
const { kwd } = router.query
const { data, error, isError, isLoading } = useGetResultsQuery(kwd);
if(isLoading) {
return (
<>
<Spinner animation="border" size="sm" role="status" />{' '} Please wait while Loading...
</>
)
}
Any idea would be very helpful as I'm completely lost with this...
Ok so problem solved, I didn't add the correct parameters :
getResearch: builder.query({
query: () => research,
providesTags: ['Research']
}),
getResults: builder.query({
query: (id) => `results?searchid=${id}`,
providesTags: ['Research'] // ========> THAT WAS MISSING
}),
Try and also make sure you are returning the correct data from the mutation. Thanks.
Return the correct fields from the mutation. If the required the field, can be id is not returned from the mutation, then there will be no refresh.
I simply want to use RTK to run an API request as part of an onSuccess function and await the response. I was doing this in Axios no problem but I'm trying to replace my Axios calls with RTK and having trouble using the LazyQuery.
I'm getting an exception
useLazyQuery is not a function or its return value is not iterable
My API:
const linkAPI = overweightRTK.injectEndpoints({
tagTypes: ['Link'],
endpoints: (builder) => ({
createLink: builder.mutation({
query: (body) => ({
url: `/links/`,
method: 'POST',
body,
invalidatesTags: ['Link']
})
})
}),
overrideExisting: false
});
export const { useCreateLinkMutation} = linkAPI;
My component:
const [trigger, result, lastPromiseInfo] = OverweightRTK.endpoints.createLink.useLazyQuery()
const onSuccess = async (data) => {
const createLinkRequest = {
payload: data
};
trigger({ createLinkRequest }, { skip: !isSubscriber })
.unwrap()
.then(res => {
if (res.status === 200) {
setSuccessMessage('Success, your account linked');
}
})
.catch((error) => console.log(error));
};
I figured it out. Apparently for mutation endpoints you don't need to use the lazyQuery(). Mutation queries by default return a similar response as lazyQuery and already return the tuple containing the trigger.
Working code: (This replaces the first line in my component code, nothing else needed to be changed)
const [trigger, result, lastPromiseInfo] = useCreateLinkMutation();
Useful docs on the subject: https://redux-toolkit.js.org/rtk-query/usage/mutations
I'm new in RTK Query and I'm struggling with a use case I have to implement.
Scenario:
I have to merge the results from two API calls: the first API call is a private API call while the second one is a public API call. I need to merge the responses from these two APIs and write the computed result into the RTK cache so the UI can update accordingly.
Problem:
I'seeing that as soon as the await queryFullfilled is invoked, RTK Query immediately write into its cache the response from that API call and then when I make my calculation and try to update the RTK cache with apiSlice.util.updateQueryData the cache will change again. That's means that the UI will render twice, the first time using a wrong value (an array of persons) and the second time with the correct value (the JSON composed by ids and entities).
Question:
Is there a way to have just 1 write into the RTK cache so I can have just the computed value I need ? Because what is happening is that for some instances I'm having into the cache an array while I need the {ids: [...], entities: {}} JSON.
import { createEntityAdapter } from '#reduxjs/toolkit';
import axios from 'axios';
export const personsAdapter = createEntityAdapter();
const permitsInitialState = personsAdapter.getInitialState();
export const apiSlice = myServiceApi.injectEndpoints({
endpoints: (builder) => ({
getPersons: builder.query({
query: () => ({ url: '/persons', method: 'get' }),
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
try {
// Resolving the private API call
const { data: persons } = await queryFulfilled;
// Just a random public API call
const { data: todos } = await axios('https://jsonplaceholder.typicode.com/todos');
const enhancedPersons = /** Here the logic that merge the todos and the persons */
const state = personsAdapter.setAll(permitsInitialState, enhancedPermits);
dispatch(
apiSlice.util.updateQueryData('getPersons', _, (draft) => {
Object.assign(draft, state);
})
);
} catch (e) {
console.error(e);
}
},
}),
}),
});
That is one of the use cases of queryFn: Performing multiple requests with a single query
import {
createApi,
fetchBaseQuery,
FetchBaseQueryError,
} from '#reduxjs/toolkit/query'
import { Post, User } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/ ' }),
endpoints: (build) => ({
getRandomUserPosts: build.query<Post, void>({
async queryFn(_arg, _queryApi, _extraOptions, fetchWithBQ) {
// get a random user
const randomResult = await fetchWithBQ('users/random')
if (randomResult.error) throw randomResult.error
const user = randomResult.data as User
const result = await fetchWithBQ(`user/${user.id}/posts`)
return result.data
? { data: result.data as Post }
: { error: result.error as FetchBaseQueryError }
},
}),
}),
})
I'm using RTK query and I want to overwrite the state with the result from my transform request. I get my overview of a todos array by calling the getTodosOverview Query. After that I call a updateTodos query and this gives me back a new array with todos. I want to overwrite the original array with these results. I'm using the function updateQueryData but it doesnt seem to work. What am I missing here?
export const todosApi = createApi({
reducerPath: 'todosApi',
baseQuery: fetchBaseQuery({ baseUrl: 'api/todos' }),
endpoints: (builder) => ({
getToDosOverview: builder.query<Array<ToDos>, string>({
query: () => `getOverview`,
transformResponse: (rawResult: { data: Array<ToDos> }) => rawResult.data,
keepUnusedDataFor: 0,
}),
updateTodos: builder.mutation<Array<ToDos>, string>({
query: (fileId) => ({
url: `updateTodos?fileId=${fileId}`,
method: 'POST',
}),
transformResponse: (rawResult: { data: Array<ToDos> }) => rawResult.data,
async onQueryStarted(uniqueIdentifier, { dispatch, queryFulfilled }) {
const { data }= await queryFulfilled;
// Update state with new data from response
const patchResult = dispatch(
todosApi.util.updateQueryData(
'getToDosOverview',
uniqueIdentifier,
() => {
return data;
}
)
);
},
}),
}),
});
Well, you're not returning anything new.
const patchResult = dispatch(
todosApi.util.updateQueryData(
'getToDosOverview',
uniqueIdentifier,
// here you are getting the old state as a variable called `ToDos`
(ToDos: Array<ToDos>) => {
// and here you return that old state without any change
return ToDos;
}
)
);
Also, you are doing that way before you have even received a response.
The response will be available after the line
await queryFulfilled;
so you probably want to do something like
const { data } = await queryFulfilled
and then use data as the new value.
Generally, it seems like you are copy-pasting from the "optimistic updates" example. Please look at the example of pessimistic updates instead.
Also, with your code you are using the wrong argument if you want to update useGetToDosOverviewQuery().
You are updating useGetToDosOverviewQuery(uniqueIdentifier) here.
You should probably call
todosApi.util.updateQueryData('getToDosOverview', undefined, ...)