How to paginate Cloud Firestore data with ReactJs - javascript

I'm working with Firebase - Cloud Firestore and at the moment I would like to paginate all the records available. I already have a list of records and what is left is some pagination for this. I'm new with Cloud Firestore, so any clarity is appreciated.
I checked the Firestore documentation (https://firebase.google.com/docs/firestore/query-data/query-cursors#paginate_a_query) and examples with ReactJS, but there is not much available.
I understand that eg:.startAt(0), .limit(10), but the question is how to paginate properly with this component called at the render method.
import React, { Component } from 'react';
import Pagination from "react-js-pagination";
import firestore from "./Firebase";
export default class DataList extends Component {
constructor(props) {
super(props);
this.state = {
dbItems: [],
currentPage: 1,
itemsPerPage: 3,
totalItemCount: 1,
activePage: 15
}
this.handlePageChange = this.handlePageChange.bind(this);
}
handlePageChange(pageNumber) {
console.log(`active page is ${pageNumber}`);
this.setState({ activePage: pageNumber });
}
async getItems() {
const { currentPage, itemsPerPage } = this.state;
const startAt = currentPage * itemsPerPage - itemsPerPage;
const usersQuery = firestore.collection('Users').orderBy("email").startAt(startAt).limit(itemsPerPage)
const snapshot = await usersQuery.get()
const items = snapshot.docs.map(doc => doc.data())
return this.setState({
dbItems: items,
totalItemCount: firestore.collection('Users').get().then(res => console.log(res.size))
})
}
componentDidMount() {
this.getItems()
}
componentDidUpdate(prevProps, prevState) {
const isDifferentPage = this.state.currentPage !== prevState.currentPage
if (isDifferentPage) this.getItems()
}
render() {
return (
<div>
{this.state.dbItems.map((users, index) => {
return (
<p key={index}>
<b>First Name:</b> {users.firstname} <br />
<b>Email:</b> {users.email}
</p>
)
})
}
<Pagination
activePage={this.state.activePage}
itemsCountPerPage={this.state.itemsPerPage}
totalItemsCount={this.state.totalItemCount}
pageRangeDisplayed={this.state.itemsPerPage}
onChange={this.handlePageChange}
/>
</div>
)
}
}
Thank you for the help!

Pagination can be achieved using startAt()
// Get Items.
async fetchUsers = () => {
// State.
const {users, usersPerPage} = this.state
// Last Visible.
const lastVisible = users && users.docs[users.docs.length - 1]
// Query.
const query = firestore.collection('Users')
.orderBy('email')
.startAfter(lastVisible)
.limit(usersPerPage)
// Users.
const users = await query.get()
// ..
return this.setState({users})
}
// Did Mount.
componentDidMount() {
this.fetchUsers()
}
// Did Update.
componentDidUpdate(prevProps, prevState) {
const isDifferentPage = this.state.currentPage !== prevState.currentPage
if (isDifferentPage) this.fetchUsers()
}

Anyone new to Firestore and Firestore Pagination with ReactJS that would be kinda confusing to understand how Pagination will work or when to trigger call to next set of documents in firestore. anyone struggle like this try my example to make some ideas and process ahead.(Im using React-Bootstrap to render UI Elements)
01 - Install Package react-infinite-scroll-component
First Install this package yarn add react-infinite-scroll-component
02 - Include Package
Include it to your file by 'import InfiniteScroll from 'react-infinite-scroll-component';' importing it
03 - Init State
initiate state with empty list array
this.state = {
list: [],
};
04 - Create Function to get first set of data and initiate it with component did mount
//component did mount will fetch first data from firestore
componentDidMount(){
this.getUsers()
}
getUsers(){
let set = this
//initiate first set
var first = set.ref.collection("users").limit(12);
first.get().then(function (documentSnapshots) {
// Get the last visible document
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
//initiate local list
const list = [];
documentSnapshots.forEach(function(doc) {
//im fetching only name and avatar url you can get any data
//from your firestore as you like
const { name, avatar_full_url } = doc.data();
//pushing it to local array
list.push({ key: doc.id, name, avatar_full_url });
});
//set state with updated array of data
//also save last fetched data in state
set.setState({ list, last: lastVisible });
});
}
05 - Create function to get balance data set
fetchMoreData = () => {
let set = this
//get last state we added from getUsers()
let last = this.state.last
var next = set.ref.collection("users").startAfter(last).limit(12);
next.get().then(function (documentSnapshots) {
// Get the last visible document
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
const list = [];
documentSnapshots.forEach(function(doc) {
//im fetching only name and avatar url you can get any data
//from your firestore as you like
const { name, avatar_full_url } = doc.data();
list.push({ key: doc.id, name, avatar_full_url });
});
//set state with updated array of data
//also save last fetched data in state
let updated_list = set.state.list.concat(list);
set.setState({ list: updated_list, last: lastVisible });
});
};
06 - Render UI
<InfiniteScroll
dataLength={this.state.list.length}
next={this.fetchMoreData}
hasMore={true}
loader={<span className="text-secondary">loading</span>}>
<Row className="mt-3">
{ this.state.list.map((single, index) => (
<Col lg={4} key={ index }>
<div>
<Image src={ single.avatar_full_url }roundedCircle width="100" />
<h2>{ single.name }</h2>
</div>
</Col>
))}
</Row>
</InfiniteScroll>

Check this example this could help anyone who trying previous / next pagination
//initial state
const [list, setList] = useState([]);
const [page, setPage] = useState(1);
//loading initial data
useEffect(() => {
const fetchData = async () => {
await firebase.firestore().collection('users')
.orderBy('created', 'desc') //order using firestore timestamp
.limit(5) //change limit value as your need
.onSnapshot(function(querySnapshot) {
var items = [];
querySnapshot.forEach(function(doc) {
items.push({ key: doc.id, ...doc.data() });
});
setList(items);
})
};
fetchData();
}, []);
After loading initial data use following function for next button trigger
//next button function
const showNext = ({ item }) => {
if(list.length === 0) {
//use this to show hide buttons if there is no records
} else {
const fetchNextData = async () => {
await firebase.firestore().collection('users')
.orderBy('created', 'desc') //order using firestore timestamp
.limit(5) //change limit value as your need
.startAfter(item.created) //we pass props item's first created timestamp to do start after you can change as per your wish
.onSnapshot(function(querySnapshot) {
const items = [];
querySnapshot.forEach(function(doc) {
items.push({ key: doc.id, ...doc.data() });
});
setList(items);
setPage(page + 1) //in case you like to show current page number you can use this
})
};
fetchNextData();
}
};
Then Previous button function
//previous button function
const showPrevious = ({item}) => {
const fetchPreviousData = async () => {
await firebase.firestore().collection('users')
.orderBy('created', 'desc')
.endBefore(item.created) //this is important when we go back
.limitToLast(5) //this is important when we go back
.onSnapshot(function(querySnapshot) {
const items = [];
querySnapshot.forEach(function(doc) {
items.push({ key: doc.id, ...doc.data() });
});
setList(items);
setPage(page - 1)
})
};
fetchPreviousData();
};
at the end create list view & two buttons like this
{
//list doc's here this will come inside return (place this code inside table)
list.map((doc) => (
<tr key={doc.key}>
<td>{ doc.name }</td>
<td>{ doc.age }</td>
<td>{ doc.note }</td>
</tr>
))
}
{
//show previous button only when we have items
//pass first item to showPrevious function
page === 1 ? '' :
<Button onClick={() => showPrevious({ item: list[0] }) }>Previous</Button>
}
{
//show next button only when we have items
//pass last item to showNext function
list.length < 5 ? '' :
<Button onClick={() => showNext({ item: list[list.length - 1] })}>Next</Button>
}
That's it check my code comments where you can change as per your need. this is what happens when you paginate using Firebase FireStore. you can use create custom hook to reuse these component as per your need.
Hope this could help someone so i made a gist check it here

here AddTable and AddForm is adding table and add form to fill data in table...
import React, { useEffect, useState } from "react";
import Button from "react-bootstrap/Button";
import Pagination from "react-bootstrap/Pagination";
import AddTable from "../management/AddTable";
import AddForm from "../management/AddSuperAdminForm";
import {
where,
getDocs,
collection,
query,
orderBy,
startAfter,
limit,
endBefore,
limitToLast,
} from "firebase/firestore";
import { db_firestore } from "../../../firebase.config";
const SuperAdmin = () => {
const [tableDataArray, setTableDataArray] = useState();
const [show, setShow] = useState(false);
const [editId, setEditId] = useState("");
const [oldUid, setOldUid] = useState("");
const [lastVisible, setLastVisible] = useState();
const [prevVisible, setPrevVisible] = useState();
const handleClose = () => {
setShow(false);
setEditId("");
};
const handleShow = () => {
setShow(true);
setEditId("");
};
let tempdata;
let pageSize = 3;
let q = query(
collection(db_firestore, "users"),
where("role", "==", "superadmin"),
orderBy("timestamps", "desc"),
limit(pageSize)
);
function nextPage(lastVisible) {
q = query(
collection(db_firestore, "users"),
where("role", "==", "superadmin"),
orderBy("timestamps", "desc"),
startAfter(lastVisible),
limit(pageSize)
);
}
function prevPage(firstVisible) {
q = query(
collection(db_firestore, "users"),
where("role", "==", "superadmin"),
orderBy("timestamps", "desc"),
endBefore(firstVisible),
limitToLast(pageSize + 1)
);
}
const newfun = async () => {
const querySnapshot = await getDocs(q);
tempdata = [];
// Get the last visible document
setLastVisible(querySnapshot.docs[querySnapshot.docs.length - 1]);
// Get the prev visible document
setPrevVisible(querySnapshot.docs[0]);
querySnapshot.forEach((doc) => {
const { name, email, uid } = doc.data();
tempdata.push([name, email, uid, doc.id]);
});
console.log("SuperAdmin...");
setTableDataArray(tempdata);
};
useEffect(() => {
newfun();
// setInterval(() => { // if you want to get new update after some secound
// newfun();
// }, 10000);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
<Button
className="d-block mx-auto my-2"
variant="primary"
onClick={handleShow}
>
Add SuperAdmin
</Button>
{/* -----> AddTable <------
Index will generate Automatic In Table.
Always keep action end of the table.
*/}
{tableDataArray ? (
<AddTable
tableHeaders={["Name", "Email", "uid", "Action"]}
tableData={tableDataArray}
fetchNew={newfun}
setEditId={setEditId}
setShow={setShow}
setOldUid={setOldUid}
/>
) : (
""
)}
<AddForm
fetchNew={newfun}
show={show}
setShow={setShow}
handleClose={handleClose}
editId={editId}
oldUid={oldUid}
/>
<Pagination className="float-end">
<Pagination.Item
className="shadow-none"
size="lg"
onClick={() => {
prevPage(prevVisible);
newfun();
}}
>
Previous
</Pagination.Item>
<Pagination.Item
className="shadow-none"
size="lg"
onClick={() => {
nextPage(lastVisible);
newfun();
}}
>
Next
</Pagination.Item>
</Pagination>
</div>
);
};
export default SuperAdmin;

Use startAt() or startAfter() for that
firestore
.collection("Users")
.startAt(0)
.limit(10)
.get()

Related

React State / DOM Not Updating When All Items Deleted

I'm building an app with React and Firebase Realtime Database. Objects are added to an array and sent to the database.
The arrays are updated in React and the result is sent to the database.
The functionality to remove items/objects from the list works fine when there are more than one (i.e. button clicked, database, DOM and state updated immediately).
However, whenever there's one item left and you click its delete button, it's deleted from the database but the state and React DOM aren't updated - you have to refresh the page for it to be removed.
I've tried using different methods to update the database in case it triggered a different response but that didn't work - any ideas would be greatly appreciated:
import React, {useState, useEffect} from 'react'
import { Button } from "react-bootstrap";
import Exercise from "./Exercise";
import AddNewWorkout from "./AddNewWorkout";
import { v4 as uuidv4 } from "uuid";
import WorkoutComponent from './WorkoutComponent';
import AddNewExercise from "./AddNewExercise"
import { database, set, ref, onValue, update } from "../firebase"
const Dashboard = ({user}) => {
const [selectedWorkout, setSelectedWorkout] = useState();
const [workouts, setWorkouts] = useState([])
const [creatingNewWorkout, setCreatingNewWorkout] = useState(false);
const [addingNewExercise, setAddingNewExercise] = useState(false)
function selectWorkout(number) {
const selection = [...workouts].filter(workout => number == workout.id);
setSelectedWorkout(selection[0])
}
function toggleNewWorkoutStatus(e) {
e.preventDefault()
setCreatingNewWorkout(creatingNewWorkout => !creatingNewWorkout)
}
function toggleNewExerciseStatus() {
setAddingNewExercise(addingNewExercise => !addingNewExercise)
}
function writeData() {
const newWorkouts = [...workouts]
const workoutTitle = document.getElementById("workoutTitle").value || new Date(Date.now()).toString()
const workoutDate = document.getElementById("workoutDate").value;
newWorkouts.push({
id: uuidv4(),
title: workoutTitle,
date: workoutDate,
exercises: []
})
set(ref(database, `${user.uid}/workouts/`), newWorkouts )
}
function addWorkoutToListDB(e) {
e.preventDefault();
writeData(user.uid)
}
function removeWorkoutFromList(id) {
const newWorkouts = [...workouts].filter(workout => id !== workout.id);
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
function addExerciseToWorkout(e) {
e.preventDefault();
if (selectedWorkout === undefined) {
alert("No workout selected")
return
}
const newWorkouts = [...workouts]
const exerciseID = uuidv4();
const exerciseName = document.getElementById("exerciseName").value
const exerciseSets = document.getElementById("exerciseSets").value
const exerciseReps = document.getElementById("exerciseReps").value
const exerciseWeight = document.getElementById("exerciseWeight").value
const exercisetTarget = document.getElementById("exercisetTarget").checked
const exerciseNotes = document.getElementById("exerciseNotes").value;
const newExercise = {
id: exerciseID,
name: exerciseName,
sets: exerciseSets,
reps: exerciseReps,
weight: `${exerciseWeight}kg`,
target: exercisetTarget,
notes: exerciseNotes,
}
for (let key of newWorkouts) {
if (key.id === selectedWorkout.id) {
if (key.exercises) {
key.exercises.push(newExercise)
} else {
key.exercises = [newExercise]
}
}
}
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
function removeExerciseFromWorkout(id) {
const newWorkouts = [...workouts];
for (let workout of newWorkouts) {
if(selectedWorkout.id === workout.id) {
if (!workout.exercises) {return}
workout.exercises = workout.exercises.filter(exercise => exercise.id !== id)
}
}
const newSelectedWorkout = {...selectedWorkout}
newSelectedWorkout.exercises = newSelectedWorkout.exercises.filter(exercise => exercise.id !== id)
setSelectedWorkout(newSelectedWorkout)
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
useEffect(() => {
function getWorkoutData() {
const dbRef = ref(database, `${user.uid}`);
onValue(dbRef, snapshot => {
if (snapshot.val()) {
console.log(snapshot.val().workouts)
setWorkouts(workouts => workouts = snapshot.val().workouts)
}
}
)
}
getWorkoutData()
},[])
return (
<div>
{creatingNewWorkout && <AddNewWorkout addWorkoutToListDB={addWorkoutToListDB} toggleNewWorkoutStatus={toggleNewWorkoutStatus} /> }
<div id="workoutDiv">
<h2>Workouts</h2><p>{selectedWorkout ? selectedWorkout.title : "No workout selected"}</p>
<Button type="button" onClick={toggleNewWorkoutStatus} className="btn btn-primary">Add New Workout</Button>
{workouts && workouts.map(workout => <WorkoutComponent key={workout.id} removeWorkoutFromList={removeWorkoutFromList} selectWorkout={selectWorkout} workout={workout}/> )}
</div>
<div>
<h2>Exercise</h2>
{addingNewExercise && <AddNewExercise selectedWorkout={selectedWorkout} addExerciseToWorkout={addExerciseToWorkout} toggleNewExerciseStatus={toggleNewExerciseStatus}/> }
<Button type="button" onClick={toggleNewExerciseStatus} className="btn btn-primary">Add New Exercise</Button>
{selectedWorkout && selectedWorkout.exercises && selectedWorkout.exercises.map(exercise => <Exercise removeExerciseFromWorkout={removeExerciseFromWorkout} key={exercise.id} exercise={exercise}/>)}
</div>
</div>
)
}
export default Dashboard
If it helps, the data flow I'm working to is:
New array copied from state
New array updated as necessary
New array sent to database
Database listener triggers download of new array
New array saved to state
I have tried to use different methods (set, update and remove) in case that triggered the onValue function.
I have also tried to send null values and deleting empty nodes if the array that will be sent to the db is empty.
The above methods didn't have any impact, there was still a problem with the last array element that was only resolved by refreshing the browser.
I have tried to remove the array dependency and add the workout state as a dependency, resulting in the following error: "Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."
I think I understand where the issue was:
In the useEffect call, I set up the state to only be updated if the value in returned from the database was null (to prevent an error I ran into). However, this meant that state wasn't being updated at all when I deleted the last item from the array.
I appear to have fixed this by adding an else clause.
useEffect(() => {
function getWorkoutData() {
const dbRef = ref(database, `${user.uid}`);
onValue(dbRef, snapshot => {
if (snapshot.val()) {
console.log(snapshot.val().workouts)
setWorkouts(workouts => workouts = snapshot.val().workouts)
} else {
setWorkouts(workouts => workouts = [])
}
}
)
}
getWorkoutData()
},[])
`````

I want to paginate data. It is working fine but when I search for specific data it always shows in the first page

What I want is to paginate my data but the problem is when I'm searching for specific data if I'm on page 3 the result shows on page 1 always and I can't see anything because I was on page no 3. I want to go to page 1 automatically when I'm searching for something. Also when I press the next button if there is no data at all it still increases the page number.
Here is my code:
import { React, useState, useEffect } from "react";
import UpdateDialogue from "./UpdateDialogue";
function List(props) {
const API_URL = "http://dummy.restapiexample.com/api/v1/employees";
const [EmployeeData, setEmployeeData] = useState([]);
const [pageNumber, setPageNumber] = useState(1);
const [postNumber] = useState(8);
const currentPageNumber = pageNumber * postNumber - postNumber;
const handlePrev = () => {
if (pageNumber === 1) return;
setPageNumber(pageNumber - 1);
};
const handleNext = () => {
setPageNumber(pageNumber + 1);
};
useEffect(() => {
fetch(API_URL)
.then((response) => response.json())
.then((response) => {
setEmployeeData(response.data);
})
.catch((err) => {
console.error(err);
});
}, []);
const filteredData = EmployeeData.filter((el) => {
if (props.input === "") {
return el;
} else {
return el.employee_name.toLowerCase().includes(props.input)
}
});
const paginatedData = filteredData.splice(currentPageNumber, postNumber);
return (
<>
<ul>
{paginatedData.map((user) => (
<UpdateDialogue user={user} key={user.id} />
))}
</ul>
<div>Page {pageNumber} </div>
<div>
<button style={{marginRight:10}} onClick={handlePrev}>prev</button>
<button onClick={handleNext}>next</button>
</div>
</>
);
}
export default List;
Maybe with a useEffect on your input:
useEffect(() => {
if (props.input) {
setPageNumber(1);
}
}, [props.input]);
That way, whenever your input changes, your page number is set to 1.

UseContext Doesn't Re-render when an component value is updated

I'm using React's context api to store an array of Favorite products.The favorites Array is filled with Boolean Value False and turned to true based on id of the products.There is collection page which displays productCards having an addtoFavorite button,Upon clicking the button disables but if the product is already present in favorites it has to disabled.
Now it works perfectly fine for the 1st Page , disabling only favorite products with the array containing values true and false based on index of the products but when navigated to another page it disables other products at the same index even though the favorites array is updated to have all values as false.If we Navigate Back or move to another page its value now remains false in the array.It looks as if UseContext updates the value of the array late or doesn't rerender on change.
I have tried implementing other stuffs but it still wouldn't re-render when the array was changed.
Here's the FavoritesContext:
const FavoritesContext = React.createContext({
addToFavorites: (id,index) => {},
favorites:[],
storedFavorites:(data) => {}
});
export const FavoritesContextProvider = (props) => {
const authCtx = useContext(AuthContext)
const token = authCtx.token;
const userId = authCtx.userId;
const [favorites,setFavorites] = useState([]);
// To retrieve stored favorites from FireBase
const retrieveStoredFavorites = (data) => {
let fav = new Array(data.length).fill(false);
setFavorites(fav);
let queryParams = '?auth=' + token + '&orderBy="userId"&equalTo="' + userId + '"';
axiosInstance.get('/Favorites.json' + queryParams)
.then((response) => {
let fetchProductData = [];
for (let key in response.data) {
fetchProductData.push({
...response.data[key],
productId: key,
});
}
let favoriteList = [];
//To find if the product is present in the Fetched Favorite products List
for(let i=0;i<data.length;i++){
let ids = data[i].id
let favoriteProducts = !!fetchProductData.find((product)=>product.id==ids)
favoriteList.push(favoriteProducts)
}
//console.log(favoriteList)
setFavorites(favoriteList)
});
}
//Add to Favorites
const addTofavoritesHandler = (Product,index) => {
axiosInstance
.post('Favorites.json?auth='+token,Product)
.then((response) => {
//console.log("SUCCESS")
})
.catch((error) => console.log(error));
let favoriteOnes = [...favorites];
favoriteOnes[index] = true;
setFavorites(favoriteOnes);
};
const contextValue = {
addToFavorites:addTofavoritesHandler,
favorites:favorites,
storedFavorites:retrieveStoredFavorites
};
return (
<FavoritesContext.Provider value={contextValue}>
{props.children}
</FavoritesContext.Provider>
);
};
export default FavoritesContext;
Now here is the Collection Page
const CollectionPage = () => {
const classes = useStyles();
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [productsPerPage] = useState(9);
const [loading, setLoading] = useState(false);
const { enqueueSnackbar } = useSnackbar();
const authCtx = useContext(AuthContext);
const token = authCtx.token;
const userId = authCtx.userId;
const favoriteCtx = useContext(FavoritesContext)
const favorites = favoriteCtx.favorites
//To Display the Products in Main Content
const DisplayProductsHandler = (Data) => {
//Get value of FirstPageNumber and LastPageNumber
const indexOfLastPage = currentPage * productsPerPage;
const indexOfFirstPage = indexOfLastPage - productsPerPage;
//console.log("[Products]")
const productData = Data.slice(indexOfFirstPage, indexOfLastPage);
favoriteCtx.storedFavorites(productData)
//console.log(productData);
const updatedProductData = productData.map((product,index) => {
return (
<ProductCard
Link={`/Info/${product.id}`}
key={product.id}
Title={product.productName}
Image={product.productImage}
Value={product.price}
addToFavorites={() => addTofavoritesHandler(product,index)}
addToCart={() => addToCartHandler(product)}
disableFavoriteButton={favorites[index]}
/>
);
});
setProducts(updatedProductData);
};
//Display the Products from DisplayProductHandler
useEffect(() => {
setLoading(true);
//Scroll To Top When Reloaded
window.scrollTo(0, 0);
//To Display the Products
if (filteredProducts.length === 0) {
DisplayProductsHandler(ProductData);
} else {
DisplayProductsHandler(filteredProducts);
}
setLoading(false);
}, [currentPage, filteredProducts]);
//Add to Favorites Handler
const addTofavoritesHandler =(likedProduct,index) => {
setLoading(true);
let updatedLikedProduct = {
...likedProduct,
userId: userId,
};
favoriteCtx.addToFavorites(updatedLikedProduct,index)
//To Display ADDED TO FAVORITES Message using useSnackbar()
enqueueSnackbar("ADDED TO FAVORITES", { variant: "success" })
setLoading(false);
};
I need it to re-render every time the array in context is updated.

useEffect being triggered multiple times and I do not know what to change

Last edit of the night. Tried to clean some things up to make it easier to read. also to clarify what is going on around the useEffect. Because I am running react in strict mode everything gets rendered twice. The reference around the useEffect makes sure it only gets rendered 1 time.
Db is a firebase reference object. I am grabbing a list of league of legends games from my database.
one I have all my games in the snapshot variable, I loop through them to process each game.
each game contains a list of 10 players. using a puuId I can find a specific player. We then pull the data we care about in addChamp.
The data is then put into a local map. We continue to update our local map untill we are done looping through our database data.
After this I attempt to change our state variable in the fetchMatches function.
My issue now is that I am stuck in an infinite loop. I think this is because I am triggering another render after the state gets changed.
import { useState, useEffect, /*useCallback,*/ useRef } from 'react'
import Db from '../Firebase'
const TotGenStats = ({ player }) => {
const [champs, setChamps] = useState(new Map())
var init = new Map()
var total = 0
console.log("entered stats")
const addChamp = /*useCallback(*/ (item) => {
console.log("enter add champ")
var min = item.timePlayed/60
//var sec = item.timePlayed%60
var kda = (item.kills + item.assists)/item.deaths
var dub = 0
if(item.win){
dub = 1
}
var temp = {
name: item.championName,
avgCs: item.totalMinionsKilled,
csMin: item.totalMinionsKilled/min,
kds: kda,
kills: item.kills,
deaths: item.deaths,
assists: item.assists,
wins: dub,
totalG: 1
}
init.set(item.championName, temp)
//setChamps(new Map(champs.set(item.championName, temp)))
}//,[champs])
const pack = /*useCallback( /*async*/ (data) => {
console.log("enter pack")
for(const item of data.participants){
//console.log(champ.assists)
if(item.puuid === player.puuid){
console.log(item.summonerName)
if(init.has(item.championName)){//only checking init??
console.log("update champ")
}
else{
console.log("add champ")
/*await*/ addChamp(item)
}
}
}
}/*,[addChamp, champs, player.puuid])*/
const fetchMatches = async () => {
console.log("enter fetch matches")
Db.collection("summoner").doc(player.name).collection("matches").where("queueId", "==", 420)
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
//console.log("loop")
console.log(doc.id, " => ", doc.data());
console.log("total: ", ++total);
await pack(doc.data());
});
})
.then( () => {
setChamps(init)
})
.catch((error) => {
console.log("error getting doc", error);
});
}
const render1 = useRef(true)
useEffect( () => {
console.log("enter use effect")
if(render1.current){
render1.current = false
}
else{
fetchMatches();
}
})
return(
<div>
<ul>
{[...champs.keys()].map( k => (
<li key={k}>{champs.get(k).name}</li>
))}
</ul>
</div>
)
}
export default TotGenStats
Newest Version. no longer infinitly loops, but values do not display/render.
import { useState, useEffect } from 'react'
import Db from '../Firebase'
const TotGenStats = ({ player }) => {
const [champs, setChamps] = useState(new Map())
var total = 0
console.log("entered stats")
const addChamp = /*useCallback(*/ (item) => {
console.log("enter add champ")
var min = item.timePlayed/60
//var sec = item.timePlayed%60
var kda = (item.kills + item.assists)/item.deaths
var dub = 0
if(item.win){
dub = 1
}
var temp = {
name: item.championName,
avgCs: item.totalMinionsKilled,
csMin: item.totalMinionsKilled/min,
kds: kda,
kills: item.kills,
deaths: item.deaths,
assists: item.assists,
wins: dub,
totalG: 1
}
return temp
}
useEffect(() => {
var tempChamp = new Map()
Db.collection("summoner").doc(player.name).collection("matches").where("queueId","==",420)
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
console.log(doc.id," => ", doc.data());
console.log("total: ", ++total);
for(const person of doc.data().participants){
if(player.puuid === person.puuid){
console.log(person.summonerName);
if(tempChamp.has(person.championName)){
console.log("update ", person.championName);
//add update
}else{
console.log("add ", person.championName);
var data = await addChamp(person);
tempChamp.set(person.championName, data);
}
}
}
})//for each
setChamps(tempChamp)
})
},[player.name, total, player.puuid]);
return(
<div>
<ul>
{[...champs.keys()].map( k => (
<li key={k}>{champs.get(k).name}</li>
))}
</ul>
</div>
)
}
export default TotGenStats
useEffect will be called only once when you will not pass any argument to it and useEffect works as constructor hence its not possible to be called multiple times
useEffect( () => {
},[])
If you pass anything as argument it will be called whenever that argument change is triggered and only in that case useEffect will be called multiple times.
useEffect( () => {
},[arg])
Though whenever you update any state value in that case component will re-render. In order to handle that situation you can use useCallback or useMemo.
Also for map operation directly doing it on state variable is not good idea instead something like following[source]:
const [state, setState] = React.useState(new Map())
const add = (key, value) => {
setState(prev => new Map([...prev, [key, value]]))
}
I have made some edits to your latest code try following:
import { useState, useEffect, useRef } from "react";
import Db from "../Firebase";
const TotGenStats = ({ player }) => {
const [champs, setChamps] = useState(new Map());
const addChamp = (item) => {
let min = item.timePlayed / 60;
let kda = (item.kills + item.assists) / item.deaths;
let dub = null;
if (item.win) {
dub = 1;
} else {
dub = 0;
}
let temp = {
name: item.championName,
avgCs: item.totalMinionsKilled,
csMin: item.totalMinionsKilled / min,
kds: kda,
kills: item.kills,
deaths: item.deaths,
assists: item.assists,
wins: dub,
totalG: 1,
};
setChamps((prev) => new Map([...prev, [item.championName, temp]]));
};
const pack = (data) => {
for (const item of data.participants) {
if (item.puuid === player.puuid) {
if (!champs.has(item.championName)) {
addChamp(item);
}
}
}
};
const fetchMatches = async () => {
Db.collection("summoner")
.doc(player.name)
.collection("matches")
.where("queueId", "==", 420)
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
await pack(doc.data());
});
})
.catch((error) => {});
};
const render1 = useRef(true);
useEffect(() => {
fetchMatches();
});
return (
<div>
<ul>
{[...champs.keys()].map((k) => (
<li key={k}>{champs.get(k).name}</li>
))}
</ul>
</div>
);
};
export default TotGenStats;

A filtered list in React flashes for a split second the whole list while clicking on an item

Hi I'm quite new to React and I am making a list of dishes which you can upvote. The dishes are fetched from Firebase and displayed as a list with a dropdown filter so that you can filter on a food category.
Filtering works fine and upvoting works as well when the app loads for the first time, however when the list is filtered and you click on the upvote button the whole list with all categories flashes for a split second before it updates the number of votes and displaying the filtered list again. How can I fix this?
The code looks like this:
App:
function App() {
const [dishes, setDishes] = useState([]);
const [category, setCategory] = useState([]);
useEffect(() => {
// This code fires when the app loads
let collection = firebase.firestore().collection("dishes");
if ((category === "All") | (category.length === 0)) {
collection = collection;
} else {
collection = collection.where("category", "==", category);
}
collection.onSnapshot((snapshot) => {
const newData = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setDishes(newData);
});
}, [category]);
return (
<div className="justify-center sm:mx-44 mt-10 ">
<h1 className="text-4xl mt-10 mx-4 ">Snibble</h1>
<h5 className=" mx-4 mb-10">Rate your favorite snibble dish!</h5>
<SearchBar
dishes={dishes}
setCategory={setCategory}
/>
{dishes.map((dish) => (
<Dish dish={dish} key={dish.id} />
))}
</div>
);
}
export default App;
The Dish component has the following functionality and is attached to the upvote button with an onnClick:
function handleVote() {
const voteRef = firebase.firestore().collection("dishes").doc(dish.id);
voteRef.get().then((doc) => {
if (doc.exists) {
//console.log(doc.data().dish);
const previousVotes = doc.data().stars;
const newVote = previousVotes + 1;
voteRef.update({ stars: newVote });
}
});
}
The Searchbar component looks like this:
<Select
//defaultValue={categoriesList(dishes)[0]}
placeholder="Select Category"
onChange={(e) => setCategory(e.value)}
//value={'All'}
options={categories}/>
You need to return collection.onSnapshot(...) from useEffect() to unsubscribe from the current collection's changes when category changes.
What you're seeing now is onSnapshot being called for both the unfiltered and filtered collection simultaneously when voteRef.update() commits to firebase.
collection.onSnapshot is a listener that fires every time the collection is updated. When your handleVote function is called, the collection.onSnapshot listener fires too. You should use the collection.get function in your useEffect instead, so that it only happens when your useEffect fires.
useEffect(() => {
// This code fires when the app loads
let collection = firebase.firestore().collection("dishes");
if ((category === "All") | (category.length === 0)) {
collection = collection;
} else {
collection = collection.where("category", "==", category);
}
collection.get().then((snapshot) => {
const newData = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setDishes(newData);
});
}, [category]);

Categories