RTK query based on data from another request - reactjs

I have some rtk query, that query data based on response of another request ( with axios )
const { dat } = useGetDataQuery({
keys // it comes from another request
})
export const someApi = createApi({
reducerPath: 'someApi',
baseQuery,
endpoints: (builder) => ({
getData: builder.query<
any,
{ keys: string[] }
>({
query: (arg) => {
const { keys } = arg
return {
url: '/some_endpoint',
params: {
keys,
},
}
},
}),
}),
})
And in this case data from previous request it's keys.
Is it possible make request inside query and use this data for my query, instead of doing
this request outside and pass data as params?

I wouldn't recommend it. Theoretically it is possible, using queryFn, as you have access to getState there - but if that state value would ever change, that wouldn't re-execute your query. It's really best to just put in dynamic values as argument.

Related

Infinite scrolling and search with RTK query duplicate results

I'm trying to implement infinite scroll page of posts with search. It works with Reddit JSON API.
There is no pagination in common sense. Reddit api returns listings. Listing JSON responses contain after field which is equivalent to the "next" buttons on the site and in combination with count can be used to page through the listing. So the first request is made without after param and all the following ones contain 'after' with if from the previous server response.
When I change search request, I clean the after state.
This is my rtk api:
export const postsAPI = createApi({
reducerPath: 'posts/api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://www.reddit.com/',
}),
endpoints: build => ({
getPosts: build.query<ITransformedPosts, { limit?: number, searchParam?: string, after?: string }>({
query: ({ limit = 8, after, searchParam }) => ({
url: searchParam ? 'search.json' : '.json',
params: {
limit,
...(after && { after }),
...(searchParam && { q: searchParam })
}
}),
serializeQueryArgs: ({ endpointName, queryArgs }) => { return queryArgs.searchParam || endpointName },
merge: (currentCache, newItems) => {
currentCache.posts.push(...newItems.posts)
currentCache.after = newItems.after
currentCache.hasMore = newItems.hasMore
},
forceRefetch({ currentArg, previousArg }) {
return currentArg !== previousArg
},
transformResponse: (response: IPosts) => ({ posts: response.data.children,
after: response.data.after })
}),
It works fine, except one bug. If I make search request and make the same afterwards, new data are concated to the previous ones. So there are doubled posts.
Are there any chance to say rtk: "Hey rtk, if there are data on the particular request, first return the existing data. And concat new data only when I sent the same request with new 'after' param.

How to implement multiple api call in a single query with RTK query

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

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 stop Redux RTK query from retrying on error

I have some requests which may return 404s. When they do, RTK query will send retries, resulting in hundreds of failed requests. Why is it trying to refetch on error and what can I do?
If your endpoint is in error, RTK Query's useQuery will send a request in two situations:
you change the argument (that would always result in a new request)
you mount a component using this useQuery.
So without seeing your code, I would assume that your component re-mounts somehow and thus leads to another request after mounting.
you can limit the number of retries that rtk automatically does by using the property maxRetries inside your end point.
import { createApi, fetchBaseQuery, retry } from
'#reduxjs/toolkit/query/react'
// maxRetries: 5 is the default, and can be omitted. Shown for
documentation purposes.
const staggeredBaseQuery = retry(fetchBaseQuery({ baseUrl: '/' }), {
maxRetries: 5,
})
export const api = createApi({
baseQuery: staggeredBaseQuery,
endpoints: (build) => ({
getPosts: build.query({
query: () => ({ url: 'posts' }),
}),
getPost: build.query({
query: (id) => ({ url: `post/${id}` }),
extraOptions: { maxRetries: 5 }, // You can override the retry behavior on each endpoint
}),
}),
})
export const { useGetPostsQuery, useGetPostQuery } = api
As docs say, for custom error handling we can use queryFn:
One-off queries that use different error handling behaviour
So if, for any reason, you want to cache request on error, you can do:
getPokemon: build.query<Pokemon, string>({
async queryFn(name, api, extraOptions, baseQuery) {
const result = await baseQuery({
url: `https://pokeapi.co/api/v2/pokemon/${name}`,
method: 'GET'
});
if (result.error?.status === 404) {
// don't refetch on 404
return { data: result.data as Pokemon };
}
if (result.error) {
// but refetch on another error
return { error: result.error };
}
return { data: result.data as Pokemon };
}
}),
You need to customize your createApi function. you can stop permanently retries with setting unstable__sideEffectsInRender property to false
import {
buildCreateApi,
coreModule,
reactHooksModule,
} from '#reduxjs/toolkit/dist/query/react';
const createApi = buildCreateApi(
coreModule(),
reactHooksModule({ unstable__sideEffectsInRender: false })
);
export default createApi;

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