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.
Related
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.
Link to the first answer question :
Problem with reset state function and only than fetching data (React hooks)
My Structure hierarchy of my components: search container --> viewSearchBranches --> profilesSearchOnline
I got a son component called: "profilesSearchOnline".
There i do an event listener to see if a new user got in the site, and if so i add him on the screen.
The problem is when i try to add him, (lets say to an empty list), the variables "profiles" which came from redux store, "remember" the last list was in another page before that. (which usally show 4 users at begining).
So i have a problem, i wanted the list to be an empty (on the first load) -> so i created the func restoreStatesToDefault().
But actually when the son components try to add a new user get in the site, i see he his full with 4 users last page it doesnt really empty because clusters.
I try to do send the variable shouldFetch to the son components which showen to me but it doesnt work.
The error is: this cause an error of Error: Cannot find module './undefined'.
profilesSearchOnline component:
const profiles = useSelector(state => state.profiles);
const currUser = useSelector(state => state.user);
const {list} = profiles;
useEffect( ()=> {
setEventsToUserConnectivity();
},[profiles]);
const setEventsToUserConnectivity = () => {
listenToswitchUserToOnlineEvent();
listenToDisconnectUserEvent();
}
const listenToswitchUserToOnlineEvent = () => {
currUser.socket_server.on('user_connect',(profileObj) => {
if(history.location.pathname == '/search/online' && !isUserAlreadyOnline(profileObj.userId) ){
dispatch(addProfileToList(list,profileObj)); --> here i am adding the "list" which should be empty but actually its full with 4 users already and there is an error.
setProfilesLoading(null);
}
})
}
I have a page I am trying to fix in order to keep scroll position when user presses back button (browser). Let's say I have a component called list, where I show the user some products. To see all the products the user can scroll down the list component. When the user clicks on some product, the application redirects the user to the detail component. Then when the user tries to go back to the list, hits the back button of the browser, the list component gets rendered and it seems like it scrolls to top automatically.
As far as I know, pressing the back button of the browser triggers a window.history.back() action, nothing else happens.
For a solution, I have implemented a variable in the context of my application that saves the scrollY value and then, in the componentWillMount (or useEffect) of the component I am trying to render (list component), I set the scroll position to the value set in the context.
Details of my solution are here, as I have based my entire code in this stack overflow's post:
How to change scroll behavior while going back in next js?
I have checked the value using some logs and the scroll position is saved correctly in the context, however, as I am using a window event listener, it sets the value to zero just after the list component is rendered.
In my code I am not using any kind of scroll configuration, so I was wondering if that behavior is some sort of default for either Next.js or react. It happens when the user hits the back button of the browser, but I am a newbie to next and I don't know if I am missing something or what, I don't even know if this issue has something to do with React or Next.js itself.
This gist may be of assistance as it includes a custom hook to manage scroll position: https://gist.github.com/claus/992a5596d6532ac91b24abe24e10ae81
import { useEffect } from 'react';
import Router from 'next/router';
function saveScrollPos(url) {
const scrollPos = { x: window.scrollX, y: window.scrollY };
sessionStorage.setItem(url, JSON.stringify(scrollPos));
}
function restoreScrollPos(url) {
const scrollPos = JSON.parse(sessionStorage.getItem(url));
if (scrollPos) {
window.scrollTo(scrollPos.x, scrollPos.y);
}
}
export default function useScrollRestoration(router) {
useEffect(() => {
if ('scrollRestoration' in window.history) {
let shouldScrollRestore = false;
window.history.scrollRestoration = 'manual';
restoreScrollPos(router.asPath);
const onBeforeUnload = event => {
saveScrollPos(router.asPath);
delete event['returnValue'];
};
const onRouteChangeStart = () => {
saveScrollPos(router.asPath);
};
const onRouteChangeComplete = url => {
if (shouldScrollRestore) {
shouldScrollRestore = false;
restoreScrollPos(url);
}
};
window.addEventListener('beforeunload', onBeforeUnload);
Router.events.on('routeChangeStart', onRouteChangeStart);
Router.events.on('routeChangeComplete', onRouteChangeComplete);
Router.beforePopState(() => {
shouldScrollRestore = true;
return true;
});
return () => {
window.removeEventListener('beforeunload', onBeforeUnload);
Router.events.off('routeChangeStart', onRouteChangeStart);
Router.events.off('routeChangeComplete', onRouteChangeComplete);
Router.beforePopState(() => true);
};
}
}, [router]);
}
Looking at your url, using shallow routing could solve the problem. Where the URL will get updated. And the page won't get replaced, only the state of the route is changed. So you can change your logic according to that.
A good example is in the official documentation:
https://nextjs.org/docs/routing/shallow-routing
And you might use display: 'hidden' to hide and show your components conditionally according to your state!
It's a way around but it could be even more useful depending on your exact situation !
After looking for another solution that does not use the window.scroll and similar methods, I have found a solution.
1st solution (worked, but for me that I have an infinite list that is loaded via API call, sometimes the window.scroll method wasn't accurate): I take the window.scrollY value and set it in the session storage, I did this before leaving the list page, so in the details page, if user hits the back button, at the moment the page is loading, I get the Y value from session storage and use the window.scroll method to force the page to scroll to the previously configured value.
As I mentioned earlier, this worked, but in my case, I have a list that is populated from an async API call, so sometimes the page loaded without all the images and the scroll was already configured, then the images and data were loaded and the user ended up seeing some other place in the page rather than the desire position.
2nd solution: In my case we are talking about a e commerce app, so I found this solution useful as it focuses in a particular item with its corresponding ID instead of the Y coord of the window. Scroll Restoration in e commerce app
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>
);
}
I am currently working on a weather app constructed with react-create-app that works on React Router to show data for 5 different days. Each day (presented in a box) is a Link that redirects the second component to it specifically so that it can show more detailed data for the specific day.
I have a function that generates the data for each tile:
generateTileData() {
const weatherData = this.state.data;
if (!weatherData) return null;
let days = [];
const newData = [...weatherData].filter(day => {
let dateFromAPI = moment.unix(day.dt).date();
if (days.indexOf(dateFromAPI) > -1) {
return false;
} else {
days.push(dateFromAPI);
return true;
}
});
// console.log(days)
return newData.map((day, item) => {
const dateId = day.dt;
return (
<Link to={`/w/${dateId}`}>
<WeatherTile key={day.dt} index={item} {...day} date={day.dt_txt} />
</Link>
);
});
}
That then is being rendered:
<HashRouter>
<React.Fragment>
<div className="columns is-gapless tiles">
{this.generateTileData()}
</div>
{weatherData && <Route exact path="/w/:dateId" render={({ match }) => <Weather data={weatherData} day={[...weatherData].find(day => day.dt == match.params.dateId)} />} />}
</React.Fragment>
</HashRouter>
The problem is that as I am fetching the data (through a form) only after the user inputs it, when I accidentally refresh the page, the application does not know where the data comes from and thus gives me a nice error that it cannot read the property main of undefined (as in my Weather component:
const Weather = ({ day }) => {
console.log(day);
const data = day;
return (
<div className="info">
<p>{data.main.temp}</p>
<p>{data.main.humidity}</p>
</div>
)
}
Is there any way that I can either prevent the user from refreshing or by default redirecting the user on refresh to the main page (that would be path="/")?
There are a few ways I think you could go about getting around this issue. One way would be how #Pat Mellon described and to add a refresh state and redirect should it be true or false (however you want to build the logic).
You could also add in some default logic, which is more of a 'React' way of doing things. For instance, if there is no user info being seen by the app, simply output some example weather.
Up next would to be adding in url logic and sticking your user data into the url itself. Kind of like an API call, the app would look at the url and not the state to pull information to display. (Look at URI.js for a nice way of doing this)
Another way that might be a little more UX friendly is to persist the data from the form into the user's local storage. Using this method would allow you to maintain the data and use it even if the page is refreshed, closed, computer is restarted, etc. The only caveat is remembering to clear the user's local storage (and more importantly, the information that you saved and not that of other websites) if the user is to ever re-enter information into your form.
Lot's of fun ways to untangle this issue, and I'm sure plenty more than the four I've listed.