Infinite scroll in React with previously changed setState not working - javascript

I have a React component which gets from an API data with fetch of 10 images. I would like to use infinite scroll to load more sets of 10 images.
What I made to do is to listen the event of reaching the bottom of the website and posting new url of nest 10 images in console only :)
Should I focus on getting all data in my url, or focus on render and usage of related function?
Or maybe the problem is because I get data in componentDidMount and I don't know how to update whole state?
import React from 'react';
import ReactDOM from 'react-dom';
class ViewSection extends React.Component {
constructor(props) {
super(props);
this.state = {
image: [],
like: [],
location: [],
first_name: [],
last_name: [],
pictureId: [0,1,2,3,4,5,6,7,8,9],
page: 1
};
this.handleScroll = this.handleScroll.bind(this) // I'M MOVING DATA TO HANDLE SCROLL
};
handleScroll(e) {
e.preventDefault();
let documentHeight = document.documentElement.offsetHeight;
let windowHeight = window.innerHeight;
let windowScroll = window.scrollY;
let scrollTotal = windowScroll + windowHeight;
if (scrollTotal == documentHeight) {
this.setState({ page: this.state.page + 1 })
// console.log(this.state.page);
}
};
componentDidMount() {
let urlImage = ('https://api.website.com/categories/' + this.props.params.sectionId + '/photos/?client_id=MYID&page=' + this.state.page); // MAP ALL PAGES?????
window.addEventListener("scroll", this.handleScroll,false);
fetch(urlImage)
.then(resp => resp.json())
.then(response => {
// console.log(response);
// console.log(this.state.page);
let arrayOfImages = response.map((item) => item.urls.small );
let arrayOfLikes = response.map((item) => item.likes );
let arrayOfLoc = response.map((item) => item.user.location );
let arrayOfFirst_Names = response.map((item) => item.user.first_name );
let arrayOfLast_Names = response.map((item) => item.user.last_name );
this.setState ({
image : arrayOfImages,
like : arrayOfLikes,
location : arrayOfLoc,
first_name : arrayOfFirst_Names,
last_name : arrayOfLast_Names
})
});
};
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll,false);
};
render() {
// console.log(this.state.image);
console.log(this.state.page); // LISTENS AND RENDERS ALL CHANGES... MAYBE PROMISE.ALL ON urlImage...?
let section = this.state.image.map((elem, i, page) => {
return (
<Link key={i} onScroll={this.handleScroll} className="section-picture" to= {`/section/${this.props.params.sectionId}/picture/${this.state.pictureId[i]}`}>
<img className="image" src={elem} alt="" />
<div className="section-picture-stats">
<div className="section-picture-stat"> author: {this.state.first_name[i]} {this.state.last_name[i]}</div>
<div className="section-picture-stat">{this.state.like[i]} like(s)</div>
<div className="section-picture-stat">{this.state.location[i]}
</div>
</div>
</Link>
)
});
return (
<div className="gallery">
<h1>Section</h1>
<div className="buttons">
<div className="sort-clicks click">sort by: <a className="click" href="">new</a> or <a className="click" href="#">trending</a></div> <Link className="click" to='/'>back</Link>
</div>
<div className="section-picture-list">{section}</div>
</div>
)
};
};
export { ViewSection }

It looks to me like the value of section will be the result of mapping an empty array, since this.state.image will be empty until the fetch in componentDidMount finishes. Try adding a check in your render function like so:
let section;
if (this.state.images.length === 0) section = <p>Loading...</p>;
else section = this.state.image.map((elem, i, page) => {
...your code goes here...
});
This way it should update properly at least on the initial render

Related

How to make loading screen run until all image are loaded?

