State management and pagination in React - javascript

I am building a simple app that calls 100 users from an API and displays them in a grid. I have filters for gender and an input for username search. I also added pagination to display 20 users per page. I pass props for the currentData and pagination functions to the child component (Feed)
My issue is: when I search the username in input, the pagination holds strong and it gives the impression there are no matching users, however when you navigate to pages 4 or 5 they will appear if they exist there.
Relevant code here:
App.js
//
const [currentPage, setCurrentPage] = useState(1);
const [usersPerPage, setUsersPerPage] = useState(20);
const paginate = (number) => setCurrentPage(number);
const lastUserIndex = currentPage * usersPerPage;
const firstUserIndex = lastUserIndex - usersPerPage;
const currentData = data?.slice(firstUserIndex, lastUserIndex);
Feed.js
//
if (username.length >= 1) {
setUsersPerPage(100)
} else {
setUsersPerPage(20)}
My initial thought (above) was to set all the users on one page to ensure the username filter picks up all the users in my data and display them on the one page. However a warning is being thrown in the console (the same as this https://github.com/facebook/react/issues/18178#issuecomment-595846312) as when the page first loads, it simultaneously sets the usersPerPage in both App.js and Feed.js. I tried migrating all the pagination functionality to Feed.js, but there was an issue with passing the props.
Any help or logic to figure this out would be much appreciated!

Based on the sandbox you linked in the comments I would say you need to rethink your component flow.
There doesn't seem to be a need to call your API in the App Component. Instead call that in your Feed component. That will make things easier to follow and separate your concerns.
The other issue I see is that your pagination component is coupled to the amount of total users, not the amount of users you are displaying in the Feed component.
Your pagination component should be dynamic and based on the amount of users you want to display in total. The logic to paginate should see that you are trying to display more than your page limit (i.e 25) and create new pages.
You could do this in several ways, but a simple one that comes to mind is to create a state array of 'pages', and have a function that updates the pages state with nested arrays of less than 25 elements.
Then you can have something like so:
const [pages, setPages] = [];
const pageLimit = 25;
const updatePages = (users) => {
let nPages = [];
// break up user array into chunks of page limits
for (let i = 0; i < users.length; i+= pageLimit) {
nPages.push(users.slice(i, i + pageLimit))
}
setPages(nPages);
}
Now you have an array of all the pages you need to display. So when you add a filter and call updatePages with only a single user, your components can know there is only a single page to display, with only a single user on that page.
This is just an example of how you might rethink this issue. But the key take away is that you need to couple your pagination to what is being rendered instead of how many items were retrieved from your initial API call.

Related

React how to display multiple components of different origin on the same parent component

I'm trying to build a Dashboard with React Js and I would like to know how you could display multiple components, but as widgets, which mean you need to be able to add them in any order and in any quantity. My problem is that I can't find a way to render a map of components.
The user may be able to add a widget to his dashboard by clicking a button, or remove them the same way, so I won't know how many of what components will be rendered
I would like to do something like, when the user clicks on a widget, it adds it to the dashboard, but I don't know how to do that.
I've tried to store components in a map, then with a forEach loop display them all by returning a div containing the component:
import Weather from '...'
import Currency from '...'
import News from '...'
const map = [Weather, Currency, News]
const runAll = () => {
map.forEach((fcn) => {
let runner = fcn
runner()
})
}
runAll()
I've searched many stack and other forums questions, without finding what I needed
Do you guys have an idea of what I could do to solve this ?
So you need to be able to easily render 2 things:
a list of widgets that the user can click and add in the dashboard
the actual dashboard. All selected widgets in a list (with a remove capability)
Let's first figure out what our state should be that also feeds the components 1 and 2.
For the 1st one we need the full list of available widgets. Since this is static (we have 3 widgets available) this can be expresses through a static mapping (a simple javascript object) declared once.
For the 2nd one we need an array of the user selected widgets. That's the dynamic part. We need to be able to set the initial widgets shown and have the capability to add and remove widgets from this list, allowing the same widget appearing more that once.
Static widget mapping
This should be a mapping between an identifier and the react widget component and should look like this:
import News from "./News";
import Weather from "./Weather";
import Currency from "./Currency";
const widgetsMapping = {
news: News,
weather: Weather,
currency: Currency
};
Widgets state
This is an array of widget identifiers (the keys from the static mapping) that the user wants in the dashboard. Also we need add and remove methods. Using useState we can write this like below:
const [widgets, setWidgets] = useState(["weather", "news"]);
const addWidget = (widget) => {
setWidgets([...widgets, widget]);
};
const removeWidget = (index) => {
const updated = [...widgets];
updated.splice(index, 1);
setWidgets(updated);
};
Rendering
Dashboard
Then we can render the dashboard by iterating our widget state array:
{widgets.map((widget, index) => {
const Widget = widgetsMapping[widget];
return (
<Widget
key={`${widget}${index}`}
removeWidget={() => removeWidget(index)}
/>
)
})}
removeWidget prop can be used to let a widget remove itself when sth is clicked.
List of available widgets
Here we will iterate through all available widgets from our static mapping and render all of them with the add functionality bound to them.
{Object.keys(widgetsMapping).map((widget) => (
<button key={widget} onClick={() => addWidget(widget)}>
{widget}+
</button>
))}
You can find a full working example in this code sandbox. Some assumptions were made about how you want to add and remove widgets but the main idea remains the same.
Keep a state (array) that holds widgets added by user. Define constants for widgets and save these constants to your persistance storage.
const widgets = {weather : 1, news: 2}
save these values to database as json with properties configured by user if needed, and then retrieve this json and render components based on it
sample JSON structure to save - [{type: 1, prop1: "val"},{type: 2, prop1: "val"}]
const renderWidgets = (array) => {
const widgets = [];
array.foreach((widget) => {
switch(widget) {
case widgets.weather:
widgets.push(<Weather ...props/>);
break;
.
.
.
etc
}
});
return widgets;
}

React force update

I always run into situations where I need to force rerender, while I'm still in the execution of some function, so I developed my solution to this and I need to know if this is right or there is a simpler way to achieve the same goal.
I rely on the state variable my_force_update, then I change it to a random value when I want to enforce a change. like:
const [my_force_update, setMyForceUpdate] = useState(0);
useEffect(()=>{}, [my_force_update]);
const handleSubmit = async () =>{
await prm1();
stMyForceUpdate(Math.random()); // enforcing the effect
await prom2();
....
}
so I have been able to enforce re-render (by enforcing the effect) while I'm still in the handleSubmit execution.
is there a simpler way? or, did I mistakenly understand the concepts of React?
update
The issue is that I have a checkout form, and I need it to be a signup form at the same time, and there is also a login component on the page.
so I need to populate the form fields with the account if information in case of login and in case of sign up.
The steps are as follow:
if user login => populate form (per fill it with user info) => move to payment.
if user fill out the form manually:
create an account.
authenticate the new user.
update the user account.
repopulate form (with data from user account).
move to payment.
so I have this function that needs to listen to the login and signup:
const token = useSelector(_token);
const loggedIn = useSelector(_loggedIn);
const profile = useSelector(_profile);
useEffect(() => {
/**
* Pre-fill the form inputs
*/
(async () => {
const r = await dispatch(fetchUserInfo());
setFormProfile(profile); // address is not updated yet
setFormAddress(r?.result?.address);
})();
}, [loggedIn, forceUpdate]);
now, there are no issues with the login process, the only problem is with the signup:
at step 2, when authenticating the user, its account is empty.
so the loggedIn changes to true when the profile is empty so I got empty form.
after updating the profile, loggedIn will not change, so I need another variable to trigger the effect again.
I tried to listen to profile here, but I got an infinite loop.
and here is the checkout flow related to the signup:
...
if (!loggedIn) {
const signupResponse = await dispatch(signupUser(params));
loginResponse = await dispatch(login(formProfile?.email, password));
}
const updateProfileResponse = await saveChangesToProfile();
// update user profile with the information in the checkout form.
...
then save changes to the profile:
const saveChangesToProfile = async () => {
const r = await dispatch(fetchUserInfo());
const addressID = r?.result?.address_id;
const res1 = await dispatch(updateUserAddress(addressID, { ID: addressID, ...formAddress }));
const res = await dispatch(UpdateUser(r?.result?.ID, formProfile));
setForceUpdate(Math.random()); // force re-render to re-populate the form.
setSuccess("Information saved to your profile!");
return res;
};
Update 2
The question is general, I solved the issue in another way days ago (involving changes to the server routes). and I'm asking the question in a general way to get some knowledge, not for others to do the work for me.
In general, you should avoid having to force an update in React but instead use existing React features to accomplish your goal. That being said, there are simple ways to force a re-render in react. You mentioned in the second update that you are looking for more general solutions - so I will provide them here.
However, please bear in mind that this topic has been discussed extensively in other stack overflow questions (I will provide links).
Forcing Re-Render using component.forceUpdate(callback)
The react docs actually list a simple way to force a component to reload (provided you maintain a reference to it). You can find more information here, but essentially it forces your component to re-render and then makes a call to the callback argument.
Forcing Re-Render using hooks
There are multiple stack overflow questions that provide simple code snipets that can force a react component to re-render by using hooks. This answer for example by #Qwerty demonstrates 2 simple code snipets to force a re-render:
const forceUpdate = React.useState()[1].bind(null, {}) // see NOTE above
const forceUpdate = React.useReducer(() => ({}))[1]
You should check out his answer for a more detailed explanation.
Other sources include this answer to the same stack overflow question that references the official FAQ.
It solves the problem by doing:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
Solving Your Specific Problem
I saw that you were able to solve your problem by using the useEffect hook - a great start for a potential solution. You also mentioned that you got an infinite loop while listening to a variable change in your hook - a common problem and one with some common solutions. In general, you should always run a check inside the useEffect hook before changing any of its dependencies. For example, run a check to see if the profile is unset before trying to update its value.
I however would recomend that you use a progress varible that would indicate your status, something like this:
const STATUS_START = 0;
const STATUS_LOGED_IN = 1;
const STATUS_SIGNING_UP = 2;
const [progress, setProgress] = useState(STATUS_START);
Then, you can simply listen to changes made to the progress variable in your useEffect hook (by passing it as your only dependent). This should automatically condition you to write the necessary logic to check for state inside of the useEffect function as I described previously.
This solution would work by initially setting the progress to either signing up or logging in, but only filling the form data if you are logged in (and after the signup progress is done calling setProgress(STATUS_LOGED_IN))

React - How can I get pagination to re-render based off amount of objects per view selected via user-input?

I have a pagination that works with <ToggleButtons> ,users can select how many items they want to display per page. The problem is, when a user select's a different amount of items to display other than the default, the pagination display shows more than needed page numbers because the calculation of items to display doesn't get re-calculated into the <Pagination> function. So the interface is still displaying the default number of page numbers.
Please see this sandbox for more info:
https://codesandbox.io/s/wizardly-agnesi-29zq8?file=/src/App.js
Basically I have these buttons that display 2, 4, or 6 items. If you toggle any of the buttons you will see that the pagination UI does not re-render itself to calculate the correct number of pages. In return there are useless pages in the interface that do nothing. Please see for yourself in the link above.
How can I solve for this? I've tried adding another state but I still had the same problem.
The answer is that you need to make sure that you are updating the noOfPages when itemsPerPage changes . Take a look at this example:
https://codesandbox.io/s/hardcore-smoke-dsxqo?file=/src/App.js
I have only made a few changes to your setup, but you can see that it should solve your problems. This function here is doing all of the heavy lifting.
const handleItemsPerPage = (event, newAmount) => {
if (newAmount === null) {
setItemsPerPage(5);
setNoOfPages(Math.ceil(SymbolData.length / 5));
return null;
}
setItemsPerPage(newAmount);
setNoOfPages(Math.ceil(SymbolData.length / newAmount));
// Checking if the page number is too high given # per page.
if (page > Math.ceil(SymbolData.length / newAmount)) {
setPage(Math.ceil(SymbolData.length / newAmount));
}
};
For starters I added the null check before attempting to set state. Then I just updated the number of pages (setNoOfPages) to account for the new change in display number per page.
Finally I added one check that will help with your UX. If the user is viewing 2 results per page, then there are 5 pages. However, if they switch the number of results per page to 6, then there are only 2 pages, but the selected page is still 5. So I added a check to determine whether or not your app should update the current page to be within the range of total pages.
I realize that you didn't ask for that, but it was bugging me so I threw it in. Ideally you might even want to handle some of this with useEffect, but since that isn't part of your current code I didn't want to add complexity.
You need to re-calculate the number of pages when the items per page changes. You can do this with an effect, so import it and then add this inside your component:
import React, { useEffect, useState } from "react";
...
useEffect(() => {
setNoOfPages(Math.ceil(SymbolData.length / itemsPerPage));
}, [itemsPerPage]);
Of course setNoOfPages is the setter for the number of pages state, so gotta destructure that too when defining the state:
const [noOfPages, setNoOfPages] = React.useState(
Math.ceil(SymbolData.length / itemsPerPage)
);
I tweaked your sandbox to show this here

How to render user list in React-Firebase

I'm still learning React and I'm trying to make a "design review app" where users signup as customers or designers and interact with each other.
I made the auth system and made sure that while signing up every user would get also some attributes in the firebase database.
Therefore, in my DB, I have a 'users/' path where every user is saved by uid.
Now I'm able to render a different dashboard if you're a customer or a designer.
In my customer dashboard, I just want to render a list of designers (and clicking on them go to their projects).
However, I'm having so many problems trying to get this stuff to work!
In the following code, I'm trying to fetch the users from the db and add their uid to an array.
Later I want to use this array and render the users with those uids.
import firebase from "firebase/app";
import "firebase/database";
export default function CustomerContent() {
const[designers, setDesigners] = useState([]);
function printUsers (){
var users = firebase.database().ref('/users/');
users.on('value', (snapshot)=>{
snapshot.forEach((user)=>{
console.log(user.key)
firebase.database().ref('/users/'+user.key).on('value', (snapshot)=>{
var role = snapshot.val().role
console.log(role)
if(role === 'designer'){
const newDesigners = [...designers, user.key];
setDesigners(newDesigners);
}
})
})
})
}
useEffect(() => {
printUsers();
console.log(designers);
}, [])
return (
<div>
designer list
</div>
)
}
Now the problem with this code is that:
it looks like it runs the printUsers functions two times when loading the page
the array is empty, however, if I link the function to a button(just to try it), it seems to add only 1 uid to the array, and always the same (I have no idea what's going on).
ps. the console.log(user.key) and the console.log(role) print the right user-role combination
It's not a stupid question. Here's what I'd change it to (of course you'd remove the console.logs later though). It's hard to know if this will work perfectly without having access to your database to run it, but based on my last react/firebase project, I believe it'll work.
The first thing was that you reference /users/, when you only need /users. I'm not sure if it makes a difference, but I did it the latter way and it worked for me.
Secondly, you're calling firebase more than you need to. You already have the information you need from the first time.
Third, and this is small, but I wouldn't call your function printUsers. You're doing more than just printing them- you're making a call to firebase (async) and you're setting the state, which are much larger things than just print some data to the console.
Lastly, I would store the entire object in your designers piece of state. Who knows what you'll want to display? Probably at least their name, then possibly their location, background, an icon, etc. You'll want all of that to be available in that array, and possibly you'll want to move that array into redux later if you're app is big enough.
I also added some JSX to the bottom that gives a simple output of what you could do with the designers array for the visual aspect of your app.
import firebase from 'firebase/app';
import 'firebase/database';
export default function CustomerContent() {
const [designers, setDesigners] = useState([]);
function printUsers() {
var users = firebase.database().ref('/users');
users.on('value', (snapshot) => {
snapshot.forEach((snap) => {
const userObject = snap.val();
console.log(userObject);
const role = userObject['role'];
console.log(role);
if (role === 'designer') {
const newDesigners = [...designers, userObject];
setDesigners(newDesigners);
}
});
});
}
useEffect(() => {
printUsers();
console.log(designers);
}, []);
return (
<div>
<h2>The designer are...</h2>
<ul>
{designers.map((designerObject) => {
return <li>{designerObject.name}</li>;
})}
</ul>
</div>
);
}

How to inject photos to a Gatsby site?

I have a Gatsby and Strapi photo blog and I want to have a home page that loads 10 pictures at a time until the user hits the bottom of the page, then load the next ten etc, so that the user isn't downloading all photos at once.
I'm using useStaticQuery to load the images initially. However, that can only run at build time. Is there a way to make another graphQL call when the user hits the bottom of the page and add it to my state? Or is this the "static" part of a static site generator 😄.
Alternatively, does making the graphQL call for all photo data make its way to the client device if I don't render it? Say if I just use React to render parts of the array at a time?
Below is my home page. I'm using Karl Run's bottom scroll listener, and the Photos component renders the photos as a list.
const IndexLayout: React.FC<Props> = () => {
const [photosData, setPhotosData] = useState<InitialQueryType['allStrapiPhoto']>(getSiteMetaDataAndTenPhotos().allStrapiPhoto)
const handleOnDocumentBottom = useCallback(() => {
console.log('at bottom, make a call')
// this throws an invalid hook error as getTenPhotos calls useStaticQuery
let morePhotosData = getTenPhotos(photosData.edges.length)
setPhotosData({ ...photosData, ...morePhotosData })
}, [photosData])
useBottomScrollListener(handleOnDocumentBottom)
return (
<LayoutRoot>
<div className={styles.bigHomeContainer}>
<div className={styles.titleContainer}>
<h1 className={styles.indexHeading}>TayloredToTaylor's Wildlife Photos</h1>
</div>
<Photos photos={photosData} />
</div>
</LayoutRoot>
)
}
export default IndexLayout
Github Repo
As you said, queries are called in the build-time. However, one workaround that may work for you is to retrieve all photos at the beginning (build-time) and show them on-demand in groups of 10 triggered by time or by the user's scroll, etc. Adapting something like this to your use-case should work:
Fetch all photos with:
const allPhotos=<InitialQueryType['allStrapiPhoto']>(getSiteMetaDataAndTenPhotos().allStrapiPhoto)
Set the current currentIndex:
const [currentIndex, setCurrentIndex]= useState(0);
And ask them on-demand:
const morePhotosData = ()=>{
let cloneOfAllPhotos= [...allPhotos];
let newPhotos= cloneOfAllPhotos.splice(currentIndex, currentIndex + 10) // change 10 to your desired value
setPhotosData(newPhotos);
setCurrentIndex(currentIndex+10);
}
Basically, you are cloning the allPhotos (let cloneOfAllPhotos= [...allPhotos]) to manipulate that copy and splicing them with a dynamic index (cloneOfAllPhotos.splice(currentIndex,currentIndex + 10)) that is increasing by 10 in each trigger of morePhotosData.

Categories