Access to localStorage from createApi RTK on first page load - reactjs

I have a case where I need to read my .env file on page load and if there is a specific value then I dispatch that value in my store, where I have createApi => baseUrl needs to access to that value, or localStorage, in order to use it as making an api call, here is an example of my code:
App.tsx
React.useEffect(() => {
const myCustomEndpoint = window._env_.MYENDPOINT;
if(myCustomEndpoint) {
dispatch(setApiEndpoint(myCustomEndpoint));
}
}, [])
src/redux/reducerSlice.ts
export const reducerSlice = createSlice({
//...more slices
setApiEndpoint: (state, action: PayloadAction<string>) => {
state.apiEndpoint = action.payload;
localStorage.removeItem('api');
localStorage.setItem('api', state.apiEndpoint);
}
})
src/services/api.ts
const baseUrl = localStorage.getItem(config.apiEndpoint) || '';
export const dataApis = createApi({
reducerPath: 'dataApis',
baseQuery: fetchBaseQuery({ baseUrl }), // here I cannot get the API for first time the page load
endpoints: (builder) => ({
// my endpoints
})
So is there a way how to access the localStorage of the api endpoint which I have set for the first time my app was loaded?

I think you can set the endpoint directly in the createApi method, so just use like this for example: fetchBaseQuery({ baseUrl: window._env_.MYENDPOINT || '' }).

Related

Can't assign to object array when fetching data with RTK Query

I have come across a problem that I do not understand while trying to integrate redux toolkit and more specifically RTK Query into my project. All I want to do is fetch an object array from my backend using a query hook, pass this data into my component and alter some of the elements inside the data based on user actions.
I am copying the incoming array so as not to mutate the original.
I can replace entire objects in the array with 'blah' but if I try to alter a single value within one of the objects I get:
TypeError: Cannot assign to read only property 'title' of object '#<Object>'
This problem didn't arise when I was using fetch().
I have been stuck on this for days!!!
apiSlice
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react';
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: process.env.devUrl }),
tagTypes: ['categories'],
endpoints: (builder) => ({
getCategories: builder.query({
query: () => 'categories/includingJobs',
transformResponse: (response) => response.data.data,
providesTags: ['categories'],
}),
}),
});
export const { useGetCategoriesQuery} = apiSlice;
Component
import {
useGetCategoriesQuery,
} from '../features/api/apiSlice';
const Test = () => {
const {
data: categories,
isLoading,
isSuccess,
isError,
error,
} = useGetCategoriesQuery();
if (isSuccess) {
let categoriesCopy = JSON.parse(JSON.stringify(categories));
//This is the line that breaks everything. Why?
categoriesCopy[0].title = 'new title';
}
return <h1>Something rendered</h1>;
};
export default Test;

FetchbaseQuery invalidate cache

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.

Redux Toolkit RTK fetchBasedQuery.prepareHeaders not working on the first call

I have this apiSlice:
export const apiSlice = createApi({
baseQuery: fetchBaseQuery({
baseUrl: `${baseUrl}${apiBasePath}`,
prepareHeaders: prepHeaders,
}),
endpoints: (builder) => ({
getPersonById: builder.query({
query: (personId) => ({
url: `person/${personId}`,
}),
}),
}),
});
const prepHeaders = (headers, { getState }) => {
const { token } = getState().auth;
headers.set('X-Request-Id', uuidv4());
headers.set('X-Client-Id', 'xxx');
headers.set('X-Client-Version', '1.0.0');
headers.set('Content-Type', 'application/vnd.api+json');
headers.set('Accept', 'application/vnd.api+json');
headers.set('Authorization', `Bearer ${token}`);
return headers;
};
Which is being used in this component:
const PersonDetails = () => {
const { personId } = useParams();
const { data, error, isLoading, isFetching } =
useGetPersonByIdQuery(personId);
return (<div>...render logic removed...</div>)
}
On load of the ReactJS page, it triggers the apiSlice right away since the default landing page renders the PersonDetails component.
1.But the first call has missing headers:
2.Refetching it via the refetch function now contains the headers:
UPDATE #1: Just observed that the Unit Test works fine and has the headers; so its really just when the App is actually running in the browser when this weird behaviour is occurring, what could it be?
UPDATE #2: Looks like it might be related to MirageJS when its turned on to as to intercept calls and return mock data. I observed that if I turn it off on startup, the initial call to the actual API has the headers. So I guess the question is now why is MirageJS causing this issue?

RTK Query | Invalidate cache of an API service from another API service