i try to make a loading screen while waiting for all images are fully loaded.
React Lifecycle is Render -> componentDidMount -> render, my images are not fully loaded, just got called but my componentDidMount always finishes and executes render even my image isn't fully loaded.
componentDidMount() {
const ie = [document.querySelectorAll('img')];
ie.map(imgElm => {
for (const img of imgElm) {
if (!img.complete) {
this.setState({ imageIsReady : true});
}
}
return this.setState({ imageIsReady : false});
})
}
on the componentDidMount for loop function try to check every img is complete or not, give me a hundred true (my image is a lot, just try to make gallery). and loading screen shows but only a few ms, then I can scroll over my image but more than half of my image is still loading.
render() {
<div>
{
this.state.imageIsReady ?
<div className='inset-0 fixed flex justify-center z-20 w-full h-full bg-black bg-opacity-25 blur'>
<img src={loading} className='w-3/12' alt="load"/>
</div> :
<div className='hidden '>
<img src={loading} alt="load"/>
</div>
}
<div>page......</div>
</div>
}
my code: https://alfianahar.github.io/MobileLegendHeroList/ in this site I use setTimeout on my componentDidMount, this does not solve my problem when using slow 3g nor fast 3g/
Maybe this example can help you. But remember that will works only for image which aren't nested inside components.
class Component extends Component {
constructor(props) {
super(props)
this.state = {
ready: false
}
}
componentDidMount() {
Promise.all(
Array.from(document.images)
.filter(img => !img.complete)
.map(img => new Promise(
resolve => { img.onload = img.onerror = resolve; }
))).then(() => {
this.setState({ ready: true })
});
}
render() {
if ( ! this.state.ready ) return <div>Loader</div>
return <div>Content</div>
}
}
<Container>
<img/> <!-- work -->
<Component>
<img/> <!-- doesn't work -->
</Component>
</Container>
React 16.8.x
import React from "react";
function App() {
const [imagesRequested, setImagesRequested] = React.useState({});
const [images, setImages] = React.useState([
{ name: "first image", src: "https://picsum.photos/200/300" },
{ name: "second image", src: "https://picsum.photos/300/300" }
]);
return (
<React.Fragment>
{images.map((currentImage) => (
<React.Fragment>
{!imagesRequested[currentImage.name] && <div>loading...</div>}
<img
style={{ opacity: imagesRequested[currentImage.name] ? 1 : 0 }}
src={currentImage.src}
onLoad={() => {
setTimeout(() => { // Fake server latency (2 seconds for per image)
setImagesRequested((previousState) => ({
...previousState,
[currentImage.name]: true
}));
}, 2000);
}}
/>
</React.Fragment>
))}
</React.Fragment>
);
}
export default App;
Your best bet is to possibly create a LoadableImage component which will then handle onLoad event for an object. This onLoad event can then call a parent callback function to set its loaded status.
LoadableImage.js
import { useState } from "react";
const LoadableImage = (props) => {
const { src, alt, width, height, onLoad, onError, id } = props;
//you can use this to render a custom broken image of some sort
const [hasError, setHasError] = useState(false);
const onLoadHandler = () => {
if (typeof onLoad === "function") {
onLoad(id);
}
};
const onErrorHandler = () => {
setHasError(true);
if (typeof onError === "function") {
onError(id);
}
};
return (
<img
src={src}
alt={alt}
width={width}
height={height}
onLoad={onLoadHandler}
onError={onErrorHandler}
/>
);
};
export default LoadableImage;
now you can handle the callbacks in your implementation and act appropriately. You can keep state of all your images, and their loading status.
App.js
export default function App() {
const [images, setImages] = useState(imageList);
const imagesLoading = images.some((img) => img.hasLoaded === false);
const handleImageLoaded = (id) => {
setImages((prevState) => {
const index = prevState.findIndex((img) => img.id === id);
const newState = [...prevState];
const newImage = { ...newState[index] };
newImage.hasLoaded = true;
newState[index] = newImage;
return newState;
});
};
return (
<div className="App">
{imagesLoading && <h2>Images are loading!</h2>}
{images.map((img) => (
<LoadableImage
key={img.id}
id={img.id}
src={img.src}
onLoad={handleImageLoaded}
/>
))}
</div>
);
}
Here handleImageLoaded will update the hasLoaded property of an image in the images state array when a image is loaded. You can then conditionally render your loading screen while (in this case) imagesLoading is true, as I have conditionally rended the "Imags are loading" text.
Codesandbox
imageList looks like this
const imageList = [
{
id: 1,
src:
"https://images.unsplash.com/photo-1516912481808-3406841bd33c?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=683&q=80",
hasLoaded: false
},
{
id: 2,
src: "https://via.placeholder.com/150",
hasLoaded: false
},
{
id: 3,
src: "https://via.placeholder.com/151",
hasLoaded: false
}
];

