How to properly paginate data in React with Firestore? - reactjs

As I am starting my experience with Firebase I am a little bit struggling with the pagination of posts on my blog website :(.
I think I kind of understood the docs from Google and I know how to move the pagination to the next page. However, I have absolutely no clue how to paginate back to the previous page.
Basically, I wanted to have a simple pagination component which will look something like that: < 1 2 3 [...] > (where you can paginate next and back using the arrows).
It is fine to paginate to the next page but, when it comes to paginating back I cannot find any proper tutorial to do it in pure React.
I have tried to use various methods from startAt, endAt, endBefore etc. But the result was or an error or it was moving me back to the first page (even when I was on the third or fourth)
I even tried to find the first object in an array and use it as endBefore but it resulted again in paginating back to the first page.
That's how my code looks right now (yes I know that pageNext() and pagePrev() are the same)
import React, { Component } from 'react'
import { withFirebase } from './Firebase'
import Post from './Post'
import '../scss/Post.scss'
class Posts extends Component {
constructor(props) {
super(props);
this.state = {
loading:false,
posts:[],
post_id:[],
lastVisible:null,
limit:2
}
this.handlePageNext = this.handlePageNext.bind(this);
}
componentDidMount() {
let newPosts=[];
let postsId=[];
this.setState({ loading: true });
this.props.firebase.posts()
.orderBy('date', 'desc')
.limit(2)
.get().then(querySnapshot => {
let lastVisible = querySnapshot.docs[querySnapshot.docs.length-1];
this.setState({ lastVisible: lastVisible});
querySnapshot.forEach(doc => {
newPosts = newPosts.concat(doc.data());
postsId = postsId.concat(doc.id);
this.setState({
posts:newPosts,
post_id:postsId,
loading:false
});
})
})
}
handlePageNext() {
let newPosts=[];
let postsId=[];
this.setState({ loading: true });
this.props.firebase.posts()
.orderBy('date', 'desc')
.startAt(this.state.lastVisible)
.limit(this.state.limit)
.get().then(querySnapshot => {
let lastVisible = querySnapshot.docs[querySnapshot.docs.length-1];
this.setState({ lastVisible:lastVisible });
querySnapshot.forEach(doc => {
newPosts = newPosts.concat(doc.data());
postsId = postsId.concat(doc.id);
this.setState({
posts:newPosts,
post_id:postsId,
loading:false
});
})
})
}
handlePagePrev() {
let newPosts=[];
let postsId=[];
this.setState({ loading: true });
this.props.firebase.posts()
.orderBy('date', 'desc')
.startAt(this.state.lastVisible)
.limit(this.state.limit)
.get().then(querySnapshot => {
let lastVisible = querySnapshot.docs[querySnapshot.docs.length-1];
this.setState({ lastVisible:lastVisible});
querySnapshot.forEach(doc => {
newPosts = newPosts.concat(doc.data());
postsId = postsId.concat(doc.id);
this.setState({
posts:newPosts,
post_id:postsId,
loading:false
});
})
})
}
render() {
return (
<div className='posts'>
<div className='row'>
{this.state.posts.map((post, i) => (
<Post
key={i}
title={post.title}
author={post.author}
desc={post.desc}
text={post.text}
id={this.state.post_id[i]}
date={post.date}
imgURL={post.imgURL}/>
))}
{this.state.loading && <p>Loading...</p>}
<button className='btn' onClick={() => this.handlePagePrev()}>←</button>
<button className='btn' onClick={() => this.handlePageNext()}>></button>
</div>
</div>
)
}
}
export default withFirebase(Posts);
I wanted to have a simple pagination using buttons (left and right arrows) but I am struggling with it for already 3rd hour and cannot find the proper solution to this.

You have to keep the "lastVisible" and pass it to startAfter(). 2 functions I wrote below:
export const getMostRecentPostsFirstPage = (limit, specificUserId) => {
if (!Number.isInteger(limit) || limit < 1) {
throw new Error('limit must be a positive integer');
}
const collection = Firestore.collection('posts');
let query = null;
if (specificUserId) {
query = collection
.where('userId', '==', `${specificUserId}`)
.orderBy('postedTimestamp', 'desc')
.limit(limit);
} else {
query = collection.orderBy('postedTimestamp', 'desc').limit(limit);
}
return new Promise((resolve, reject) => {
const posts = [];
query
.get()
.then(snapshot => {
const lastVisible = snapshot.docs[snapshot.docs.length - 1];
snapshot.forEach(post => {
posts.push(post.data());
});
const hasMore = posts.length == limit;
resolve({ posts: posts, lastVisible: lastVisible, hasMore: hasMore });
})
.catch(error => reject(error));
});
};
export const getMostRecentPostsNextPage = (lastVisible, limit, specificUserId) => {
if (!lastVisible) {
throw new Error('Need to provide lastVisible argument');
}
if (!Number.isInteger(limit) || limit < 1) {
throw new Error('limit must be a positive integer');
}
const collection = Firestore.collection('posts');
let query = null;
if (specificUserId) {
query = collection
.where('userId', '==', `${specificUserId}`)
.orderBy('postedTimestamp', 'desc')
.startAfter(lastVisible)
.limit(limit);
} else {
query = collection
.orderBy('postedTimestamp', 'desc')
.startAfter(lastVisible)
.limit(limit);
}
return new Promise((resolve, reject) => {
const posts = [];
query
.get()
.then(snapshot => {
const lastVisible = snapshot.docs[snapshot.docs.length - 1];
snapshot.forEach(post => {
posts.push(post.data());
});
const hasMore = posts.length == limit;
resolve({ posts: posts, lastVisible: lastVisible, hasMore: hasMore });
})
.catch(error => reject(error));
});
};
It uses redux-saga, but you get the idea.
on first query, do not call "startAfter()", but do on the subsequent queries, and you must save "lastVisible" between each call.

Here is standard pagination by using Firebase in reactjs.

Related

getDownloadURL in array of dictionary (re-rendering issue, forEach)

I have an array of dictionaries, (e.g. [{}, {}, {}, {}], each dictionary contains information about book)
I want to download image from firebase storage using getDownloadURL.
My current code's like...
const [resObj, setresObj] = useState() // empty variable for update state
let result = [] //create empty array for copy & push new obj
useEffect(() => {
props.resObj.forEach((obj) => { // props.resObj: array of dictionary I explained before
const jpgName = 'bookDB/'+ obj.도서번호 + '.jpg';
const imgRef = ref(storage, jpgName)
getDownloadURL(imgRef)
.then((url) => {
result1.push({
...obj,
bookUrl: url
}) // copy & push dictionary
})
.catch((error) => {
if (error.code === 'storage/object-not-found') {
console.log('이미지 파일 없음')
result1.push({
...obj,
bookUrl: "https://upload.wikimedia.org/wikipedia/commons/a/ac/No_image_available.svg"
})
} else { console.log(error)}
})
})
setresObj(result1)
}, [])
after this code update 'resObj' variable,
I map resObj in component like...
return (
<div>
{resObj? resObj.map(item => {
<img
key = {}
className = '~~'
onClick = {}
src = {item.bookUrl}
/>
})}
</div>
)
unfortunately.. it doesn't show nothing..
It seems that forEach, useEffect, useState, getDownloadURL Promise seriously entangled..
I tried 1) devide download image code as function, 2) devide download image code as recoil, 3) escape download image code from useEffect, 4) ...(extra variances of code)...
The problem is that your call to setresObj happens before any of the calls to result1.push have happened, so you're always setting an empty array. It's easiest to verify this by setting breakpoints and running in the debugger, or by adding some console.log calls.
The fix is to use Promise.all to wait for all download URLs to have been retrieved and only then call setresObj. Something like this:
useEffect(() => {
let promises = props.resObj.map((obj) => {
const jpgName = 'bookDB/'+ obj.도서번호 + '.jpg';
const imgRef = ref(storage, jpgName)
return getDownloadURL(imgRef)
.then((url) => {
return {
...obj,
bookUrl: url
}
})
.catch((error) => {
if (error.code === 'storage/object-not-found') {
console.log('이미지 파일 없음')
result1.push({
...obj,
bookUrl: "https://upload.wikimedia.org/wikipedia/commons/a/ac/No_image_available.svg"
})
} else { console.log(error)}
})
})
Promise.all(promises).then((results) => {
setresObj(results);
});
}, [])

