This is my first time to develop a react application.
I am encountering problem when retrieving the checkedMap useState hook in react that does not update its status whenever the checkbox is checked then unchecked. The status seems saves the entries checked regardless if the next instance it was already unchecked.
I accessed the value of by: Array.from(checkedMap.keys()));
Scenario 1
Checkbox A - ticked
Checkbox B - ticked
checkedMap= id of Checkbox A and B
Scenario 2
Checkbox A - ticked
Checkbox B - unticked
checkedMap= still id of Checkbox A and B
//should be id of Checkbox A only
Thank you very much for your help.
import React, { useState, useEffect, useRef } from "react";
// Simulate a server
const getServerData = async ({ filters, sortBy, pageSize, pageIndex }) => {
await new Promise(resolve => setTimeout(resolve, 500));
// Ideally, you would pass this info to the server, but we'll do it here for convenience
const filtersArr = Object.entries(filters);
// Get our base data
const res = await axios.get(
`url here`
);
let rows = res.data;
// Apply Filters
if (filtersArr.length) {
rows = rows.filter(row =>
filtersArr.every(([key, value]) => row[key].includes(value))
);
}
// Apply Sorting
if (sortBy.length) {
const [{ id, desc }] = sortBy;
rows = [...rows].sort(
(a, b) => (a[id] > b[id] ? 1 : a[id] === b[id] ? 0 : -1) * (desc ? -1 : 1)
);
}
// Get page counts
const pageCount = Math.ceil(rows.length / pageSize);
const rowStart = pageSize * pageIndex;
const rowEnd = rowStart + pageSize;
// Get the current page
rows = rows.slice(rowStart, rowEnd);
return {
rows,
pageCount
};
};
export default function({ infinite }) {
const [checkedMap, setCheckedMap] = useState(new Map());
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const currentRequestRef = useRef();
let newMap = new Map();
const fetchData = async () => {
setLoading(true);
// We can use a ref to disregard any outdated requests
const id = Date.now();
currentRequestRef.current = id;
// Call our server for the data
const { rows, pageCount } = await getServerData({
filters,
sortBy,
pageSize,
pageIndex
});
// If this is an outdated request, disregard the results
if (currentRequestRef.current !== id) {
return;
}
// Set the data and pageCount
setData(rows);
setState(old => ({
...old,
pageCount
}));
rows.forEach(row => newMap.set(row, false));
//setCheckedMap(newMap);
setLoading(false);
};
const handleCheckedChange = transaction_seq => {
let modifiedMap = checkedMap;
modifiedMap.set(transaction_seq, !checkedMap.get(transaction_seq));
setCheckedMap(modifiedMap);
};
const columns = [
{
Header: "Transaction(s)",
className: "left",
columns: [
{
id: "checkbox",
accessor: "checkbox",
Cell: ({ row }) => {
return (
<input
type="checkbox"
className="checkbox"
checked={checkedMap.get(row.original.transaction_seq)}
onChange={() =>
handleCheckedChange(row.original.transaction_seq)
}
/>
);
},
const state = useTableState({ pageCount: 0 });
const [{ sortBy, filters, pageIndex, pageSize }, setState] = state;
const paginationButtons = (
<React.Fragment>
<Button onClick={() => reprocessConfirmation()}>Reprocess</Button>
<Button onClick={() => reprocessConfirmation()}>View Details</Button>
</React.Fragment>
);
function reprocessConfirmation() {
let confirmation = window.confirm(
"Do you want to reprocess transaction sequence " +
Array.from(checkedMap.keys())
);
if (confirmation === true) console.log(Array.from(checkedMap.keys()));
else console.log("CANCEL");
}
function updateConfirmation() {
let confirmation = window.confirm("Do you want to update transaction");
if (confirmation === true) console.log("OK");
else console.log("CANCEL");
}
// When sorting, filters, pageSize, or pageIndex change, fetch new data
useEffect(() => {
fetchData();
}, [sortBy, filters, pageIndex, pageSize]);
return (
<React.Fragment>
<MyTable
{...{
data,
checkedMap,
paginationButtons,
columns,
infinite,
state, // Pass the state to the table
loading,
manualSorting: true, // Manual sorting
manualFilters: true, // Manual filters
manualPagination: true, // Manual pagination
disableMultiSort: true, // Disable multi-sort
disableGrouping: true, // Disable grouping
debug: true
}}
/>
</React.Fragment>
);
}
Since you are just toggling the value on check and uncheck, the key will still be present when the checkbox is unchecked, You either must delete the key or look for a true value in the map instead of just checking if the key is present or not.
Code snippet for second solution
function reprocessConfirmation() {
const keys = [];
checkedMap.forEach((value, key) => {
if(value) keys.push[key]
})
let confirmation = window.confirm(
"Do you want to reprocess transaction sequence " +
keys
);
if (confirmation === true) console.log(keys);
else console.log("CANCEL");
}
Related
I have a table where I'm setting data inside a useEffect from an api. My filter logic iterates through the "rows" variable which is being set inside this useEffect. However, every-time the user searches via an input which has an onChange event the useEffect setRows I believe is setting the data over and over again.
What would be a better way to set the data so it doesn't conflict with my filtering logic?
//State
const [documents, setDocuments] = useState<IDocument[]>([]);
const [rows, setRows] = useState<Data[]>([]);
//useEffect to setData
useEffect(() => {
//setDocuments from claimStore when component mounts
setDocuments(claimsStore.getIncomingDocuments());
//setRows from documents when component mounts
setRows(
documents.map((document) =>
createData(
document.documentAuthor ?? '',
document.documentMetadataId.toLocaleString(),
document.documentMetadataId.toLocaleString(),
document.documentName ?? '',
document.documentSource ?? '',
document.documentType,
document.featureId ?? '',
document.mimeType,
document.uploadDateTime,
),
),
);
}, [claimsStore, documents]);
//Filter logic that updates rows as user input values captured
const filterBySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const newFilters = { ...filters, [name]: value };
//Update filters with user input
setFilters(newFilters);
//Filter documents based on user input
const updatedList = rows.filter((document) => {
return (
document.documentAuthor.toLowerCase().includes(filters.documentAuthor.toLowerCase()) &&
document.documentName.toLowerCase().includes(filters.documentName.toLowerCase()) &&
document.documentSource.toLowerCase().includes(filters.documentSource.toLowerCase()) &&
document.documentType.includes(filters.documentType === 'All' ? '' : filters.documentType) &&
document.featureId.includes(filters.featureId)
);
});
//Trigger render with updated values
setRows(updatedList);
};
Use of filterBySearch:
<TableCell align={'center'} className={classes.tableCell}>
<input
value={filters.featureId}
onChange={(e) => filterBySearch(e)}
name="featureId"
className={classes.inputCell}
/>
</TableCell>
This is one of the things useMemo is good for: Have an array of filtered rows, that you update as necessary when rows or filters changes:
const [documents, setDocuments] = useState<IDocument[]>([]);
const [rows, setRows] = useState<Data[]>([]);
// ...
const filteredRows = useMemo(
() => rows.filter((document) => (
document.documentAuthor.toLowerCase().includes(filters.documentAuthor.toLowerCase()) &&
document.documentName.toLowerCase().includes(filters.documentName.toLowerCase()) &&
document.documentSource.toLowerCase().includes(filters.documentSource.toLowerCase()) &&
document.documentType.includes(filters.documentType === 'All' ? '' : filters.documentType) &&
document.featureId.includes(filters.featureId)
)),
[rows, filters]
);
Then display filteredRows, not rows.
With that change, filterBySearch just sets the filter, it doesn't actually do the filtering:
const filterBySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const newFilters = { ...filters, [name]: value };
//Update filters with user input
setFilters(newFilters);
};
useMemo will only call your callback when either rows or filters changes; otherwise, it'll just return the previous filtered array.
Here's a simplified demo — it shows words filtered by whatever you type in the filter, and randomly adds a word once every couple of seconds (this demonstrates that the filtering is repeated when the filter changes or when the rows change):
const { useState, useEffect, useRef, useMemo } = React;
const words = "one two three four five six seven eight nine ten".split(" ");
let nextRowId = 1;
const Example = () => {
const [rows, setRows] = useState(
words.slice(0, 5).map((value) => ({ id: nextRowId++, value }))
);
const [filter, setFilter] = useState("");
const filteredRows = useMemo(() => {
console.log(`Filtering rows`);
if (!filter) {
return rows;
}
return rows.filter((row) => row.value.includes(filter));
}, [rows, filter]);
useEffect(() => {
let handle;
tick();
function tick() {
handle = setTimeout(() => {
const value = words[Math.floor(Math.random() * words.length)];
console.log(`Adding "${value}"`);
setRows((rows) => [...rows, { id: nextRowId++, value }]);
tick();
}, 2000);
}
return () => {
clearTimeout(handle);
};
}, []);
const filterChange = ({ currentTarget: { value } }) => {
console.log(`Setting filter to "${value}"`);
setFilter(value);
};
return (
<div>
<div>
Filter: <input type="text" value={filter} onChange={filterChange} />
</div>
Rows - showing {filteredRows.length} of {rows.length} total:
<div>
{filteredRows.map((row) => (
<div key={row.id}>{row.value}</div>
))}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
React's documentation says that useMemo is just for performance enhancement, it isn't a semantic guarantee (basically, React may call your callback even when nothing has actually changed). If you want a semantic guarantee, you can do it with a ref. You can even wrap that up into a hook that provides the semantic guarantee — I call it useHardMemo:
const useHardMemo = (fn, deps) => {
const ref = useRef(null);
let { current } = ref;
if (current) {
// Consistency check
if (
(deps && !current.deps) ||
(!deps && current.deps) ||
(deps && deps.length !== current.deps.length)
) {
throw new Error(
`Invalid call to useHardMemo, the dependency array must either always be present ` +
`or always be absent, and if present must always have the same number of items.`
);
}
}
if (!current || !deps?.every((dep, index) => Object.is(current.deps?.[index], dep))) {
ref.current = current = {
deps: deps?.slice(),
value: fn(),
};
}
return current.value;
};
Live Example:
const { useState, useEffect, useRef, createElement } = React;
const useHardMemo = (fn, deps) => {
const ref = useRef(null);
let { current } = ref;
if (current) {
// Consistency check
if (
(deps && !current.deps) ||
(!deps && current.deps) ||
(deps && deps.length !== current.deps.length)
) {
throw new Error(
`Invalid call to useHardMemo, the dependency array must either always be present ` +
`or always be absent, and if present must always have the same number of items.`
);
}
}
if (!current || !deps?.every((dep, index) => Object.is(current.deps?.[index], dep))) {
ref.current = current = {
deps: deps?.slice(),
value: fn(),
};
}
return current.value;
};
const words = "one two three four five six seven eight nine ten".split(" ");
let nextRowId = 1;
const Example = () => {
const [rows, setRows] = useState(
words.slice(0, 5).map((value) => ({ id: nextRowId++, value }))
);
const [filter, setFilter] = useState("");
const filteredRows = useHardMemo(() => {
console.log(`Filtering rows`);
if (!filter) {
return rows;
}
return rows.filter((row) => row.value.includes(filter));
}, [rows, filter]);
useEffect(() => {
let handle;
tick();
function tick() {
handle = setTimeout(() => {
const value = words[Math.floor(Math.random() * words.length)];
console.log(`Adding "${value}"`);
setRows((rows) => [...rows, { id: nextRowId++, value }]);
tick();
}, 2000);
}
return () => {
clearTimeout(handle);
};
}, []);
const filterChange = ({ currentTarget: { value } }) => {
console.log(`Setting filter to "${value}"`);
setFilter(value);
};
// I'm using `createElement` because I had to turn off SO's hopelessly outdated Babel because
// I wanted to be able to use optional chaining and such; so I couldn't use JSX.
// return (
// <div>
// <div>
// Filter: <input type="text" value={filter} onChange={filterChange} />
// </div>
// Rows - showing {filteredRows.length} of {rows.length} total:
// <div>
// {filteredRows.map((row) => (
// <div key={row.id}>{row.value}</div>
// ))}
// </div>
// </div>
// );
return createElement(
"div",
null,
createElement(
"div",
null,
"Filter: ",
createElement("input", { type: "text", value: filter, onChange: filterChange })
),
`Rows - showing ${filteredRows.length} of ${rows.length} total:`,
createElement(
"div",
null,
filteredRows.map((row) => createElement("div", { key: row.id }, row.value))
)
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(createElement(Example));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
i am trying to make a filter feature for a website i am working on, i am using an html range slider. The problem is that the values update just if they are going down, for example if i set the slider to $500, only the products that cost $500 or less will appear, if i set the value lower, it's going to work how is supposed to work, but if i try to set the value bigger, the items will not filter, for example, the value is set to $500, if set the value to $600 only the items that are $500 or less will render, but not the $600 ones.
here is my code:
const Shop = () => {
const [sliderValue, setValue] = useState(0);
const [filterItems, setApplyFilter] = useState(false);
const [newData, setData] = useState(data);
const checkChange = () => {
if (sliderValue > 3) {
setApplyFilter(true);
} else {
setApplyFilter(false);
}
console.log(applyFilter);
};
const applyFilter = () => {
if (filterItems === true) {
const filteredData = newData.filter((item) => item.price <= sliderValue);
console.log(filteredData);
setData(filteredData);
} else {
setData(data);
}
};
useEffect(() => {
checkChange();
applyFilter();
}, [sliderValue]);
const handleChange = (value) => {
setValue(value);
};
return (
<div className="slider-container">
<input
type="range"
min={0}
max={1000}
value={sliderValue}
onChange={(e) => handleChange(e.target.value)}
className="slider"
/>
</div>
);
}
The problem: you are changing the data with setData(), so every time you move your scrollbar this deletes some data. If you want to keep a constant information that is available to all your application, consider using useRef(). This creates a persistent object for the full lifetime of the component.
import { useRef } from 'react'
const Shop = () => {
const dataArr = useRef(data)
...
const applyFilter = () => {
if (filterItems === true) {
// Access with "current" attribute
const filteredData = dataArr.current
.filter((item) => item.price <= sliderValue);
setData(filteredData);
}
}
}
Working example
I think it's something to do with this two lines:
const filteredData = newData.filter((item) => item.price <= sliderValue);
setData(filteredData);
Once you have filtered your data once, the value of newData in your state will be only the already filtered data.
Let's say we start with prices: newData=[100, 200, 300, 400]
We filter it for the first time down to 200, so now newData=[100, 200]
Next we filter up to 300, but newData only has [100, 200]
So just change those two lines for:
const filteredData = data.filter((item) => item.price <= sliderValue);
setData(filteredData);
This is asuming you have a variable data declared or imported somewhere with the comple data set.
You don't need state for data array since it can be determined on every render based on some other state.
const Shop = ({ inputData }) => {
const [sliderValue, setValue] = useState(0);
// This flag is deterministic based on sliderValue, so determine it here
const filterItems = sliderValue > 3;
// The items that will make it past the filter are deterministic, based on your filterItems flag
// so no state is necessary
const renderItems = filterItems ? inputData.filter(i => i.price <= sliderValue) : inputData;
const handleChange = (value) => {
setValue(value);
};
return ...
};
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.
I am new to React and I am building a budget calculator. I am taking the amount from one input and adding it to another input so I can come up with the balance. I have tried reduce and concat and they are coming up to sum but the value is wrong. I don't know what I'm doing wrong. Can anyone point me in the right direction. I think the problem is that the values are rendering twice and that's throwing off the math. I don't know.
Here is my code:
// this is the component to get the balance
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
balance: []
}
}
getBalance = (total) => {
this.setState((prevState) => ({
balance: [prevState.balance, total].reduce((acc, currentVal) => {
return Number(currentVal) + Number(acc)
}, 0)
}));
}
render() {
return (
<div className="App" >
<div className="count">
<h2 className="balancetitle">Your Balance</h2>
<h1 style={{ color: this.state.balance >= 0 ? 'green' : 'red' }}>${this.state.balance}</h1>
</div>
<Transactions getBalance={(total) => this.getBalance(Number(total))} />
<Income getBalance={(total) => this.getBalance(Number(total))} />
</div>
);
}
}
// this is the code to get the transaction. I have another component that is identical to get the sum of the income.
const Transactions = (props) => {
const [expenses, setExpense] = useState([])
const [amount, setAmount] = useState([])
const [id, setId] = useState([])
const [listOfTrans, setListofTrans] = useState([])
const [total, setTotal] = useState([0])
//fires on click or enter
const handleSubmit = (e) => {
e.preventDefault()
addExpense({
amount,
expenses,
id
});
setAmount('')
setExpense('')
}
//get value of inputs
const getValue = (hookSetter) => (e) => {
let { value } = e.target;
return hookSetter(value)
}
// turn amount and expense into objects and put them setListofTranas
const addExpense = (expenseObject) => {
setListofTrans([...listOfTrans, expenseObject])
}
const show = () => {
if (listOfTrans.legnth > 1) {
return listOfTrans
} else return null
}
// get total amount of listoftrans
const getAmount = () => {
if (listOfTrans.length > 0) {
let listAmount = listOfTrans.map(list => {
if (list.amount) {
return -Math.abs(list.amount);
} else {
return 0;
}
})
return listAmount.reduce((acc, currentValue) => {
return Number(acc) + Number(currentValue)
}, 0)
} else return 0
}
//update amount total on click
useEffect(() => {
setTotal(getAmount())
props.getBalance(getAmount())
}, [listOfTrans])
// delete item from array
const deleteExpense = (i) => {
let objExpense = i
setListofTrans(listOfTrans.filter((list) => {
return list.id !== objExpense
}))
}
I am adding it here as the suggestion is not possible to add long description in comments section.
What you are doing buggy in the the solution above is making use of useEffect to do the calcualtions. The approach can be real buggy and difficult to debug.
//update amount total on click
useEffect(() => {
setTotal(getAmount())
props.getBalance(getAmount())
}, [listOfTrans])
In the code above listOfTans is an array , may be changing due to various operation, which cause the useEffect callback to run repeatedly. The callback is reponsible for updating the sum.
So instead of doing that, you should just call
props.getBalance(getAmount())
in onClick Handler.
This is just the suggestion for what I can understand from the question above.
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()