anshul
anshul

Reputation: 791

How can i manipulate DOM inside the onScroll event in ReactJS?

I am trying to creating an arrow-up button, which will be set to display: "none" if I am at a top of my page, and when I scroll down further, I want to set it to display: "block". How can i achieve this? I am trying to manipulate the DOM inside my handleScroll function but it doesn't work.

My TopButton.js component

import React from "react";
import "../assets/arrow-up.png";

class TopButton extends React.Component {
  constructor(props) {
    super(props);
    this.handleScroll = this.handleScroll.bind(this);
  }

  componentDidMount = () => {
    window.addEventListener("scroll", this.handleScroll, true);
  };

  handleClick = () => {
    document.body.scrollTop = 0;
    document.documentElement.scrollTop = 0;
  };

  handleScroll = () => {
    console.log("scroll");

    let x = document.getElementsByClassName("topbutton_button");
    var x = document.getElementsByClassName("topbutton_button");
    // x.style.display = "none";

    // console.log(event.target.className);

    // if (
    //   document.body.scrollTop > 40 ||
    //   document.documentElement.scrollTop > 40
    // ) {
    //   style.display = "block";
    // } else {
    //   display = "none";
    // }
  };

  render() {
    return (
      <div className="topbutton_container">
        <button
          style={{ display: "block" }}
          onClick={this.handleClick}
          onScroll={this.handleScroll}
          className="topbutton_button"
        >
          <img src={require("../assets/arrow-up.png")} />
        </button>
      </div>
    );
  }
}

export default TopButton;

Upvotes: 3

Views: 221

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074138

There are at least two reasons it didn't work:

  • See this question's answers; basically, getElementsByClassName returns an HTMLCollection, not a single element, but your commented-out code was treating it as though it were a single element.

  • If your component was ever re-rendered, it would be rendered in its default state, not the updated state you changed via the DOM

But that's not how you'd do it with React. Instead, you'd:

  1. have the button state (whether it should be block or not) held as state in your component;

  2. use that state when rendering the topbutton_button, setting its style or class accordingly; and

  3. update that state in your handleScroll handler

A couple of others notes:

  • You also need to remove your handler when the component is unmounting

  • You shouldn't use arrow functions for component lifecycle functions

  • You don't need to use bind on an arrow function (handleScroll for instance). Either make it an arrow function or use bind in the constructor to bind it.

Something along these lines, see the *** comments

import React from "react";
import "../assets/arrow-up.png";

// *** Reusable function to decide whether we're "at the top" or not
function bodyIsAtTop() {
  return (
    document.body.scrollTop <= 40 &&
    document.documentElement.scrollTop <= 40
  );
}

class TopButton extends React.Component {

  constructor(props) {
    super(props);
    // *** Initial state
    this.state = {
      atTop: bodyIsAtTop()
    };
    // *** No need for the following if you use an arrow function
    // this.handleScroll = this.handleScroll.bind(this);
  }

  // *** Don't make this an arrow, make it a method
  componentDidMount() {
    window.addEventListener("scroll", this.handleScroll, true);
  };

  // *** Need to unbind when unmounted
  componentWillUnmount = () => {
    window.removeEventListener("scroll", this.handleScroll, true);
  };

  handleClick = () => {
    document.body.scrollTop = 0;
    document.documentElement.scrollTop = 0;
  };

  handleScroll = () => {
    // *** Update state (possibly; if the flag isn't different, this doesn't do anything)
    this.setState({atTop: bodyIsAtTop()});
  };

  render() {
    // *** Get the flag from state, use it below in style
    const {atTop} = this.state;
    return (
      <div className="topbutton_container">
        <button
          style={{ display: atTop ? "none" : "block" }}
          onClick={this.handleClick}
          onScroll={this.handleScroll}
          className="topbutton_button"
        >
          <img src={require("../assets/arrow-up.png")} />
        </button>
      </div>
    );
  }
}

export default TopButton;

There I've kept your arrow functions for handleScroll and handleClick. There's an argument for making them methods and using bind in the constructor instead, but it's mostly a style thing. (Well...style and it's easier to mock prototype methods for testing, which is a non-style reason for using prototype methods and bind.)

Upvotes: 3

Related Questions