how to update/re-render a React list after an item is deleted, using classes

Thanks for any support. I'm learning React and need to solve the problem consisting in that I can't make React to re-render after an item is deleted from a list.
Firstly I would like to say that I have follow the answers I found searching but still no luck.
The scenario is that I'm using React to fetch a list from and API and render it in the same screen with a form for editing and listing the specific information for every item in the list (fields are just name and lastname). The list is displayed with a button for edit which makes the form for edit, and with another button for delete. The list displays the two only fields which are name and lastname which are displayed using ListGroupItem from reacstrap that when onClick uses the form for listing only. I also have the logic for add items.
I'm able to add, update, list with no problems and re-rendering properly. However when deleting I'm just able to delete the item from the API but I have to manually re-render to display the update list
import React, { Component } from "react";
import { Button, Container, Row, Col } from "reactstrap";
import ListBebes from "./components/ListBebes";
import AddBebeForm from "./components/AddBebeForm";
import EditBebeForm from "./components/EditBebeForm";
import { fetchBebes, fetchBebe, addBebe, deleteBebe } from "./api";
import Websocket from "react-websocket";
class App extends Component {
constructor(props) {
super(props);
this.state = {
bebes: [],
bebe: {},
current_bebe_id: 0,
is_creating: true,
is_fetching: true,
is_justRead: true,
has_updated: false,
};
this.socket = React.createRef();
this.focusSocket = this.focusSocket.bind(this);
this.handleItemClick = this.handleItemClick.bind(this);
this.handleEditClick = this.handleEditClick.bind(this);
this.handleDeleteClick = this.handleDeleteClick.bind(this);
this.handleAddBebe = this.handleAddBebe.bind(this);
this.getData = this.getData.bind(this);
this.handleSaveBebe = this.handleSaveBebe.bind(this);
this.handleOnNombresChange = this.handleOnNombresChange.bind(this);
this.handleOnApellidosChange = this.handleOnApellidosChange.bind(this);
}
componentDidMount() {
this.getData();
}
componentDidUpdate(prevProps, prevState) {
if (this.state.has_updated === true) {
this.getData();
this.setState({ has_updated: false });
}
}
focusSocket() {
this.socket.current.focus();
}
async getData() {
let data = await fetchBebes();
this.setState({ bebes: data, is_fetching: false });
}
async handleItemClick(id) {
let selected_bebe = await fetchBebe(id);
this.setState((prevState) => {
return {
is_creating: false,
is_justRead: true,
current_bebe_id: id,
bebe: selected_bebe,
};
});
}
async handleEditClick(id) {
let selected_bebe = await fetchBebe(id);
this.setState((prevState) => {
return {
is_creating: false,
is_justRead: false,
current_bebe_id: id,
bebe: selected_bebe,
};
});
}
async handleDeleteClick(id) {
let antesBebes = [...this.state.bebes];
console.log(antesBebes);
let index = antesBebes.findIndex((i) => i.id === id);
console.log(`the index es ${index} y el id es ${id}`);
await deleteBebe(id);
antesBebes.splice(index, 1);
console.log(antesBebes);
this.setState({ bebes: [...antesBebes], has_updated: true });
//this.setState({ bebes: this.state.bebes, has_updated: true });
//console.log(antesBebes);
console.log("it was deleted...");
//window.location.reload();
//this.setState((prevState) => {
//return {
//bebes: antesBebes,
//has_updated: true,
//};
//});
//this.getData();
}
handleAddBebe() {
this.setState((prevState) => {
return { is_creating: true };
});
}
async handleSaveBebe(data) {
await addBebe(data);
await this.getData();
}
handleData(data) {
let result = JSON.parse(data);
let current_bebe = this.state.bebe;
if (current_bebe.id === result.id) {
this.setState({ bebe: result });
}
}
handleOnNombresChange(e) {
let nombres = e.target.value;
let current_bebe = this.state.bebe;
current_bebe.nombres = nombres;
this.setState({
bebe: current_bebe,
has_updated: true,
});
const socket = this.socket.current;
socket.state.ws.send(JSON.stringify(current_bebe));
}
handleOnApellidosChange(e) {
let apellidos = e.target.value;
let current_bebe = this.state.bebe;
current_bebe.apellidos = apellidos;
this.setState({
bebe: current_bebe,
has_updated: true,
});
//const socket = this.refs.socket;
const socket = this.socket.current;
socket.state.ws.send(JSON.stringify(current_bebe));
}
render() {
return (
<>
<Container>
<Row>
<Col xs="10">
<h2>Hello</h2>
</Col>
<Col>
<Button color="primary" onClick={this.handleAddBebe}>
Create a new note
</Button>
</Col>
</Row>
<Row>
<Col xs="4">
{this.state.is_fetching ? (
"Loading..."
) : (
<ListBebes
bebes={this.state.bebes}
handleItemClick={(id) => this.handleItemClick(id)}
handleEditClick={(id) => this.handleEditClick(id)}
handleDeleteClick={(id) => this.handleDeleteClick(id)}
></ListBebes>
)}
</Col>
<Col xs="8">
{this.state.is_creating ? (
<AddBebeForm handleSave={this.handleSaveBebe} />
) : (
<EditBebeForm
handleNombresChange={this.handleOnNombresChange}
handleApellidosChange={this.handleOnApellidosChange}
bebe={this.state.bebe}
soloLeer={this.state.is_justRead}
/>
)}
<Websocket
ref={this.socket}
url="ws://127.0.0.1:8000/ws/bebes"
onMessage={this.handleData.bind(this)}
/>
</Col>
</Row>
</Container>
</>
);
}
}
export default App;
Can you debug the following lines? and print [...antesBebes] | this.state.bebes and antesBebes after line 3 ?
I have some suspension about these lines, Can't debug them though because you haven't added all your components in here.
1 antesBebes.splice(index, 1);
2 console.log(antesBebes);
3 this.setState({ bebes: [...antesBebes], has_updated: true });
My recommendation is to use one of the following to manage your state in react application:
React Hooks -- recommended for small application like yours link
React Redux -- link
I found the solution. It happened that by placing code in the delete function handleDeleteClick() and also in componentDidUpdate I was messing things up.
The final code for delete is:
async handleDeleteClick(id) {
let antesBebes = [...this.state.bebes];
let index = antesBebes.findIndex((i) => i.id === id);
await deleteBebe(id);
antesBebes.splice(index, 1);
await this.setState({ bebes: antesBebes });
}
This code may have other problems but as far as the original goal was, this solve the problem.

