3

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;
anshul
  • 661
  • 9
  • 27

1 Answers1

3

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.)

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thanks for answering, I made it work in a different way whatsoever. My approach was to **1)** Replace the className by id and thus `document.getElementById("buttonid")` _buttonid is my ID for button_ But the problem with this approach is that the button would render for the very first time. **2)**In order to remove this inconsistency, i set the `display = "none"` in my `componentDidMount()` method. And it is working fine. Do you see a negative side with this approach? – anshul Dec 01 '19 at 09:03
  • @AbhinavAnshul - It's working against the library rather than using it, and it means you can't use more than one copy of the component (unless you take the `id` as a prop). It probably *works*, though. – T.J. Crowder Dec 01 '19 at 09:20