React js control componentDidUpdate() method

I want to change a API parameter by click function and render new data. When I trigger componentDidUpdate by onclick event listener,the api data changed first and worked fine for first click. But When click second time the api call ran completely. The parameter currentPage is assigned to this.state.count and this this.state.count valued in incremented on click.
My code below:
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
products: [],
count: 1,
};
}
componentDidMount() {
this.ProductList();
}
componentDidUpdate() {
let change = document.getElementById("change");
change.addEventListener("click",(e)=>{
this.changeParams();
this.ProductList();
})
}
changeParams = (e) =>{
this.setState({count: this.state.count + 1})
}
ProductList() {
var myHeaders = new Headers();
myHeaders.append("Cookie", "PHPSESSID=822cu5ctftcpo8f98ehklem4k9");
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
fetch("http://192.168.31.236/magento/rest/V1/products?searchCriteria[filterGroups][0][filters][0][field]=category_id& searchCriteria[filterGroups][0][filters][0][value]=2& searchCriteria[filterGroups][0][filters][0][conditionType]=eq&searchCriteria[sortOrders][0][field]=price& searchCriteria[sortOrders][0][direction]=ASC& searchCriteria[pageSize]=20& searchCriteria[currentPage]="+this.state.count, requestOptions)
.then(response => response.text())
.then(result => this.setState({products:result}),)
.catch(error => console.log('error', error));
}
render() {
const productsList = () =>{
let pro = [];
if(typeof this.state.products === 'string') {
pro = JSON.parse(this.state.products)
console.log(pro)
}else{
pro = []
}
if(pro.items && typeof pro.items !== "undefined"){
return pro.items.map((item, i) => (
<div>
<h1>{ item.name }</h1>
</div>
));
}
}
return(
<div>
{productsList()}
<button id="change">Change</button>
</div>
);
}
}
export default App;
Rather than manually attaching event listeners, do it through React. In pretty much most cases you shouldn't be doing DOM operations directly.
class App extends React.Component {
// ...
/* You don't need this
componentDidUpdate() {
}
*/
handleChangeClick = () => {
this.changeParams();
this.ProductList();
}
// ...
render() {
// ...
return(
<div>
{productsList()}
<button id="change" onClick={this.handleChangeClick}>Change</button>
</div>
);
}
}
The reason why your approach doesn't work is because React may be producing and destroying DOM elements in ways you don't expect, so making sure you manually attach and detach event listeners to the right elements is difficult to get right.

Scroll height adjustment with infinity scroll