React Native & Flatlist, array items not available (.length === 0)

I'm experience an issue in react native where when the array i have in my state is behaving very strangely. It says i have content in the array, but the array.length === 0. Here is a screenshot of my chatrooms array to show you what i mean (this is at the beginning of my FlatList component (receiving props.chatrooms array)
console.log('props.chatrooms')
console.log(props.chatrooms)
console.log(props.chatrooms.length)
I'm grabbing all the private and group chatIds, then making a call to those chatrooms to get the details of the users. Here is how i'm pulling the information:
componentDidMount() {
this.setState({ isLoading: true })
this.props.navigation.addListener('focus', () => {
//? Refactor to pull userId from Redux Store
let friendsArray = []
let chatrooms = []
let privateChatIds = []
let groupChatIds = []
AsyncStorage.getItem('userId').then(userId => {
let currentUser = {}
firebase.firestore().collection('users').doc(userId).get().then(snapshot => {
currentUser = snapshot.data()
}).then(() => this.setState({ currentUser: currentUser }))
firebase.firestore().collection('users').doc(userId).collection('friends').get().then((snapshot) => {
if (!snapshot.empty) {
snapshot.forEach((doc) => {
friendsArray.push(doc.data())
privateChatIds.push(doc.data().chatroomId)
});
} else {
console.log('Nothing to Grab')
}
}).then(() => this.setState({ friends: friendsArray }))
// get each array and put into the chatroomsArray
firebase.firestore().collection('users').doc(userId).collection('groupChatrooms').get().then((snapshot) => {
if (!snapshot.empty) {
snapshot.forEach((doc) => {
groupChatIds.push(doc.data().key)
})
}
//! will want to limit this number to like 10-15 or something
}).then(() => {
for (let i = 0; i < privateChatIds.length; i++) {
firebase.firestore().collection('chatrooms').doc('private').collection(privateChatIds[i]).doc('info').get().then((snapshot) => {
chatrooms.push(snapshot.data())
})
}
for (let i = 0; i < groupChatIds.length; i++) {
firebase.firestore().collection('chatrooms').doc('group').collection(groupChatIds[i]).doc('info').get().then((snapshot) => {
chatrooms.push(snapshot.data())
})
}
}).then(() => {
console.log('chatrooms')
console.log(chatrooms)
this.setState({ chatrooms, isLoading: false })
})
})
})
}
is my code perfect? no, probably pretty terrible performance-wise, but i'm just trying to get it to work. I figure i'm resetting the chatrooms array or something, but why would it log as an array of items i want but then immediately after log as .length === 0??? Help!
Update: I'm seeing this content render from the flatlist initially and then it disappears

