Tim
Tim

Reputation: 13

Why is React rendering my component over and over with an unchanged state?

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

Answers (3)

John McCollum
John McCollum

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

skyboyer
skyboyer

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

Azamat Zorkanov
Azamat Zorkanov

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

Related Questions