RTK query use infinity scroll and pagination at the same time - reactjs

Describtion:
I am trying to use infinity scroll and pagination at the same time.
I have a component which loads the user orders with infinity scroll and another component in different page which loads user order by pagination.
after i read the documentation I noticed we could use serializeQueryArgs, merge, forceRefetch to create these effects so I did it in my api slice.
Problem:
I have nice infinity scroll but my pagination works just like my infinity scroll
every time I click on a page the new data merged with my prev page.
so I decided to use extera prams for my query just only for serializeQueryArgs mehtod and check if I call this api from the component with infinity scroll or pagination and it works better and now i have another problem for example when I'm in the page 1 and go to page 2 everything seems normal but when i back to page 1 i have duplicated the page one array.
here is my api Slice:
export const orderApi = ApiSlice.injectEndpoints({
tagTypes: ['Orders'],
endpoints: (builder) => ({
getOrdersCount: builder.query({
query: ({ page, size, other }) => ({
url: `/order/orders/count?${page ? `page=${page}` : ''}${
size ? `&size=${size}` : ''
}${other || ''}`,
method: 'GET'
}),
transformResponse: (res) => res // just only one number
}),
getOrderList: builder.query({
providesTags: (result) => {
return result.map((item) => ({ type: 'Orders', id: item.id }));
},
async queryFn({ page, size, other }) {
const portfolioList = await axios.get(
`/order/orders?${page ? `page=${page}` : ''}${size ? `&size=${size}` : ''}${
other || ''
}`
);
if (portfolioList.error) return { error: portfolioList.error };
const endpoints = portfolioList.data.map((item) =>
axios.get(`/market/instruments/${item.insRefId}`)
);
let error = null;
let data = null;
try {
data = await Promise.all(endpoints).then((res) => {
return res.map((item, index) => {
return { ...item.data, ...portfolioList.data[index] };
});
});
} catch (err) {
error = err;
}
return data ? { data } : { error };
},
// Only have one cache entry because the arg always maps to one string
serializeQueryArgs: ({ endpointName, queryArgs }) => {
const { infinity, page, size } = queryArgs;
if (infinity) return endpointName;
return endpointName + page + size;
},
// Always merge incoming data to the cache entry
merge: (currentCache, newItems) => {
currentCache.push(...newItems);
},
// Refetch when the page arg changes
forceRefetch({ currentArg, previousArg }) {
console.log('currentArg', currentArg);
return currentArg !== previousArg;
},
})
})
});
any solution or best practice ?

Related

How do i stop a dependency from re-rendering infinite times in a useEffect?

I have a react-select multiselect component which is required to have preselected values based on the user role. The react-select component is used to filter data in a react-table.
I have 2 user roles - Dev and tester.
If it the dev, I need to have open and Reopened issues to be filtered on load
If it is a tester, I need to have resolved issues on load
This is a part of the code that i am using to achieve preselected
async function loadInfo() {
const body = {
project_id: projDetails.id,
};
const response = await axios({
method: "post",
url: apilist.dropdownData,
data: body,
})
.catch((err) => console.log(err));
if (response) {
const getData = response.data.data;
// console.log("IsGeneralInfo:", getData)
setGeneralInfo(getData);
let filteredenv = getData.status.filter((item) => item.id == 8 || item.id == 6)
let envfiltered = filteredenv.map((k) => {
return ({ label: k.values, value: k.values });
})
// console.log("envfilter", envfiltered);
// handleMultiStatusFilter(envfiltered);
}
}
// const {current:myArray}=useRef([{ label: 'Closed', value: 'Closed' }])
useEffect(() => {
if(envfilter){
let myArray=[{ label: 'Closed', value: 'Closed' },{ label: 'Reopen', value: 'Reopen' }];
handleMultiStatusFilter(myArray);
}
}, [selectedOptions])
const handleStatusFilter = (e) => {
setFilterValue(e);
if (e.length > 0) {
dispatch(filterByValue({ type: e, viewIssue: viewIssue, }))
}
else {
dispatch(showAllStatus({ type: 'All', viewIssue: viewIssue, }))
}
}
const handleMultiStatusFilter = (e) => {
setFiltered([])
let arr = []
e.map((data) => {
setFiltered(prevState => [...prevState, data.value]);
arr.push(data.value);
})
setSelectedOptions(e)
handleStatusFilter(arr)
}
This is a part of the redux code used for filtering
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchIssueList.fulfilled, (state, action) => {
// Add user to the state array
state.issuesList = {
status: 'idle',
data: action.payload.data.data !== undefined ? action.payload.data.data : [],
dataContainer: action.payload.data.data !== undefined ? action.payload.data.data : [],
no_of_records: action.payload.data.data !== undefined ? action.payload.data.data.length : 0,
error: {}
}
})
The code works fine with the filtering once i login, but the rerendering keeps going to infinite loop
Is there any way i could stop the infinite rerendering of code and have the filtering happen on load of the screen?
while working with dependencies in useEffect, try to use the most primitive part you can find. no complex objects, as they change way too fast.
for example: use the length of an array not the array itself.
even though for arrays it's mostly safe to use itself.
sorry. correction: for arrays it's not safe either. complex objects are compared by reference not by value. for that you need primitive types like number, string or boolean.

