How to make an infinite scroll with react-query? - reactjs

I'm trying to use react-query useInfiniteScroll with a basic API, such as the cocktaildb or pokeapi.
useInfiniteQuery takes two parameters: a unique key for the cache and a function it has to run.
It returns a data object, and also a fetchMore function. If fetchMore is called - through an intersection observer for exemple -, useInfiniteQuery call its parameter function again, but with an updated payload thanks to a native callback getFetchMore().
In the official documentation, getFetchMore automatically takes two argument: the last value returned, and all the values returned.
Based on this, their demo takes the value of the previous page number sent by getFetchMore, and performs a new call with an updated page number.
But how can I perform the same kind of thing with a basic api that only return a json?
Here is the official demo code:
function Projects() {
const fetchProjects = (key, cursor = 0) =>
fetch('/api/projects?cursor=' + cursor)
const {
status,
data,
isFetching,
isFetchingMore,
fetchMore,
canFetchMore,
} = useInfiniteQuery('projects', fetchProjects, {
getFetchMore: (lastGroup, allGroups) => lastGroup.nextCursor,
})

infinite scrolling relies on pagination, so to utilize this component, you'd need to somehow track what page you are on, and if there are more pages. If you're working with a list of elements, you could check to see if less elements where returned in your last query. For example, if you get 5 new items on each new fetch, and on the last fetch you got only 4, you've probably reached the edge of the list.
so in that case you'd check if lastGroup.length < 5, and if that returns true, return false (stop fetching more pages).
In case there are more pages to fetch, you'd need to return the number of the current page from getFetchMore, so that the query uses it as a parameter. One way of measuring what page you might be on would be to count how many array exist inside the data object, since infiniteQuery places each new page into a separate array inside data. so if the length of data array is 1, it means you have fetched only page 1, in which case you'd want to return the number 2.
final result:
getFetchMore: (lastGroup, allGroups) => {
const morePagesExist = lastGroup?.length === 5
if (!morePagesExist) return false;
return allGroups.length+1
}
now you just need to use getMore to fetch more pages.

The steps are:
Waiting for useInfiniteQuery to request the first group of data by default.
Returning the information for the next query in getNextPageParam.
Calling fetchNextPage function.
Reference https://react-query.tanstack.com/guides/infinite-queries
Example 1 with rest api
const fetchProjects = ({ pageParam = 0 }) =>
fetch('/api/projects?cursor=' + pageParam)
const {
data,
isLoading,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage) => {
// lastPage signature depends on your api respond, below is a pseudocode
if (lastPage.hasNextPage) {
return lastPage.nextCursor;
}
return undefined;
},
})
Example 2 with graphql query (pseudocode)
const {
data,
fetchNextPage,
isLoading,
} = useInfiniteQuery(
['GetProjectsKeyQuery'],
async ({ pageParam }) => {
return graphqlClient.request(GetProjectsQuery, {
isPublic: true, // some condition/variables if you have
first: NBR_OF_ELEMENTS_TO_FETCH, // 10 to start with
cursor: pageParam,
});
},
{
getNextPageParam: (lastPage) => {
// pseudocode, lastPage depends on your api respond
if (lastPage.projects.pageInfo.hasNextPage) {
return lastPage.projects.pageInfo.endCursor;
}
return undefined;
},
},
);
react-query will create data which contains an array called pages. Every time you call api with the new cursor/page/offset it will add new page to pages. You can flatMap data, e.g:
const projects = data.pages.flatMap((p) => p.projects.nodes)
Call fetchNextPage somewhere in your code when you want to call api again for next batch, e.g:
const handleEndReached = () => {
fetchNextPage();
};
Graphql example query:
add to your query after: cursor:
query GetProjectsQuery($isPublic: Boolean, $first: Int, $cursor: Cursor) {
projects(
condition: {isPublic: $isPublic}
first: $first
after: $cursor
) ...

Related

Velo by Wix, JSON data in repeater

I'm trying to get the temperature of each hour from this website: https://www.smhi.se/vader/prognoser/ortsprognoser/q/Stockholm/2673730
I'm getting the data from https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/16/lat/58/data.json. The "t" object is the temperature.
The problem I have is displaying the data for each hour in the repeater.
Here is my backend-code:
import { getJSON } from 'wix-fetch';
export async function getWeather() {
try {
const response = await getJSON('https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/16/lat/58/data.json');
console.log(response) // all data
const tempData = response.timeSeries[0].parameters[10].values[0];
return tempData // Only returns "t" - temperature
} catch (e) {
return e;
}
}
The backend part works, however the frontend doesn't.
import { getWeather } from 'backend/getSMHI.jsw'
$w.onReady(function () {
(
getWeather().then(weatherInfo => {
$w('#weatherRepeater').onItemReady(($item, itemData, index) => {
if (index > 6) {
$item('#tempText').text = itemData.timeSeries[index].parameters[1].values[0];
} else if (index === 6) {
$item('#tempText').text = itemData.timeSeries[index].parameters[0].values[0];
} else {
$item('#tempText').text = itemData.timeSeries[index].parameters[10].values[0];
} // The parameters number for "t" changes depending on the index
})
$w('#weatherRepeater').data = weatherInfo;
})
)
})
Seems like there are at least a couple of issues here.
First, you are retrieving a single number from the API and trying to put that in a repeater. From the description of what you're trying to do, it would seem that you mean to be retrieving a list of numbers, probably as an array. You probably want to do some filtering and/or mapping on the response data instead of directly accessing a single value.
Second, the data you send to a repeater must be in the proper format. Namely, it must be an array of objects, where each object has a unique _id property value (as a string). You are not doing that here. You are simply assigning it a number.
Third, and this is just an efficiency thing, you don't need to define the onItemReady inside the then(). Not that it will really make much of a difference here.

Updating multiple queries in useMutation Apollo-client without refetching them

I'm executing this mutation in my NewBook component:
const [addBook] = useMutation(ADD_BOOK, {
update: (cache, response) => {
cache.updateQuery({ query: ALL_BOOKS }, ({ allBooks }) => {
return { allBooks: allBooks.concat(response.data.addBook) };
});
},
refetchQueries: [{ query: ALL_AUTHORS }, { query: ALL_GENRES }],
options: {
awaitRefetchQueries: true,}});
Instead of having to refetch those two queries, I'd like to update them like ALL_BOOKS - but could not find any example in the docs. Does anyone know a way to accomplish that?
Thank you.
What you need to do is make multiple cache updates based on response data.
Once you add your new book to the query, the next step is to fetch all authors.
cache.updateQuery({ query: ALL_BOOKS }, ({ allBooks }) => {
return { allBooks: allBooks.concat(response.data.addBook) };
});
//Get all authors
const existingAuthors = cache.readQuery({
query: ALL_AUTHORS,
//variables: {}
});
//If we never called authors, do nothing, as the next fetch will fetch updated authors. This might be a problem in the future is some cases, depending on how you fetch data. If it is a problem, just rework this to add newAuthor to the array, like allAuthors: [newAuthor]
if(!existingAuthors.?length) {
return null
}
The next thing is that we need to compare the new book's author with existing authors to see if a new author was added.
//continued
const hasAuthor = existingAuthors.find(author => author.id === response.data.createBook.id)
//Double check response.data.createBook.id. I don't know what is returned from response
//If author already exists, do nothing
if(hasAuthor) {
return null
}
//Get a new author. Make sure that this is the same as the one you fetch with ALL_AUTHORS.
const newAuthor = {
...response.data.createBook.author //Check this
}
cache.writeQuery({
query: ALL_AUTHORS,
//variables: {}
data: {
allAuthors: [newAuthor, ...existingAuthors.allAuthors]
},
});
Then continue the same with ALL_GENRES
Note:
If you called ALL_GENERES or ALL_BOOKS with variables, you MUST put the SAME variables in the write query and read query. Otherwise Apollo wont know what to update
Double check if you are comparing numbers or strings for authors and genres
Double check all of the variables I added, they might be named different at your end.
Use console.log to check incoming variables
You can probably make this in less lines. There are multiple ways to update cache
If it doesn't work, console.log cache after the update and see what exactly did apollo do with the update (It could be missing data, or wrong variables.)
Add more checks to ensure some cases like: response.data returned null, authors already fetched but there are none, etc...

How to use data from last query as argument for next call of the same query with RTK Query?

I'm moving from plain redux thunks to RTK query and I've run into a problem. After first query, the response contains a field with a value (kinda token or cursor) and I need to use it as argument in the next fetch of the same query and so on. With plain thunks I just read the token value from the store with a selector and use it as an argument
Something like this, but of course it causes an error:
const { data } = useUsersQuery({
someToken: data.someToken,
});
How can I achieve it?
UPDATE
I solved it with useLazyQuery():
const [trigger, { data }] = useLazyUsersQuery();
trigger({
someToken: data?.someToken,
});
It looks ugly, though
You can just use skipToken or the skip option:
const { data } = useUsersQuery({
someToken: firstResult.data.someToken,
}, {
skip: !firstResult.isSuccess
});
or
const { data } = useUsersQuery(firstResult.isSuccess ? {
someToken: firstResult.data.someToken,
} : skipToken);

map redux saga results

Code
I followed this stackoverflow post to understand how to use the map with the yield.
my code is splitted in 3 parts:
Example data
citiesCode = [
{
bankCityId: A305
cityId: B544
},
{
bankCityId: R394
cityId: D873
},
]
1) the function that invoke when i launch the relative action
export function* getInvoiceCities({ citiesCode }) {
yield call(invoiceCities, citiesCode);
}
2)this function allow me to map the array that citiesCode is
export function* invoiceCities(citiesCode) {
yield all(citiesCode.map(cityCode => call(getCityInfo, cityCode)));
}
3) in this last function i use the relative code to made a call to the bankCityUrl and cityUrl to obtain the information about the relative city.
const citiesInfoList = [];
function* getCityInfo({ bankCity, city }) {
const cityUrl = `/cities/${city}`;
const bankCityUrl = `/cities/${bankCity}`;
try {
const cityInfoResponse = yield call(fetchWrapper, {
url: cityUrl,
});
const bankCityInfoResponse = yield call(fetchWrapper, {
url: bankCityUrl,
});
citiesInfoList.push(cityInfoResponse, bankCityInfoResponse);
console.log('cities.info', citiesInfoList);
// if (cityInfoResponse.status && bankCityInfoResponse.status === 'success') {
// yield put(saveInvoiceCitiesResponse(citiesInfoList));
// }
} catch ({ packet, response }) {
if (response.status !== 422) {
yield put(pushError({ text: 'sendPraticeSectionError' }));
}
}
BUG
The main bug is: If I call multiple time getInvoiceCities save to make this redux call I store the same cities more and more time.
Just to make an example:
citiesInfoList = []
I call it for the first time: I console.log('cities.info', citiesInfoList); citiesInfoList will be filled with the right results
I call it for the second time: I console.log('cities.info', citiesInfoList); citiesInfoList will be filled with the right results x 2
I call it for the second time: I console.log('cities.info', citiesInfoList); citiesInfoList will be filled with the right results x 3
there is a way to avoid this behaviour ? can i stop to store multiple times the same results ?
This scenario is happening because all your sagas sits in one file
If I undertood you correctly, when you call an action that invokes getInvoiceCities, it will invoke invoiceCities, and the later will invoke getCityInfo for each cityCode.
BUT, since you have defined const citiesInfoList = []; in the same module. What happens next is that getCityInfo will start using citiesInfoList for each getCityInfo invocation, resulting in a duplicated results as you have described.
I suggest the following:
Recommended: Get citiesInfoList out of this file to a separate file, and build a reducer to manage it.
Reset citiesInfoList = [] before every push.
i found, just some hours ago, an easy answer.
1) i deleted citiesInfoList = [] ( it was very ugly to see imho)
2) in the second function i did this
export function* invoiceCities(citiesCode) {
const results = yield all(
citiesCode.map(cityCode => call(getCityInfo, cityCode)),
);
yield put(saveInvoiceCitiesResponse(results));
}