At the moment when 'scroll' is at the very bottom, the function getUsers () is called. How to set the scroll so that it doesn't reach the end of the slider, and the getUsers () function is called. That there would be an infinity scroll effect. I mean the scroll effect like here: https://codesandbox.io/s/ww7npwxokk. When the scroll reaches the bottom, it goes back.
Code here: https://stackblitz.com/edit/react-nq8btq
import './style.css';
import axios from 'axios';
class App extends Component {
constructor() {
super();
this.state = {
users: [],
page: 1
};
}
componentDidMount() {
this.getUsers();
}
getUsers = () => {
axios({
url: `https://jsonplaceholder.typicode.com/users`,
method: "GET"
})
.then(res => {
this.setState({
users: res.data
});
})
.catch(error => {
console.log(error);
})
}
scroll = (e) => {
const page = this.state.page;
const bottom = e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
if (bottom) {
alert('bottom');
this.getUsers()
this.setState({
page: this.state.page + 1
})
}
const top = e.target.scrollTop;
if(top === 0 && page > 1) {
alert('I AM AT THE TOP');
this.setState({
page: this.state.page - 1
})
}
}
render() {
console.log(this.state.page)
console.log(this.state.users)
return (
<div>
<div onScroll={this.scroll} className="container">
<ul>
{this.state.users.map((user, index) =>
<li>
{user.name}
</li>
)}
</ul>
</div>
</div>
);
}
}
render(<App />, document.getElementById('root'));
Here I've updated your code, slightly simplified, but mostly your code with the key points commented.
class App extends Component {
state = {
users: [],
page: 1
};
componentDidMount() {
this.getUsers();
}
getUsers = () => {
axios({
url: `https://jsonplaceholder.typicode.com/users`,
method: "GET"
})
.then(res => {
this.setState({
// *you must append to users in state, otherwise
// the list will not grow as the user scrolls
users: [...this.state.users, ...res.data],
page: this.state.page + 1
});
})
.catch(error => {
console.log(error);
})
}
scroll = (e) => {
// *simplified, to only handle appending to the list
// note the 50px from the bottom, adjust as required
// so that the request is made before the users reaches
// the bottom of the page under normal scrolling conditions.
if (e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight + 50) {
this.getUsers();
}
}
render() {
return (
<div onScroll={this.scroll} className="container">
<ul>
{this.state.users.map((user, index) =>
// *always add a unique key for each item
<li key={user.name}>
{user.name}
</li>
)}
</ul>
</div>
);
}
}
All you need to do is decide when to call get users.
i.e. if you want it to be called when you have already scrolled through 70% the height then the factor would be 0.3 (30%)
Whenever the scroll exceeds the 70% mark this.getusers() would be called.
Check out how I have modified the same condition written by you.
Rest of the code remains the same
import React, { Component } from 'react';
import { render } from 'react-dom';
import Hello from './Hello';
import './style.css';
import axios from 'axios';
class App extends Component {
constructor() {
super();
this.state = {
users: [],
page: 1
};
}
componentDidMount() {
this.getUsers();
}
getUsers = () => {
axios({
url: `https://jsonplaceholder.typicode.com/users`,
method: "GET"
})
.then(res => {
this.setState({
users: res.data
});
})
.catch(error => {
console.log(error);
})
}
scroll = (e) => {
const page = this.state.page;
const bottom = e.target.scrollHeight - e.target.scrollTop < e.target.clientHeight-0.3*e.target.clientHeight;
if (bottom) {
alert('bottom');
this.getUsers()
this.setState({
page: this.state.page + 1
})
}
const top = e.target.scrollTop;
if(top === 0 && page > 1) {
alert('I AM AT THE TOP');
this.setState({
page: this.state.page - 1
})
}
}
render() {
console.log(this.state.page)
console.log(this.state.users)
return (
<div>
<div onScroll={this.scroll} className="container">
<ul>
{this.state.users.map((user, index) =>
<li>
{user.name}
</li>
)}
</ul>
</div>
</div>
);
}
}
render(<App />, document.getElementById('root'));
This lib should do the trick for you
$ yarn add react-infinite-scroller
Very simple to implement
<InfiniteScroll
pageStart={0}
loadMore={this.getUsers()}
hasMore={this.state.hasMoreUsers}
loader={<div key={0}>loading ...</div>}
>
<li>
{user.name}
</li>
</InfiniteScroll>
Don't forget to import the lib in your component file
import InfiniteScroll from 'react-infinite-scroller';

React addEventListener having issues when the page rerenders