What is the correct way to call updateCachedData on a click event in a component that uses the RTKQ query?

I can only think of storing a reference to updateCachedData somewhere globally and use it in that click event but I am not sure this is the React way of doing this.
I have a notifications feed built with a Socket.IO server.
By clicking on a notification it should get deleted from the list. (The list should show only unread notifications.)
But when deleting from the list I create a new array as state in the notifications pane.
When I receive a new notification, all the deleted notifications return back - this is not what I intended.
How can I change the cache entry, more precisely remove items from it without remaking the request for all the notifications?
There are no error messages.
Code
getNotifications: build.query<
IDbNotification[],
IGetNotificationsQueryParams
>({
query: (params: IGetNotificationsQueryParams) => ({
url: `notifications?authToken=${params.authToken || ""}&limit=${
params.limit
}&userId=${params.userId || ""}${
params.justUnread ? "&justUnread" : ""
}`,
method: "GET"
}),
keepUnusedDataFor: 0,
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
const { myIo, connectHandler } = getWebSocketConnection(
"notifications",
clone({
subscribtions: arg.userId
? getFollowedUserIds().concat({
uid: arg.userId,
justUnread: arg.justUnread
})
: getFollowedUserIds()
})
);
const listener = (eventData: IDbNotification) => {
if (
(eventData as any).subscriber === arg.userId &&
(!arg.justUnread || typeof eventData.readDateTime === "undefined")
) {
updateCachedData(draft => {
draft.unshift(eventData);
if (draft.length > arg.limit) {
draft.pop();
}
});
}
};
try {
await cacheDataLoaded;
myIo.on("notifications", listener);
} catch {}
await cacheEntryRemoved;
myIo.off("notifications", listener);
myIo.off("connect", connectHandler);
}
})
You can use updateQueryData - updateCachedData is just a shortcut for the current cache entry for convenience.
dispatch(
api.util.updateQueryData('getNotifications', arg, (draft) => {
// change it here
})
)
See this for more context: https://redux-toolkit.js.org/rtk-query/usage/optimistic-updates

Is there any way to fetch all the responses stored in api slice [RTK Query]?

Here's the hook which returns the data based on the page
const {
data,
isFetching,
isLoading,
isError,
} = useGetResourceQuery(page, perPage );
here's the api
export const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: "http://localhost:3001",
}),
tagTypes: ["resource"],
endpoints: (build) => ({
getResource: build.query({
query: (page = 1, perPage = 20) =>
`resource?spage=${page}&limit=${perPage}`,
}),
}),
});
export const {useGetResourceQuery} = api
Is there any way we can extract the state of all the queries?
I want to implement pagination based on scroll (infinite scroll)
Do we need to update the middleware? or are there any ways to access the state and expose it as a function above,
I went through the documentation but couldn't find a solution
I can use local state and concatenate them but wanted to know if it's possible using RTK
Thanks
I think most implementations overcomplicate the problem of "infinite scroll" by a lot.
You can achieve this by stepping a bit back and thinking about what you really need:
the current in-view data
a bit extra data into the direction we're scrolling to
Since we are lazy and do not want to track which direction we're scrolling to, just assume
the current in-view data
a bit extra data into both directions.
Also let's assume the query returns a response like
{
offset: 50,
items: [...]
}
So assuming one page is big enough to contain all the data for a screen, we'd end up with something like
const currentPage = // something calculated from ScrollPosition
const lastResult = usePageQuery(currentPage - 1, { skip: currentPage === 1 }) // don't fetch pages before 0
const currentResult = usePageQuery(currentPage)
const nextResult = usePageQuery(currentPage + 1)
const combined = useMemo(() => {
const arr = new Array(pageSize * (currentPage + 1))
for (const data of [lastResult.data, currentResult.data, nextResult.data]) {
if (data) {
arr.splice(data.offset, data.items.length, ...data.items)
}
}
return arr
}, [pageSize, currentPage, lastResult.data, currentResult.data, nextResult.data])
// work with `combined` here
Since requests that are not referenced by your component, will stay in the store for 60 seconds, the user can scroll up quickly through many pages without further requests - but also, data that will probably never scrolled to again will be removed from the cache and simply re-fetched when necessary.
You can use queryFn to assemble the cached results you already have. remember to use invalidation to get the updated state.
In the following code, the first queryFn is just a fake API, and the second endpoint is the solution you are looking for:
export const songsApi = createApi({
reducerPath: "songsApi",
baseQuery: fetchBaseQuery(),
endpoints: (builder) => ({
getSongs: builder.query<Result<Song>, { offset: number; limit: number }>({
queryFn: ({ offset, limit }) => {
let list: Song[] = [];
for (let i = offset; i < offset + limit; i++) {
list.push({ id: i.toString(), name: `Song ${i}` });
}
return new Promise((resolve) => {
setTimeout(() => {
resolve({
data: {
total: 100,
list
} as Result<Song>
});
}, 1000);
});
}
}),
getAllSongs: builder.query<Result<Song>, void>({
queryFn: (_, { getState }) => {
let state = getState();
let songs: Song[] = [];
for (let offset = 0; offset < 100; offset += 10) {
const { data } = songsApi.endpoints.getSongs.select({
offset,
limit: 10
})(state as any);
if (data && data.list) {
songs.push(...data!.list);
}
}
return Promise.resolve({ data: { total: 100, list: songs as Song[] } });
}
})
})
});
interface Result<T> {
total: number;
list: T[];
}
Since RTK v1.9 you can now use the merge option to merge the data of multiple queries. This is especially useful for things like infinite scrolling. Please check the answer I posted on another question for more information.
createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
listItems: build.query<string[], number>({
query: (pageNumber) => `/listItems?page=${pageNumber}`,
// Only have one cache entry because the arg always maps to one string
serializeQueryArgs: ({ endpointName }) => {
return endpointName
},
// Always merge incoming data to the cache entry
merge: (currentCache, newItems) => {
currentCache.push(...newItems)
},
// Refetch when the page arg changes
forceRefetch({ currentArg, previousArg }) {
return currentArg !== previousArg
},
}),
}),
})

