(RTK) How to pass the query parameter from react to createSelector - reactjs

I am currently using RTK query to get the event log from the database according to the date at the end of the url:
export const extendedChartApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getChart: builder.query({
query: (date) => `/api/eventlog/${date}`,
transformResponse: responseData => {
const loadedData = responseData['event_log']
return chartAdapter.setAll(initialState, loadedData);
},
providesTags: (result, error, arg) => [
{ type: 'Chart', id: 'LIST' },
...result.ids.map(id => ({ type: 'Chart', id }))
]
}),
})
});
For now, I am able to obtain the data with the useGetChartQuery like this:
const {
data: logEventByDate,
isLoading,
isSuccess,
isError,
error
} = useGetChartQuery(date);
However, the data obtained is the complete normalized data including the ids and entities, what I am trying to achieve is to destructure the normalized data in the slice by creating a memorized selector like this:
export const selectChartResult = extendedChartApiSlice.endpoints.getChart.select();
export const selectChartData = createSelector(
selectChartResult,
chartResult => chartResult.data
);
However, the selectChartData returned undefined. I think I need to pass the parameter to the selectChartData first, like I did with the useGetChartQuery. Has anybody got an idea what is wrong with my code?

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.

Why is updateQueryData from RTK query not updating my state?

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, ...)

How to refresh page after deleting an item using REDUX/RTK Query?