I have put together a infinite scroll to load more items when a user reaches the bottom of the screen. It works fine the first time but for some reason the eventListener seems to disappear when the first nextLink from redux is loaded.
My code:
import React, { Component } from "react";
import { connect } from "react-redux";
import * as teamsActions from "../../store/teams/actions";
import TeamCard from "../../common/teamCard/teamCard";
import ReactAI from "react-appinsights";
import WithLoading from "../../common/utils/withLoading";
import {
objectToArray,
sortArray
} from "../../assets/general_functions/objectsAndArrays";
import { faRubleSign } from "#fortawesome/free-solid-svg-icons";
class TeamsContainer extends Component {
_isMounted = false;
state = {
scrolling: false
};
componentDidMount() {
this._isMounted = true;
this.props.onFetchTeams();
this.scrollListener = window.addEventListener("scroll", this.handleScroll);
}
s;
componentWillUnmount() {
this._isMounted = false;
window.removeEventListener("scroll", this.handleScroll);
}
loadTeams = () => {
console.log("is teams loading?", this.props.loading);
if (this.props.loading === false) {
console.log("What is the nextLink", this.props.teams["#odata.nextLink"]);
this.props.onFetchMoreTeams(this.props.teams["#odata.nextLink"]);
}
};
loadMore = () => {
this.setState(
{
scrolling: true
},
this.loadTeams
);
};
handleScroll = () => {
const { scrolling } = this.state;
if (scrolling) return;
if (
typeof this.props.teams.value !== "undefined" ||
this.props.teams.value > 0
) {
console.log("value", this.props.teams.value);
const lastTeam = document.querySelector(
".team-card-wrapper:last-of-type"
);
// get the height of the current team, and get the height of the current position on screen.
const lastTeamOffset = lastTeam.offsetTop + lastTeam.clientHeight;
const pageOffset = window.pageYOffset + window.innerHeight;
const bottomOffset = 30;
if (pageOffset > lastTeamOffset - bottomOffset) {
this.loadMore();
}
}
};
render() {
let loading = "";
let error = "";
let teams = [];
let delay = 0;
let loadMoreButton = "";
// check whether the component is fetching data
let loader = "";
if (this.props.teamsLoading) {
loader = <WithLoading isLoading={true} />;
}
// check if there was an error
this.props.error && this.props.loading === false
? (error = <p>There was an error</p>)
: (error = "");
// reorder the teams and make teamCards of it.
if (this.props.teams["value"]) {
// convert the teams object to an array of objects.
// order it by sequence property.
teams = this.props.teams.value;
teams = objectToArray(this.props.teams["value"]);
teams = teams.sort(sortArray("sequence")).reverse();
teams = teams.map(team => {
if (delay === 300) {
delay = 0;
}
delay = delay + 75;
return (
<TeamCard
delay={delay}
id={team.id}
title={team.title}
description={team.description}
isFavorite={team.isFavorite}
memberCount={team.memberCount}
key={team.id}
/>
);
});
} else {
teams = loader = <WithLoading isLoading={true} />;
}
// this.props.teams["value"]);
return (
<React.Fragment>
<div className="App">
{loader == "" ? (
<div className="team-cards-wrapper">{teams}</div>
) : (
<div>{loader}</div>
)}
</div>
</React.Fragment>
);
}
}
const mapStateToProps = state => {
return {
error: state.teamsSlice.teamsError,
loading: state.teamsSlice.teamsLoading,
teams: state.teamsSlice.teams,
searchTerm: state.teamsSlice.searchTerm
};
};
const mapDispatchToProps = dispatch => {
return {
onFetchTeams: () => dispatch(teamsActions.fetchTeams()),
onFetchMoreTeams: teamsNextLink =>
dispatch(teamsActions.fetchMoreTeams(teamsNextLink))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(ReactAI.withTracking(TeamsContainer));
My console (The value is printed on scroll. after loading it, it doesnt echo any value anymore):
Event handlers according to the console:
Redux store:
Edit:
I found that there was a problem in the state.scrolling. It's set to true but never reset after the data has loaded.
any help is much appreciated! Cheers
I think because you are having two componentWillUnMount. and this line
window.addEventListener("scroll", e => {
this.handleScroll(e);
});
seems to be a little wrong. it maybe window.addEventListener("scroll", this.handleScroll) only
- second thing is that I think you should use debounce on scroll event so it can be better for performance
I found out that the problem was setting the scrolling state back to false.
After fetching more redux items, i decided to use a timeout to debounce incoming similar api requests.
setTimeout(() => {
this.setState({ scrolling: false });
}, 300);

Send props from parent to child and update them in child component (ReactJs)

I trying to make a pagination with a response from an API request made with ReactJs. I have one Main.js page that send props to child component which is PageButtons.js. Everything is passing well, I checked that by console logging this.props of the values I passed.
The thing is I need to update the state of props and I need that to get updated on parent component, which is Main.js. I'm using this to increment the value of limit and offset of the fetch API request, depending on the button I just clicked, but that does not happen... :(
This question as little bit more details, like the array of the fetch response (Client-side pagination of API fetch only with ReactJs).
I'll leave here Main.js code (no imports included):
export class Main extends React.Component {
constructor(props) {
super(props);
this.state = {
token: {},
isLoaded: false,
models: [],
offset: offset,
limit: limit
};
}
componentDidMount() {
/* here is other two fetches that ask for a token */
fetch(url + '/couch-model/', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'JWT ' + (JSON.parse(localStorage.getItem('token')).token)
}
}).then(res => {
if (res.ok) {
return res.json();
} else {
throw Error(res.statusText);
}
}).then(json => {
this.setState({
models: json.results
}, () => {});
})
}
render() {
const { isLoaded, models } = this.state;
if (!isLoaded) {
return (
<div id="LoadText">
Estamos a preparar o seu sofá!
</div>
)
} else {
return (
<div>
{models.map(model =>
<a href={"/sofa?id=" + model.id} key={model.id}>
<div className="Parcelas">
<img src={model.image} className="ParcImage" alt="sofa" />
<h1>Sofá {model.name}</h1>
<p className="Features">{model.brand.name}</p>
<button className="Botao">
<p className="MostraDepois">Ver Detalhes</p>
<span>+</span>
</button>
<img src="../../img/points.svg" className="Decoration" alt="points" />
</div>
</a>
)}
<PageButtons limit={limit} offset={offset}/>
</div>
)
}
}
}
And now PageButtons.js code:
export class PageButtons extends React.Component {
ButtonOne = () => {
let limit = 9;
let offset = 0;
this.setState({
limit: limit,
offset: offset
});
};
ButtonTwo = () => {
this.setState({
limit: this.props.limit + 9,
offset: this.props.offset + 9
});
};
render() {
console.log('props: ', this.props.limit + ', ' + this.props.offset);
return (
<div id="PageButtons">
<button onClick={this.ButtonOne}>1</button>
<button onClick={this.ButtonTwo}>2</button>
<button>3</button>
<button>></button>
</div>
)
}
}
Add below methods to Main.js
fetchRecords = (limit, offset) => {
// fetch call code goes here and update your state of data here
}
handleFirstButton = (limit, offset) => {
this.setState({limit : limit, offset: offset})
this.fetchRecords(limit, offset)
}
handleSecondButton = (limit, offset) => {
this.setState({limit: limit, offset : offset})
this.fetchRecords(limit, offset)
}
Main.js render method changes :
<PageButtons
limit={limit}
offset={offset}
handleFirstButton={this.handleFirstButton}
handleSecondButton={this.handleSecondButton}/>
PageButtons.js changes.
ButtonOne = () => {
let limit = 9;
let offset = 0;
this.props.handleFirstButton(limit, offset);
};
ButtonTwo = () => {
let {limit, offset} = this.props;
limit += 9;
offset += 9;
this.props.handleSecondButton(limit, offset);
};

Categories