Why only one button?

I make a request to my local server using fetch() method. The server returns this response:
{
// total quantity elements in all page
"total":7,
// quantity elements in one page
"perPage":3,
// current page
"page":1,
// quantity page
"lastPage":3,
// it category list. I display my category list on page.
"data":[
{"id":1,"title":"animals","created_at":"/...","updated_at":"/..."},
{"id":2,"title":"space","created_at":"/...","updated_at":"/..."},
{"id":3,"title":"sport","created_at":"/...","updated_at":"/..."}
]
}
Also in my local server, I have ability to use query parameters page or limit which I insert in URL:
page - using this param I can implement pagination
limit - using this param I can implement choose quantity element
I have two tasks:
Make pagination (DONE)
Make ability to choose quantity element on page using three buttons (quantity three elements, quantity four elements, quantity five elements)
First task I've already done, however the second task is where I have a problem.
Instead of three buttons, I have only one button. When I click this button in my page it displays two elements. I also need to show the other 2 buttons which will display three and four elements respectively when clicked.
See screenshot:
What to fix in the code?
Maybe I wrote something wrong in the return?
I comment code line which implement choose quantity element
Home.js:
const Home = () => {
const [value, setValue] = useState({
listCategory: [],
currentPage: 1,
buttonsPagination: 0,
quantityElementPage: 3, // this line
buttonsQuantityElementPage: 3 // this line
});
useEffect(() => {
async function fetchData(currentPage, quantityElementPage) { // this line
try {
const res = await apiCategory('api/categories', {
method: 'GET',
}, currentPage, quantityElementPage ); // this line
console.log(res);
setValue({
listCategory: res.data,
currentPage: res.page,
buttonsPagination: Math.ceil(res.total / res.perPage),
quantityElementPage: res.perPage, // this line
});
} catch (e) {
console.error(e);
}
}
fetchData(value.currentPage, value.quantityElementPage); // this line
}, [value.currentPage, value.quantityElementPage]); // this line
const changePage = (argPage) => {
setValue((prev) => ({
...prev,
currentPage: argPage,
}));
};
const changeQuantityElementPage = (argElement) => { // this method
setValue((prev) => ({
...prev,
quantityElementPage: argElement,
}));
};
return (
<div>
<Table dataAttribute={value.listCategory} />
{[...Array(value.buttonsPagination)].map((item, index) => (
<button key={'listCategory' + index}
onClick={() => changePage(index + 1)}>{index + 1}
</button>
))}
//here I display button who choose quantity element:
{[...Array(value.buttonsQuantityElementPage)].map((item, index) => (
<button onClick={() => changeQuantityElementPage(index+2)}>quantity element - {index+2}
</button>
))}
</div>
);
};
apiCategory.js:
export const apiCategory = async (url, args, valuePage, valueElement) => { //add valueElement in argument
const getToken = localStorage.getItem('myToken');
const response = await fetch(`${apiUrl}${url}?page=${valuePage}&limit=${valueElement}`, { //add valueElement in param limit
...args,
headers: {
"Content-type": "application/json; charset=UTF-8 ",
"Authorization": `Bearer ${getToken}`,
"Accept": 'application/json',
...args.headers,
},
});
return response.json();
}
This is happening because your value.buttonsQuantityElementPage is undefined. When you call setValue in useEffect, you did not include the previous values. buttonsQuantityElementPage no longer exists on value.
Also, if the Array constructor Array() receives an undefined value, it will return an array with a single undefined value. i.e. [...Array(undefinedValue)] will yield [undefined]
There are two options for a fix.
Option 1. Update the setValue in your useEffect to include all previous values and then override the properties with new values.
setValue(prev => ({
...prev,
listCategory: res.data,
currentPage: res.page,
buttonsPagination: Math.ceil(res.total / res.perPage),
quantityElementPage: res.perPage,
}));
Option 2. Split out the buttonsQuantityElementPage into its own variable. As far as I can see in your code, buttonsQuantityElementPage does not change. If that is the case then use
const buttonsQuantityElementPage = 3
and in the return section
[...Array(buttonsQuantityElementPage)].map((item, index) => (
<button onClick={() => changeQuantityElementPage(index + 2)}>quantity element - {index + 2}
</button>
))

