I have a simple collection which I would like to paginate.
I want to sort it and paginate it by a timestamp doc called createdAt.
This is how the call currently looks like:
function getPaginatedItems (db, startAfter) {
return db
.collection('items')
.orderBy('createdAt')
.startAfter(startAfter) // startAfter parameter will be a createdAt Timestamp doc
.limit(3)
.get()
}
To make this easier to work with and display, I created a function that will turn this query snapshot into a paginated object. This looks something like this:
function querySnapshotToPaginatedObject (querySnapshot, total, limit = 3) { if (querySnapshot.empty) {
return {
total: 0,
limit,
data: []
}
} else {
return {
total,
limit,
data: querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}))
}
}
}
As it stands I have a total of 11 items in my firestore, but would like to get them in chunks of three. This all works perfectly when moving forward with the data, however my question then becomes, how do I go back? That is, how do I get data from the previous pages?
Currently what I have in my hands is the total number of items I have, the limit which can be displayed and obviously the three items I am currently displaying.
I have no idea how to keep track of all other ones in order to jump back pages, or jump more than one page for that matter.
So I guess there are two questions here: how do I go back to previous data? And how could I jump different chunks of data?
Is there another way to do this, perhaps by index instead of a specific doc (like I am doing with createdAt)?
Edit: I was asked how I am making my next queries. Basically I have buttons (all with their page numbers) and when I click on them, I do a second call starting with the createdAt attribute of the last item. I then do a second call to my initial query, but passing in the last object as the startAfter parameter in the getPaginatedItems function call.
I am using react as the front-end, so it looks something like this:
getNextBatch (startAfter) {
return {
paginatedItems: querySnapshotToPaginatedObject(
await getPaginatedItems(db, startAfter), 11, 3
)
}
}
...
export default class MyComponent extends React.Component (
render () {
return (
<div>
{this.props.paginatedItem.map(x => <div>{x.name} {x.createdAt}</div>)}
<button onClick={(evt) => console.warn('How do I go back???')}>
Back
</button>
<button onClick={(evt) => getNextBatch(paginatedItem[paginatedItem - 1].createdAt)}>
Next
</button>
</div>
)
}
)
Keep in mind that the component does re-render every time I click the buttons.
Cloud Firestore APIs don't provide a way to page backward. The easiest thing to do in your case is to remember a list of startAfter values that you've used to fetch each page. Each value will represent a page of data. With that, going back to a previous page is just a matter of find the desired startAfter value from that list, then making the query with that.
To be honest, though, your total data set is pretty small, and it's probably not worth paging at all. I'd just get the whole thing and keep it in memory. Paging probably don't become worthwhile until you reach hundreds or thousands of documents (depending on how big each document is, of course, and how much memory you expect to have available).
Related
I'm setting the data that my flatlist component displays using a state called selectedStream. selectedStream changes every time the user presses a different group option. I've noticed that the flatlist takes 1-3 seconds to refresh all the posts that it's currently displaying already. I want there to be a loading indicator so that by the time the indicator goes away, the list is already properly displayed with the newly updated data.
<FlatList
maxToRenderPerBatch={5}
bounces={false}
windowSize={5}
ref={feedRef}
data={selectedStream}/>
Whenever we are working with anything related to the UI, sometimes we may face delays in UI re-rendering. However, we need to first figure out what is actually causing the delay.
The right question to ask about your code would be:
Is the rendering of items taking longer than expected? Or, is the data being passed with a delay because it is dependant on an API call or any other async task?
Once you answer that question, you may end up with two scenarios:
1. FlatList taking longer to render views
This doesn't usually happen as the RN FlatList will only render views that are visible to the user at any given time and will keep rendering new views as the user scrolls through the list. However, there may be some flickering issues for which you can refer to the below article:
8 Ways to optimise your RN FlatList
2. Passing the data causes the delay
This is the most common scenario, where we may call an API endpoint and get some data and then do setState to update any view/list accordingly. A general approach is to show some sort of a progress-bar that would indicate that the application is busy and thus maintaining a proper user-experience. The easiest way to do that is by conditional rendering.
A general example would be:
const [myList, setMyList] = useState();
function callAPIforMyList(){
// logic goes here
}
return {
{myList ? <ActivityIndicator .../> : <Flatlist .... />
}
The above code will check if myList is undefined or has a value. If undefined, it will render the ActivityIndicator or else the FlatList.
Another scenario could be when myList may have existing data but you need to update/replace it with new data. This way the above check may fail, so we can put another check:
const [myList, setMyList] = useState();
const [isAPIbusy, setAPIBusy] = useState(false)
function callAPIformyList() {
setAPIBusy(true)
/// other logics or async calls or redux-dispatch
setAPIBusy(false)
}
return {
{!isAPIBusy && myList ? (<Flatlist .... />) : (<ActivityIndicator .../>)
}
You can add multiple conditions using more turneries such as isAPIBusy ? <View1> : otherBoolean ? <View2> : <Default_View_When_No_Conditions_Match)/>
Hope this helps clarify your needs.
for example, i've a component to fetch and call an API, but that query by default is enabled: false and i should fire that by onClick:
const query = useQuery('key', fetch, { enabled: false })
const exec = () => query.refetch()
return <button onClick={exec}>Load</button>
But i've a new API call after every clicks on the button, actually i want to cancel re-calling the API still the cached data is available and is not stale...
I there any way to implement something like refetch to retrieve cached data but without re-calling the API? our basically react-query has a re-call for any data reteive?
in fact, our data doesn't change frequently and is fix for 2-3 days...
other words, our clients frequently work with nested drop-downs with same API calls and i want to reduce same key queries... imagine that, something like category to select a brand for products
Thanks
You can set staleTime and cacheTime options of your query to Infinity to keep your cache always available
https://react-query.tanstack.com/reference/useQuery
imagine that, something like category to select a brand for products
This is a classic example for react-query where you want to put all dependencies of your query into your cache key (see the official docs). That way, the caches won't override each other, and you'll instantly get the values back from the cache if they are available. Not that you will also get a background refetch, and that's where staleTime comes in. If you don't want that, set a higher staleTime to only retrieve the value from the cache if it exists.
To illustrate, let's take your example of a select of categories for products:
function MyComponent() {
const [category, setCategory] = useState(null)
const { data } = useQuery(['key', category], () => fetchProducts(category), { enabled: !!brand, staleTime: 1000 * 60 * 3 })
<CategorySelect onSelect={setCategory} />
}
Multiple things going on here:
I have some local state, where I store the selected category
This selection drives the query. The query is disabled as long as I have no selection, but enables once the user makes a selection
every time the category changes, the query key changes, which will make react-query trigger a refetch automatically, for the new data
I've set the staleTime to 3 minutes. If I chose a category that I have already chosen, I will get data from the cache. Within 3 minutes, I will only get it from the cache. If my data is older, I will get it from the cache and the data will also be updated in the background.
I am pulling data from Themoviedb and right now you can only retrieve 20 results per page. I'm building a movie-rating application where the user can rate movies as they appear on the screen. One movie at a time is shown. When the user gets through the first 20 movies from Page 1, I want to automatically run a new request for Page 2 to get the next list of movies. I don't want the user to have to click a button to load more movies, or have a "Next Page" button or anything like that. Everything should be on the same page and load automatically.
Right now I am making the request in ComponentDidMount(), loading the list of movies into an array and storing that in my state, and I am clicking through the movies by keeping track of the array index. When I reach the end of the array, that's when I want to make another request for Page 2. This is where I'm having issues. My initial request is in ComponentDidMount(), so I'm not too sure how to handle a second request. Is there any way I can trigger this without a button click? I want something like when the array of the first 20 results is empty, request results from the next page. Would it be bad practice to call ComponentDidMount() again once array.length === 0?
My state:
state = {
currentMovieIndex: 0,
filteredMovieList: [],
currentPage: 1,
}
My request:
getAllMovies(page) {
return fetch(`https://api.themoviedb.org/3/discover/movie?api_key=2bb6427016a1701f4d730bde6d366c84&page=${page}`)
.then(res =>
(!res.ok)
? res.json().then(e => Promise.reject(e))
: res.json())
},
componentDidMount() {
Promise.all([
MovieService.getMyMovies(),
MovieService.getAllMovies(this.state.currentPage)
]).then(([arr1, arr2]) => {
// filter out movies that the user has already rated
let myMovieIds = [];
arr1.map(movie => {
myMovieIds.push(movie.id);
})
let filteredMovies = arr2.results.filter(val => !myMovieIds.includes(val.id))
this.setState({
filteredMovieList: filteredMovies,
});
})
}
If you want to implement a pagination logic without a "Next Page" button or another click trigger I think you should go and search about the "Cursor pagination" logic.
You should get the scroll event and calculate the position and call for more or next data when the scroll reaches a specific position.
(Sry, didn't found any good example!)
I am using React Native. I want to create a very long list.
Each element in the List component is a ListItem component.
My problem is that the component is loading too much time. The List is created with Array.map().
Please have a look at the code.
My question is whether it is somehow possible to create async map() so elements in the list will load one by one and the component won't wait that much?
Tried something like this but nothing works for me. Can someone help please?
class List extends React.Component {
render() {
// very big array
var list = [...Array(5000).keys()];
return (
<Content>
{
list.map((item, index) => {
return (
<ListItem text={item} key={index}/>
);
})
}
</Content>
);
}
}
class ListItem extends React.Component {
render() {
return (
<Button>
<Text>{this.props.text}</Text>
</Button>
);
}
}
The async map code you linked to works, it just doesn't do what you want it to. The time it takes to render 5000 components from your array still takes a long time, async will just allow you to asynchronously do stuff on its completion.
From a UX perspective, you almost never need to render that many elements. Instead, you should load the data in as you need it (user scrolls down to a certain point).
Option 1
So the data flow logic would look something like:
Load first 100 entries to display.
User needs more entries
(scrolls down the list further) so you make a request
Load the
next 100 entries to display.
This is a common pattern if you are using an API to interface with your data.
Option 2
If you have the array in memory, you could try dividing the array into smaller chunks with Array.slice() and rendering each of those asynchronously.
Using async wont fix the issue. The size of array is big and mapping means running a for loop so for loop for let's say 5000 items will take time.
Restrict the size of array or increment them in bundles of 100 as you
go.
So far every question I can find about async rendering involves an AJAX call.
I've got a text input in my React app, which you can type into to filter a big list of items rendered underneath. Of course, as this list gets bigger, it gets more expensive, so typing into the search box is slow and laggy.
Is there a way to render the list asynchronously from the text input? Or somehow else have it on a separate thread? I don't really want to turn this into a remote AJAX request just because it's too pricy to render - I have all the data I need already.
Basically, my text input's onChange method points at handleChange, then in each item's render() function, it checks the hiddenBySearch method to see if it should be displayed:
handleChange = value => {
this.setState({
searchValue: value
})
}
hiddenBySearch = item => {
if(this.props.data.hiddenBySearch){
return this.props.data.hiddenBySearch(item, this.state.searchValue)
}else{
return false
}
}
There's a little more to it but I'd say it's not relevant.
EDIT: It's not a possible duplicate of this post - I'm specifically asking about offsetting React's rendering. I'm fairly sure it's impossible to put this in a Web Worker.