I'm trying to add infinite scroll in my web application.When the user scrolls down the page, there must be an API call to load the data beneath the existing data.So, the problem here is when I reach the bottom of the web page, the API is not being called.
import React from "react";
import { Link } from 'react-router-dom';
import axios from 'axios';
class InfiniteData extends React.Component{
constructor(props){
super(props);
this.state={olddata: [],newData: [], requestSent: false}
}
componentDidMount(){
window.addEventListener('scroll', this.handleOnScroll);
this.doQuery();
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleOnScroll);
}
doQuery() {
console.log("indoquery");
axios.get("https://jsonplaceholder.typicode.com/posts")
.then( res=>
this.setState({
olddata: res.data,
newData: this.state.olddata.concat(this.state.olddata)
})
)
.catch(console.log("error"))
}
handleOnScroll(){
var scrollTop = (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;
var scrollHeight = (document.documentElement && document.documentElement.scrollHeight) || document.body.scrollHeight;
var clientHeight = document.documentElement.clientHeight || window.innerHeight;
var scrolledToBottom = Math.ceil(scrollTop + clientHeight) >= scrollHeight;
if (scrolledToBottom) {
console.log("At bottom");
// enumerate a slow query
setTimeout(this.doQuery, 2000);
}
}
render()
{
return (
<div>
<div className="data-container">
{this.state.newData && this.state.newData.map((dat,i)=>
<div key={i}>
{dat.body}
</div>
)}
</div>
</div>
);
}
}
export default InfiniteData;
This is actually just an obscured case of not binding this correctly: the following line calls handleOnScroll using window (not the InfiniteData component instance) as this:
window.addEventListener('scroll', this.handleOnScroll);
Then, your setTimeout call is trying to call this.doQuery, which is undefined since window.doQuery doesn't exist.
If you bind this correctly for the EventListener, this should work out: either change to window.addEventListener('scroll', this.handleOnScroll.bind(this)); in componentDidMount or add the following line inside the constructor to keep it bound across the board:
this.handleOnScroll = this.handleOnScroll.bind(this)
Note: this isn't the problem you're having, but be careful inside your setState call--do you mean to use newData: this.state.olddata.concat(res.data)? (passing res.data to the concat call)
Following should work.
import React from "react";
function doQuery() {
console.log("indoquery");
// Do something here
}
class InfiniteData extends React.Component{
constructor(props){
super(props);
this.state={olddata: [],newData: [], requestSent: false}
}
componentDidMount(){
window.addEventListener('scroll', this.handleOnScroll);
doQuery();
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleOnScroll);
}
handleOnScroll(){
// .....
if (scrolledToBottom) {
setTimeout(function () {doQuery()}, 2000);
}
}
render() {
return (
<div>
</div>
);
}
}
export default InfiniteData;
Note that I have removed some code to make it compact and easier for you to understand where the problem lies. Basically the fix is where you define the doQuery function and how you pass that function to setTimeout
As Daniel Thompson stated, the problem is that you do not bind this. To elaborate: when the eventListener invokes the function this will not be bound to your component, but rather to window.
Binding this in the event listener will solve the problem of the code not being called, but will create a new problem. When you call handleOnScroll.bind(this) a new function is created, so when you try to un-register it, it will not unregister. To get both registration and unregistration to work as expected, save that new function and use it to register/unregister.
constructor(props){
super(props);
// Need to bind 'this' to refer to component.
// This will create a new function which will have to be stored so it can be unregistered
this.scrollListener = this.handleOnScroll.bind(this);
this.state={olddata: [],newData: [], requestSent: false}
}
componentDidMount(){
window.addEventListener('scroll', this.scrollListener);
this.doQuery();
}
componentWillUnmount() {
window.removeEventListener('scroll', this.scrollListener);
}
Related
When I leave a route component, I get the following warning:
Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an
unmounted component. This is a no-op.
The warning started appearing after I added the scroll event listener. How do I prevent this warning? I'm binding the event method only once. The event listener is added in componentDidMount and removed in componentWillUnmount.
class DashboardRoute extends React.Component {
constructor (props) {
super(props)
this.handleScroll = this.handleScroll.bind(this);
this.state = {
scrolled: false
};
}
componentDidMount () {
window.addEventListener('scroll', throttle(this.handleScroll, 250));
}
componentWillUnmount () {
window.removeEventListener('scroll', throttle(this.handleScroll, 250));
}
handleScroll (e) {
let scrolled = (window.scrollY > 0);
if (scrolled !== this.state.scrolled) {
this.setState({scrolled: scrolled});
}
}
render () {
return (
<div>
<!-- Component code -->
</div>
);
}
}
Since the scroll event handler can be delayed as much as 250 ms, your component might be unmounted before it is called. This depends on how DashboardRoute is used throughout your app.
You can use an instance variable called e.g. _mounted that keep track of if the component is actually still mounted or not and use that in your scroll handler before you use setState.
window.removeEventListener('scroll', throttle(this.handleScroll, 250)) will also try to remove a newly created function as scroll listener, so the listener created in window.addEventListener('scroll', throttle(this.handleScroll, 250)) will not be removed. You can create the throttled method once instead and reference that.
Example
class DashboardRoute extends React.Component {
_mounted = true;
state = {
scrolled: false
};
componentDidMount() {
window.addEventListener("scroll", this.handleScroll);
}
componentWillUnmount() {
this._mounted = false;
window.removeEventListener("scroll", this.handleScroll);
}
handleScroll = throttle(e => {
let scrolled = window.scrollY > 0;
if (this._mounted && scrolled !== this.state.scrolled) {
this.setState({ scrolled: scrolled });
}
}, 250);
render() {
return <div>{/* ... */}</div>;
}
}
Something that used to be simple is quite complicated when you don't know the React way.
I'm trying to create a component to act like a sticky header or footer when it reaches the top of the page.
At the moment I'm quite content in adding the below:
componentDidMount() {
window.addEventListener('scroll', this.onScroll, false);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.onScroll, false);
}
However I've hit a wall to how I get the scroll top position of a styled component. So lets say I had a styled component called Container and that outputted a form I usually just add a data attribute and do something like the below:
const container = document.getElementbyTag("data-sticky-container")
const position = container.offsetTop
How would I go about doing this in React?
Update
Now using ref. I've fallen into a problem where current isn't defined:
constructor(props) {
super(props);
this.optionBox = React.createRef();
}
componentDidMount() {
window.addEventListener('scroll', this.onScroll, false);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.onScroll, false);
}
onScroll() {
console.log(this.optionBox.current.offsetTop);
}
In react you would use a reference for your component: https://reactjs.org/docs/refs-and-the-dom.html
You would have something like this:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
getOffset = () => {
console.log(this.myRef.current.offsetTop);
}
render() {
return <Container ref={this.myRef} onClick={this.getOffset} />;
}
}
And now you can access the container offsetTop by this.myRef.current.offsetTop inside of your onScroll function like in getOffset
Another option is to use an innerRef. This can be better because it will give you a reference to the DOM node and not just a wrapper. See this answer.
You would then have more direct access to properties like scrollTop.
Unlike ref, innerRef uses a callback:
In your case it would look like:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef;
}
getOffset = () => {
console.log(this.myRef.offsetTop);
}
render() {
return <Container
innerRef={element => this.textInput = element}
onClick={this.getOffset} />;
}
}
You can add a ref
to the component for which u want an offsetTop. This ref will have all the computed css values of that component.
I'm making a simple scroll-to-top component and I thought that React will only re-render a component if something in it changes. Since I have a conditional tied to state in my render, shouldn't React only render it if the state changes? Instead, I'm seeing it re-render with every little scroll.
Also, if I left it as-is, are there any downsides to it re-rendering so much?
import React from 'react';
import './scroll-to-top.css';
export default class extends React.Component {
state = {
shouldShowButton: false
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll = () => {
this.setState({
shouldShowButton: window.scrollY > 250 ? true : false
});
}
render () {
{console.log("i have rendered!")}
return (
this.state.shouldShowButton ? <a className="scroll-to-top" href="#">Return to Top</a> : null
);
};
};
Welcome to Stack Overflow :)
Let's think through your code.
When the component loads, you're attaching a listener to the scroll event:
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
This fires handleScroll when the user scrolls. handleScroll sets the state of the component, regardless of whether or not the ternary condition resolves as true or false:
handleScroll = () => {
this.setState({
shouldShowButton: window.scrollY > 250 ? true : false
});
}
Whenever we use setState, React triggers render. Hence, render is triggering with every little scroll.
Downsides - you should be really careful of attaching anything to scroll, as it can affect performance. You might consider debouncing the event if you really, really need to do so. (Where debouncing is the technique of rate-limiting how many times a function can be called.)
This happens, because you are calling handleScroll function every time scroll event is fired. To fix this, setState only in condition:
import React from 'react';
import './scroll-to-top.css';
export default class extends React.Component {
state = {
shouldShowButton: false
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll = () => {
const {shouldShowButton} = this.state;
if (!shouldShowButton && window.scrollY > 250) {
this.setState({
shouldShowButton: true
});
} else if (shouldShowButton && window.scrollY <= 250) {
this.setState({
shouldShowButton: false
});
}
}
render () {
{console.log("i have rendered!")}
return (
this.state.shouldShowButton ? <a className="scroll-to-top" href="#">Return to Top</a> : null
);
};
};
No, it's typical for Component. It's re-rendered(not in DOM but in virtual DOM) each time .setState is called, props are changes or parent element is re-rendered.
Just an example how re-rendering parent also fires re-rendering for child:
import React from "react";
import ReactDOM from "react-dom";
class Child extends React.Component {
render() {
console.log('child re-rendered');
return 'test';
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {a: 1};
setInterval(() => this.setState(oldState => ({...oldState, a: oldState.a + 1})), 1000);
}
render() {
return (
<div className="App">
<Child />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Here you can check that child is re-rendered in the line as parent's .setState is called.
But it is not 100% to be issue for performance. Virtual DOM is much faster than browser DOM.
But if you want to avoid such a behavior you may use React.PureComponent instead of React.Component and then it will not be re-rendered on parent's update. Also PureComponent handles case when .setState does not actually changes value.
So there will be less re-rendering.
Official docs are good enough but here is also fine article at Medium
I've set up a StackNavigator which will fire a redux action to fetch data on componentDidMount, after navigating to another screen and going back to the previous screen, componentDidMount is no longer firing and I'm presented with a white screen. I've tried to reset the StackNavigator using StackActions.reset but that just leads to my other screens not mounting as well and in turn presenting an empty screen. Is there a way to force componentDidMount after this.props.navigation.goBack()?
Go Back Function
_goBack = () => {
this.props.navigation.goBack();
};
Navigation function to new screen
_showQuest = (questId, questTitle ) => {
this.props.navigation.navigate("Quest", {
questId,
questTitle,
});
};
Edit :
Hi , I'm still stuck on this matter and was wondering if the SO Community has a solution to force componentDidMount to be called after Navigation.goBack() is called or rather how to Unmount the component after this.props.navigation.navigate is called
The components are not removed when the navigation changes, so componentDidMount will only be called the first time it is rendered.
You could use this alternative approach instead:
class MyComponent extends React.Component {
state = {
isFocused: false
};
componentDidMount() {
this.subs = [
this.props.navigation.addListener("didFocus", () => this.setState({ isFocused: true })),
this.props.navigation.addListener("willBlur", () => this.setState({ isFocused: false }))
];
}
componentWillUnmount() {
this.subs.forEach(sub => sub.remove());
}
render() {
if (!this.state.isFocused) {
return null;
}
// ...
}
}
The didFocus event listener didn't work for me.
I found a different listener called focus which finally worked!
componentDidMount() {
const { navigation } = this.props;
this.focusListener = navigation.addListener('focus', () => {
// call your refresh method here
});
}
componentWillUnmount() {
// Remove the event listener
if (this.focusListener != null && this.focusListener.remove) {
this.focusListener.remove();
}
}
I created a HOC to listen for clicks outside its wrapped component, so that the wrapped component can listen and react as needed.
The HOC looks like this :
const addOutsideClickListener = (WrappedComponent) => {
class wrapperComponent extends React.Component {
constructor() {
super();
this._handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener('click', this._handleClickOutside, true);
}
componentWillUnmount() {
document.removeEventListener('click', this._handleClickOutside, true);
}
_handleClickOutside(e) {
// 'this' here refers to document ???
const domNode = ReactDOM.findDOMNode(this);
if ((!domNode || !domNode.contains(e.target))) {
this.wrapped.handleClickOutside();
}
}
render() {
return (
<WrappedComponent
ref={(wrapped) => { this.wrapped = wrapped }}
{...this.props}
/>
);
}
}
return wrapperComponent;
}
Whenever I click anywhere, I get the error "Uncaught Error: Element appears to be neither ReactComponent nor DOMNode" on the _handleOutsideClick callback.
Any ideas what could be causing this ?
Update:
OK so the source of the error is that "this" inside _handleClickOutside is now referring to 'document', which is what is expected
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#The_value_of_this_within_the_handler
This looks to be an absolute mess - it seems I can either bind the event correctly but then not be able to unbind it, or I can unbind it correctly but the binding method will throw an error...
Try using this -
constructor() {
super();
this._handleClickOutsideRef = this._handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener('click', this._handleClickOutsideRef, true);
}
componentWillUnmount() {
document.removeEventListener('click', this._handleClickOutsideRef, true);
}
Binding has to be done like this -
constructor() {
super();
this._handleClickOutside = this._handleClickOutside.bind(this);
}
or use arrow function for _handleClickOutside.
_handleClickOutside = (e) => {
// 'this' here refers to document ???
const domNode = ReactDOM.findDOMNode(this);
if ((!domNode || !domNode.contains(e.target))) {
this.wrapped.handleClickOutside();
}
}