react-admin: changing a list from store without http requests

I am using react-admin and I need to control directly the store from one resource, in my case, the orders resource.
Everytime I run the GET_LISTit appends the new records in the list from the store, but, I would like to get a new list from the server and discard the old ones. Here`s where I retrieve the records:
dataProvider(GET_LIST, 'orders', {
filter: { updatedAt: filterDate }, // Get date from Filter.
sort: { field: 'updatedAt', order: 'DESC' },
pagination: { page: 1, perPage: 999 },
}).then(response => response.data)
So, I decided to manipulate the store directly and after some digging I saw this answer and this code from the source:
const dataReducer: Reducer<RecordSetWithDate> = (
previousState = initialState,
{ payload, meta }
) => {
if (meta && meta.optimistic) {
if (meta.fetch === UPDATE) {
const updatedRecord = {
...previousState[payload.id],
...payload.data,
};
return addRecords([updatedRecord], previousState);
}
if (meta.fetch === UPDATE_MANY) {
const updatedRecords = payload.ids.map(id => ({
...previousState[id],
...payload.data,
}));
return addRecords(updatedRecords, previousState);
}
if (meta.fetch === DELETE) {
return removeRecords([payload.id], previousState);
}
if (meta.fetch === DELETE_MANY) {
return removeRecords(payload.ids, previousState);
}
}
if (!meta || !meta.fetchResponse || meta.fetchStatus !== FETCH_END) {
return previousState;
}
switch (meta.fetchResponse) {
case GET_LIST:
case GET_MANY:
case GET_MANY_REFERENCE:
return addRecords(payload.data, previousState);
case GET_ONE:
case UPDATE:
case CREATE:
return addRecords([payload.data], previousState);
default:
return previousState;
}
};
So, based on that, I created a custom action to delete the old ids from my list and add the new ones retrieved from the data source:
import {GET_LIST, DELETE_MANY, FETCH_END } from 'react-admin';
export const UPDATE_ORDER_ADMIN = 'UPDATE_ORDER_ADMIN';
export const update_orders_admin = (data, oldIDS) => ({
type: UPDATE_ORDER_ADMIN,
payload: { data, ids: oldIDS },
meta: {
resource: 'orders',
optimistic: true,
fetch: DELETE_MANY,
fetchResponse: GET_LIST,
fetchStatus: FETCH_END,
},
});
And I am using this custom action after retrieve data from the backend:
dataProvider(GET_LIST, 'orders', {
filter: { updatedAt: filterDate }, // Get date from Filter.
sort: { field: 'updatedAt', order: 'DESC' },
pagination: { page: 1, perPage: 999 },
}).then(response => response.data)
.then(data => {
const ids = orders ? Object.keys(orders) : [];
update_orders_admin(data, ids);
this.setState({ isLoading: false })
return null;
});
However, the system is calling the DELETE action from backend, trying to delete the records from the database, while, what I would like is just delete these records from my view.
Any thoughts?
In your custom action you have the fetch set as DELETE_MANY which will do a loop over every id performing DELETE operation. Not sure if you implementation will work, but the current error is about that. You could try to remove the fetch ans see what happens, but I think without it he will not fetch records. If I'm not mistaken RA only adds new ids to data, however if data changed in the meantime I don't think it will replace the changed data for that you need to reimplement the data provider to change the update data behaviour which is similar to what you're trying.

Resources