How to verify sorting of multiple ag-grid columns in Protractor

I am using protractor for e2e testing.
There is a ag-grid table where multiple columns are sorted in ascending order.
How do i go about verifying this?
Picture of Sample table
In AgGrid the rows might not always be displayed in the same order as you insert them from your data-model. But they always get assigned the attribute "row-index" correctly, so you can query for it to identify which rows are displayed at which index.
So in your E2E-tests, you should create two so-called "page objects" (a selector fo something in your view, separated from the text-execution code, google for page object pattern) like this:
// list page object
export class AgGridList {
constructor(private el: ElementFinder) {}
async getAllDisplayedRows(): Promise<AgGridRow[]> {
const rows = $('div.ag-body-container').$$('div.ag-row');
await browser.wait(EC.presenceOf(rows.get(0)), 5000);
const result = await rows.reduce((acc: AgGridRow[], elem) => [...acc, new AgGridArtikelRow(elem)], []);
return await this.sortedRows(result);
}
private async sortedRows(rows: AgGridRow[]): Promise<AgGridRow[]> {
const rowsWithRowsId = [];
for (const row of rows) {
const rowIndex = await row.getRowIndex();
rowsWithRowsId.push({rowIndex, row});
}
rowsWithRowsId.sort((e1, e2) => e1.rowIndex - e2.rowIndex);
return rowsWithRowsId.map(elem => elem.row);
}
}
// row page object
export class AgGridRow {
constructor(private el: ElementFinder) {}
async getRowIndex(): Promise<number> {
const rowIndexAsString: string = await this.el.getAttribute('row-index');
return parseInt(rowIndexAsString, 10);
}
}
And in your test:
it('should display rows in right order', async () => {
const rows = await gridList.getCurrentDisplayedRows(); // gridList is your AgGridList page object, initialised in beforeEach()
// now you can compare the displayed order to the order inside your data model
});
What this code does: you make page objects for accessing the table as a whole and for accessing elements within a row. To accessing the list in the same order as it is displayed in the view, you have to get all displayed rows (with lazy loading or pagination it should be below 100, otherwise your implementation is bad), get the rowIndex from each of them, sort by it and only then return the grid-list to the test-execution (.spec) file.

Resources