(Refactor/Improve) Loop to make API calls and manupilate Array following the "no-loop-func"

Despite looking and following numerous answers here at stackoverflow,I have still failed to refactor this code to abide by the ESLint no-loop-func.
I keep getting the following warning, despite my efforts to refactor the code:
Compiled with warnings.
Function declared in a loop contains unsafe references to variable(s) 'lastResult', 'biologyBooks', 'page' no-loop-func
Here's the code:
import React from 'react';
import { apiFullCall } from '../../apiHelper';
const MyComponent = props => {
const [state, setState] = React.useState({ total: 0, biologyBooksByAuthor: [] });
let isLoaded = React.useRef(true);
const token = sessionStorage.getItem('token');
const authorID = sessionStorage.getItem('author_id');
const getBooks = async() => { // fetch items
let page = 1;
let scienceBooks, biologyBooks;
// create empty arrays to store book objects for each loop
let scienceBooks = biologyBooks = [];
// create a lastResult object to help check if there is a next page
let lastResult = { next: null };
do { // the looping - this is what I have failed to refactor
try {
await apiFullCall( // Make API calls over paginated records
'',
token,
'get',
`books/?author_id=1&page=${page}`
).then(res => {
if (res) {
const { status, body } = res;
if (status === 200 || status === 201) {
lastResult = body; // assign lastResult to pick "next"
body &&
body.results &&
body.results.map(eachBook => { // we map() over the returned "results" array
// the author with queried "author_id" writes science books;
// so we add each book (an object) into the science category
scienceBooks.push(eachBook);
// We then filter the author's biology books (from other science books)
biologyBooks = scienceBooks.filter(
({ is_biology }) =>
typeof(is_biology) === "boolean" && is_biology === true
);
return null;
}
);
// increment the page with 1 on each loop
page++;
}
}
}).catch(error => console.error('Error while fetching data:', error));
} catch (err) { console.error(`Oops, something went wrong ${err}`); }
// keep running until there's no next page
} while (lastResult.next !== null);
// update the state
setState(prevState => ({
...prevState, total: scienceBooks.length, biologyBooksByAuthor: biologyBooks,
}));
};
React.useEffect(() => { // fetch science books by author (logged in)
if (isLoaded && authorID) {
getBooks();
};
return function cleanup() {...}; // clean up API call, on unmount
}, [isLoaded, authorID]);
return (
// render the JSX code
);
}
Please note that I actually declared the said variables lastResult, biologyBooks and page outside the "do-while".
Any help or clues will be greatly appreciated.
The function the warning is referring to is the .then callback, if you're using async/await stick to it, try removing the .then part by assigning the result to a variable instead and remove the unnecessary .map, you can concatenate previous results with spread operator or .concat.
import React from 'react';
import { apiFullCall } from '../../apiHelper';
const MyComponent = props => {
const [state, setState] = React.useState({
total: 0,
scienceBooksByAuthor: [],
});
const isLoaded = React.useRef(true);
const token = sessionStorage.getItem('token');
const authorID = sessionStorage.getItem('author_id');
const getBooks = async () => {
// fetch items
let page = 1;
let scienceBooks = [];
// create a lastResult object to help check if there is a next page
let lastResult = { next: null };
do {
// the looping - this is what I have failed to refactor
try {
const res = await apiFullCall(
// Make API calls over paginated records
'',
token,
'get',
`books/?author_id=1&page=${page}`,
);
if (res) {
const { status, body } = res;
if (status === 200 || status === 201) {
lastResult = body; // assign lastResult to pick "next"
// concatenate new results
scienceBooks = [
...scienceBooks,
...((lastResult && lastResult.results) || []),
];
// increment the page with 1 on each loop
page += 1;
}
}
} catch (err) {
console.error(`Oops, something went wrong ${err}`);
}
// keep running until there's no next page
} while (lastResult.next !== null);
const biologyBooks = scienceBooks.filter(
({ is_biology }) =>
typeof is_biology === 'boolean' && is_biology === true,
);
// update the state
setState(prevState => ({
...prevState,
total: scienceBooks.length,
scienceBooksByAuthor: scienceBooks,
}));
};
React.useEffect(() => {
// fetch science books by author (logged in)
if (isLoaded && authorID) {
getBooks();
}
return function cleanup() {...}; // clean up API call, on unmount
}, [isLoaded, authorID]);
return (
// render the JSX code
);
};