I am fetching data from my api using RTK Query like this
export const coinApi = createApi({
reducerPath: 'coinApi',
baseQuery: fetchBaseQuery({ baseUrl }),
endpoints: (builder) => ({
getCoins: builder.query({
query: () => createRequest(`/watchlist`),
})
}),
});
and im deleting a coin from my table like this
export const deleteCoin = (id) => async (dispatch, getState) => {
try {
dispatch({
type: COIN_DELETE_REQUEST,
});
await axios.delete(`/api/coins/watchlist/${id}`);
dispatch({
type: COIN_DELETE_SUCCESS,
});
} catch (error) {
const message =
error.response && error.response.data.message
? error.response.data.message
: error.message;
dispatch({
type: COIN_DELETE_FAIL,
payload: message,
});
}
};
and in my frontEnd component: I am calling dispatch(deleteCoin(id));
the delete functionality is working, since in my database it is removed however the component does not refresh so the coin still exists on the UI unless I refresh the page myself manually.
I've tried accessing the global data from the RTK query, but cannot do it successfully
I was trying to use useEffect and pass in the dependency data from
const { data, isFetching } = useGetCoinsQuery();
However its still not reloading my component?
How else can i reload my component? This is my first time using RTK Query so I'm not sure how to really access that data and how can it listen to data changes in teh API server?
Thanks
const coins = useSelector((state) => state.coinApi.queries)
const {
loading: loadingDelete,
error: errorDelete,
success: successDelete,
} = coinDelete;
useEffect(() => {}, [dispatch, successDelete, data]);
if (isFetching) return <Loader></Loader>;
const deleteHandler = (id) => {
if (window.confirm('Are you sure?')) {
dispatch(deleteCoin(id));
}
};
Normally, you can use providesTags and invalidatedTags withing RTK-Query to make related queries automatically refetch after a mutation is run. In your case, your delete is not a mutation, but you can still use that mechanism.
In the long run I would encourage you to make a mutation out of your delete action here though, since RTK-Query will work a lot better the more you do in there - and you won't have to have all that code written by hand.
baseQuery: fetchBaseQuery({ baseUrl }),
tagTypes: ['Coins'],
endpoints: (builder) => ({
getCoins: builder.query({
query: () => createRequest(`/watchlist`),
providesTags: [ 'Coins' ]
})
await axios.delete(`/api/coins/watchlist/${id}`);
dispatch({
type: COIN_DELETE_SUCCESS,
});
dispatch(api.util.invalidateTags(['Coins'])) // this will refetch all queries that "provide" the tag `"Coins"`
} catch (error) {
You should read this example https://redux-toolkit.js.org/rtk-query/usage/examples. Notice that provideTags and invalidatesTags.
A general way to refetch:
const MyComponent = () =>{
const { refetch, data, error, isFetching} = useGetGithubByNameQuery();
const toRender = error ? (
<p>Oh no, there was an error</p>
) : isFetching ? (
<p>"Loading..."</p>
) : data ? (
data.map((item)=>{return <p>item.toString()</p>})
) : null;
return(
<>
{toRender}
<button onClick={refetch}>Refresh</button>
</>
)
}

Dispatching function multiple times in order to execute it with different parameters

Background
I'm building a React Native 0.64.1 app using Redux 4.1.0. This app fetches data from an API endpoint via POST which can take multiple "category" params. Only one value can be passed as category at a time, so in order to display data from multiple categories one would have to execute the function one time per category.
This is how the axios request is handled:
export const getData = (tk, value) =>
apiInstance
.request({
url: ENDPOINTS.CATEGORIES,
method: 'POST',
data: qs.stringify({
token: tk,
category: value,
}),
})
.then(response => {
return response.data;
})
.catch(error => {
return Promise.reject(error.message);
});
This function is then executed via a redux action/reducer, etc.
The tricky part is that "value" is set by the user and can be changed at any point in time.
The front end meets this function in a certain screen where this happens:
useEffect(() => {
dispatch(retrieveData(tk, value));
}, [dispatch, value]);
Problem & Question
I've tried doing a for loop that would iterate through an array that contains the possible strings of text value could be, that would look something like this:
const arrayOfValues = ['A','B','C','D']
let value = null;
useEffect(() => {
for (let i = 0; i < arrayOfValues.length; i++) {
value = arrayOfValues[i];
dispatch(retrieveData(tk, value));
}
}, [dispatch, value]);
I know this is horrible and I'm just showing it because it's the only thing I could think about (and it doesn't even work).
An ideal solution would:
Execute the first request on load
Run a request once per item in an array WITHOUT deleting the previously called for data
Each time it runs it needs to update the "value" parameter.
As a note about "retrieveData()", that is just the redux action.
Any help would be very much appreciated.
Solution by #rigojr
This seems like it should work, but either I haven't expressed myself properly or there's something wrong with the answer. I'm guessing it's the former.
#rigojr proposed the following:
export const getData = (tk, values) => values.map((value) => apiInstance
.request({
url: ENDPOINTS.CATEGORIES,
method: 'POST',
data: qs.stringify({
token: tk,
category: value,
}),
}))
Promise.all(getData(tk,values)) *****
.then(responseValues => {
// Dispatch the response, it will come an array of values response.
})
.catch(eer => {
// Error handling
})
Howeve, values in the line marked with many asterisks is inaccessible. I believe this is because previosuly I failed to mention that the whole Redux data flow happens in three separate files.
Dispatching the action: UI dispatches an action onLoad in App.js:
useEffect(() => {
dispatch(retrieveData(tk, values));
}, [dispatch, value]);
The action is ran in action.js file. It looks something like this:
Note that I have added the Promise.all() in this screen, as it seems like the place where it should actually go, instead of the other one.
export const actionTypes = keyMirror({
RETRIEVE_REQUEST: null,
RETRIEVE_SUCCESS: null,
RETRIEVE_FAILURE: null,
});
const actionCreators = {
request: createAction(actionTypes.RETRIEVE_REQUEST),
success: createAction(actionTypes.RETRIEVE_SUCCESS),
failure: createAction(actionTypes.RETRIEVE_FAILURE),
};
export const retrieveData = (tk, values) => dispatch => {
dispatch(actionCreators.request());
Promise.all(getData(tk, values))
.then(data => dispatch(actionCreators.success(data)))
.catch(error => dispatch(actionCreators.failure(error)));
};
Then there's the reducer, of course in reducer.js:
export const initialState = {
loadingData: false,
data: [],
error: null,
};
const actionsMap = {
[actionTypes.RETRIEVE_REQUEST]: state => ({
...state,
loadingData: true,
}),
[actionTypes.RETRIEVE_SUCCESS]: (state, action) => ({
...state,
loadingData: false,
data: action.payload,
}),
[actionTypes.RETRIEVE_FAILURE]: (state, action) => ({
...state,
loadingData: false,
error: action.payload,
}),
};
export default (state = initialState, action) => {
const actionHandler = actionsMap[action.type];
if (!actionHandler) {
return state;
}
return actionHandler(state, action);
};
Data is then accessed via a selector:
const data = useSelector(state => state.data.data);
When running the code above, I am greeted with the following lovely error message:
TypeError: undefined is not a function (near '...}).then(function (response)...')
And in the emulator, I get pointed in the direction of these lines of code:
export const getData = (tk, values) => values.map((value) => apiInstance
.request({
url: ENDPOINTS.CATEGORIES,
method: 'POST',
data: qs.stringify({
token: tk,
category: value,
}),
}))
More specifically, the emulator seems to think that the error has to do with value.map, as it points a little red arrow at "values" just before the method.
Any idea on what went wrong?
Note
Upon refresh the error might change, for example just now it has shown the same error message but it points in the direction of
export const retrieveData = (tk, values) => dispatch => {
dispatch(actionCreators.request());
Promise.all(getData(tk, values))
.then(data => dispatch(actionCreators.success(data)))
.catch(error => dispatch(actionCreators.failure(error)));
};
More specifically, the little red arrow points at getData.
Refreshing again, and the error points at
useEffect(() => {
dispatch(retrieveData(tk, values));
}, [dispatch, value]);
Refrsh once more and it just loses it and goes for a module, as shown in the image:
It doesn't go further from there. Just mind that every single time, the error message is TypeError: undefined is not a function (near '...}).then(function (response)...'), it just points in a new direction.
Solved in
Unable to perform .map whithin function
Try to use a Promise.all():
export const getData = (tk, values) => values.map((value) => apiInstance
.request({
url: ENDPOINTS.CATEGORIES,
method: 'POST',
data: qs.stringify({
token: tk,
category: value,
}),
}))
Promise.all(getData(tk,values))
.then(responseValues => {
// Dispatch the response, it will come an array of values response.
})
.catch(eer => {
// Error handling
})
Read more about Promise.all() here

Resources