I have created multiple RTK Query API services split into multiple files. For this issue I have two services: "Contracts" and "Properties". The contracts service should be able to invalidate the Properties cache when a contract updates, but even after supplying the "Properties" tag to the Contracts service - the cache is not invalidated.
Here is my setup:
Properties:
export const propertyApi = createApi({
reducerPath: 'propertyApi',
baseQuery: fetchBaseQuery({ baseUrl: `${API_BASE_URL}/properties` }),
tagTypes: ['Properties'],
endpoints: builder => ({
// many endpoints
})
})
export const {
// many hooks
} = propertyApi
Contracts:
export const contractApi = createApi({
reducerPath: 'contractApi',
baseQuery: fetchBaseQuery({ baseUrl: `${API_BASE_URL}/contracts` }),
tagTypes: ['Contracts', 'Properties'],
endpoints: builder => ({
// ...
modifyContract: builder.mutation<Contract, { contract: Partial<ContractDto>, contractId: Contract['id'], propertyId: Property['id'] }>({
query: ({ contract, contractId }) => {
return {
url: `/${contractId}`,
method: 'PATCH',
credentials: "include",
body: contract
}
},
// to my understanding, this should invalidate the property cache for the property with 'propertyId', but it doesn't seem to work
invalidatesTags: (_res, _err, { propertyId }) => ['Properties', 'Contracts', { type: 'Properties', id: propertyId }]
})
})
})
export const {
// ...
useModifyContractMutation
} = contractApi
Store setup:
export const STORE_RESET_ACTION_TYPE = 'RESET_STORE'
const combinedReducer = combineReducers({
[photoApi.reducerPath]: photoApi.reducer,
[authApi.reducerPath]: authApi.reducer,
[propertyApi.reducerPath]: propertyApi.reducer,
[cronApi.reducerPath]: cronApi.reducer,
[contractApi.reducerPath]: contractApi.reducer,
auth: authReducer
})
const rootReducer: Reducer = (state: RootState, action: AnyAction) => {
if (action.type === STORE_RESET_ACTION_TYPE) {
state = {} as RootState
}
return combinedReducer(state, action)
}
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => {
return getDefaultMiddleware().concat([
photoApi.middleware,
authApi.middleware,
propertyApi.middleware,
cronApi.middleware,
contractApi.middleware,
errorHandlerMiddleware
])
}
})
setupListeners(store.dispatch)
export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
If those api services have data depending on each other (which is kinda implied by the fact that they should invalidate each other) they should not be multiple api services - they are really just multiple endpoints of one api. We state so at multiple places in the documentation.
Quoting the quickstart tutorial for example:
Typically, you should only have one API slice per base URL that your application needs to communicate with. For example, if your site fetches data from both /api/posts and /api/users, you would have a single API slice with /api/ as the base URL, and separate endpoint definitions for posts and users. This allows you to effectively take advantage of automated re-fetching by defining tag relationships across endpoints.
Instead, if you want to split that one api into multiple files, you can do so - using the code splitting mechanisms described in the documentation.
That also means that you do not have to add a lot of api slices and middlewares in your configureStore call, but just one.
If you're like me, and don't have a say in how the API is made but still have to invalidate the other API's Tag here's a workaround:
...
query: ...
onCacheEntryAdded: (args, { dispatch }) => {
dispatch(otherAPI.util.invalidateTags(["Tag"]))
}
Down side: it fires before query is resolved so we can't check for success

Redux Toolkit RTK Query call endpoints in queryFn

I'm using an endpoint with queryFn instead of query to perform many requests. Is there a way to calling endpoints that are already define instead of using fetchWithBQ ?
Here is an example.
export const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({
baseUrl: "url",
}),
endpoints: (builder) => {
return {
device: builder.query<Device, string>({
query: (id) => `devices/${id}`, // repeat 1
}),
deployments: builder.query<Deployment[], string>({
queryFn: async (arg, _api, _extraOptions, fetchWithBQ) => {
// I would preferred to call the device endpoint directly.
// It will prevent to repeat the url and get cached data.
const result = await fetchWithBQ(`devices/${arg}`); // repeat 2
return ...
},
}),
};
},
});
No, at the moment that is not possible because it would add a tracking of "what depends on what else" to the whole things and that would get very complicated to manage internally.
You would usually do dependent queries just by using two useQuery hooks. And for abstraction of course you could combine those into a custom hook.
const useMyCustomCombinedQuery = (arg) => {
const result1 = useMyFirstQuery(arg)
const result2 = useMySecondQuery(result1.isSuccess ? result1.data.something : skipToken)
return {result1, result2}
}

Resources