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.
Related
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 am getting error while making request to three endpoints using redux toolkit. I have a lot of others endpoints too they are working fine but in production its not working.
import { createApi, fetchBaseQuery } from "#reduxjs/toolkit/query/react";
const tmdbApiKey = process.env.REACT_APP_TMDB_KEY;
const config = {
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "text/plain",
},
};
export const tmdbApi = createApi({
reducerPath: "tmdbApi ",
baseQuery: fetchBaseQuery({
baseUrl: "https://api.themoviedb.org/3",
config,
}),
endpoints: (builder) => ({
getGenres: builder.query({
query: () => `/genre/movie/list?api_key=${tmdbApiKey}`,
}),
getMovies: builder.query({
query: ({ categorieName, page, searchQuery }) => {
// ! Get Search Result
if (searchQuery) {
return `/search/movie?query=${searchQuery}&page=${page}&api_key=${tmdbApiKey}`;
}
// ! get Movies by Category
if (categorieName && typeof categorieName === "string") {
return `movie/${categorieName}/?page=${page}&api_key=${tmdbApiKey}`;
}
// ! get Movies by Id
if (categorieName && typeof categorieName === "number") {
return `discover/movie?with_genres=${categorieName}&page=${page}&api_key=${tmdbApiKey}`;
}
// ! get Popular Movies
return `/movie/popular?page=${page}&api_key=${tmdbApiKey}`;
},
}),
getMovie: builder.query({
query: (id) =>
`/movie/${id}?append_to_response=videos,credits&api_key=${tmdbApiKey}`,
}),
// ! Get User Specific Lists
getList: builder.query({
query: ({ listName, accountId, sessionId, page }) =>
`/account/${accountId}/${listName}?api_key=${tmdbApiKey}&session_id=${sessionId}&page=${page}`,
}),
// ! get User Specific List
getRecommendations: builder.query({
query: ({ id, list }) => `/movie/${id}/${list}?api_key=${tmdbApiKey}`,
}),
// !
getActor: builder.query({
query: (id) => `/person/${id}?api_key=${tmdbApiKey}`,
}),
getMoviesByActor: builder.query({
query: ({ id, page }) =>
`/discover/movie?with_cast=${id}&page=${page}&api_key=${tmdbApiKey}`,
}),
}),
});
export const {
useGetMoviesQuery,
useGetGenresQuery,
useGetMovieQuery,
useGetRecommendationsQuery,
useGetActorQuery,
useGetMoviesByActorQuery,
useGetListQuery,
} = tmdbApi;
Thats my tmdbapi endpoints. and getting error when make request to Top Rated, Upcoming and Popular in production. The error is ** Mixed Content: The page at 'https://demoviedbapp.netlify.app/' was loaded over HTTPS, but requested an insecure resource 'http://d2nsx85y22o8i8.cloudfront.net/3/movie/popular?page=1&api_key=76265cb27f37556383131d31275d50cc'. This request has been blocked; the content must be served over HTTPS. **
I have 2 completely independent components without any parent-child relationship being displayed on a single page.
Component 1 : Makes an API call fetches some records and display it in a table having server side-pagination
Component 2 : Contains a form, when the user submits the form the data in the component 1 needs to be refetch-ed through the backend.
Since I am using fetchBaseQuery to query the data, I believe I need to invalidate the cache in order to make the API call in the component 1.
I tried refetch() to fulfil that requirement but got no luck. I also tried setting the cache timeout using keepUnusedDataFor that too didn't work. Also, tried to do something with the tags, but for that I will have to use mutation instead of query and I am not sure how mutation is useful as per my use case
Here's some of the code :
component1.tsx
let { data, error, isSuccess, isError, isFetching, refetch } = useGetQuery(request, { skip});
const records = data?.records;
React.useEffect(() => {
if (records) {
// set records within table
}
}, [records]);
useGetQuery.ts
const extendedApi = mainApi.injectEndpoints({
endpoints: (builder) => ({
getQuery: builder.query<response, request>({
query: (request?: request) => ({
url: "someURL",
body: request,
method: "POST",
}),
providesTags: ["Requests"],
}),
}),
overrideExisting: true,
});
export const { useGetQuery } = extendedApi;
component2.tsx
let [trigger, data] = useSubmitFormMutation();
const submitForm = (e) => {
e.preventDefault();
trigger(// Some Object);
}
React.useEffect(() => {
if (isSuccess) {
updateRefreshRecords(true); // setting the hook to true to make an API call in component 1
}
}, [isSuccess]);
useSubmitFormMutation.ts
const extendedApi = mainApi.injectEndpoints({
endpoints: (builder) => ({
submitForm: builder.mutation<response, request>({
query: (request?: request) => ({
url: "some_other_url",
body: request,
method: "POST",
}),
invalidatesTags: ["Requests"],
}),
}),
overrideExisting: false,
});
export const { useSubmitFormMutation } = extendedApi;
mainAPI.ts
export const dynamicBaseQuery: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = async (args, api, extraOptions) => {
const { mainApiUrl } = (api.getState() as RootState).settings.endpoints;
const rawBaseQuery = fetchBaseQuery({
baseUrl: mainApiUrl,
prepareHeaders: (headers, { getState }) => {
// Use getState to pull the jwtToken and pass it in the headers to the api endpoint.
const { jwtToken } = (getState() as RootState).auth;
headers.set("authorization", jwtToken);
return headers;
},
});
return rawBaseQuery(args, api, extraOptions);
};
export const mainApi = createApi({
reducerPath: "mainApi",
baseQuery: dynamicBaseQuery,
endpoints: () => ({}),
tagTypes: ["Requests"],
});
store.ts
export const store = configureStore({
reducer: {
// other reducers
[localApi.reducerPath]: localApi.reducer,
[mainApi.reducerPath]: mainApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
})
.concat(localApi.middleware)
.concat(mainApi.middleware),
});
Can you please help me how can I invalidate the cache as per my use case.
Any help would be highly appreciated
Thanks
You can just add invalidatesTags to your mutation and that should refresh the query:
const extendedApi = mainApi.injectEndpoints({
endpoints: (builder) => ({
submitForm: builder.mutation<response, request>({
query: (request?: request) => ({
url: "some_other_url",
body: request,
method: "POST",
}),
invalidatesTags: ["Requests"]
}),
}),
overrideExisting: false,
});
No need for manual refetching or keepUnusedDataFor.
If that doesn't work, double-check that you added the api's middleware to the middlewares in your configureStore
Simply change your submitForm endpoint to mutation type and invalidate "Requests" tag on this endpoint. This way you don't have to use updateRefreshRecords.
You can then remove below useEffect in Component1.tsx
React.useEffect(() => {
if (refreshRecords) {
refetch();
}
}, [refreshRecords]);
and also remove keepUnusedDataFor: 5, from getQuery endpoint
I am not sure how mutation is useful as per my use case
When form is submitted, you are either creating or updating some data on backend. So, mutation is the right type of endpoint here. Use query type endpoint when you want to fetch some data from backend.
I am developing mobile app with react-native, redux toolkit, redux-persist and I need an advice for folowing scenerio. The app basically stores todo tasks between group of people, and they can create them, delete them ...
Now I am facing this problem:
If the user is offline, he can`t do anything with the data stored on server in database. And I would like to store that action and just keep it for the moment that he will be online again and then dispatch it - send the request to server.
Here is the example of actions in createApi :
export const taskApi = createApi({
reducerPath: "taskApi",
baseQuery: fetchBaseQuery({
baseUrl: API,
prepareHeaders: (headers, { getState }) => {
if ((getState() as RootState).auth.logged) {
const token = (getState() as RootState).auth.user?.token;
headers.set("token", token ?? "");
}
return headers;
},
}),
endpoints: (builder) => ({
getTasks: builder.query<Task[], any>({ query: () => "" }),
addTask: builder.mutation<any, Task>({
query(body) {
return { url: "", method: "post", body };
},
}),
deleteTask: builder.mutation<any, string>({
query(id) {
console.log(id);
return { url: `?id=${id}`, method: "delete"};
},
}),
getTimeTableData: builder.query<TimeTable[][], void>({
query() {
return { url: "timetable", method: "get" };
},
}),
}),
});
And in extraReducers in createSlice I have :
.addMatcher(taskApi.endpoints.addTask.matchPending, (state, action) => {
console.log("addTask pending");
const newTask = action.meta.arg.originalArgs;
state.tasks = state.tasks ? [...state.tasks, newTask] : [newTask];
})
I add the task to frontend while pending so it`s faster
I would be glad for any answer or hint, thanks very much for help 🧑💻.
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.