Reputation: 13
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
);
};
};
Upvotes: 1
Views: 86
Reputation: 5142
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.)
Upvotes: 4
Reputation: 23763
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
Upvotes: 0
Reputation: 819
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
);
};
};
Upvotes: 0