ReactJS: Check if array contains value else append

I'm trying to check if a JSON response contains a value already inside an array and if it doesn't add it in. The problem I'm having is understanding how to approach this in reactjs. I'm checking before I append it but it doesn't want to work. I've tried passing in user object & user.id but these fail. The attempt below fails to compile but it should help understand what I'm trying to achieve.
Code:
componentWillMount() {
fetch('http://localhost:8090/v1/users')
.then(results => {
return results.json();
})
.then(data => {
data.map((user) => (
if(userList.hasOwnProperty(user.id)) {
userList.push({label: user.title, value: user.id})))
}
})
}
map return the resultant array, but you are not returning anything from it, you should instead use forEach Also you need to check if the userList array contains the id, for that you can use findIndex
What you need is
state = {
userList: [];
}
componentDidMount() {
fetch('http://localhost:8090/v1/users')
.then(results => {
return results.json();
})
.then(data => {
const newUserList = [...this.state.userList];
data.forEach((user) => { // use { here instead of
if(userList.findIndex(item => item.value === user.id) < 0) {
newData.push({label: user.title, value: user.id})
}
})
this.setState({userList: newUserList});
});
}
render() {
return (
{/* map over userList state and render it here */}
)
}
I'd recommend using reduce to turn the returned data into an array you'd like, then adding those values to your existing user list:
fetch('http://localhost:8090/v1/users')
.then(res => res.json())
.then(data => data.reduce((acc, user) => {
const idList = userList.map(user => user.id);
if (idList.indexOf(user.id) === -1) {
acc.push({label: user.title, value: user.id})
}
return acc;
},[]))
.then(newList => userList = [...userList, ...newList]);

Resources