Automatically scrolling React component into view upon render

Person using computer presumably writing React

You’ve just finished up your fancy new React component and got it into the workflow. You now perform an action on the page. As a result your component renders but it is under the fold and isn’t visible. You want it to be visible immediately. What can you do? Today let’s talk about automatically scrolling React components into view upon render.

Code for today’s post can be located on this codesandbox project.

Getting Started

I’m pretending you have set up your React application already and will jump right to the meat. I used the default create-react-app template on codesandbox.io to create this example today.

I’m adding the dependency for smoothscroll-polyfill so that this example will work on less rich browsers such as Microsoft Edge. Add that to your index.js like so:

import * as smoothscroll from "smoothscroll-polyfill";
smoothscroll.polyfill();

Scroll Helpers

For any of today’s code to work we really need a helper to funnel functionality through. I’m creating scroll-helpers.js to contain them. For organization-sake I’m also putting it in a sub-folder titled helpers. Before I show you the code I will explain what’s going on.

I’m putting four static methods–scrollTo, scrollIntoView, setScrollPosition, and scrollTop— into this JavaScript class. These methods only ever operate on passed parameters. As such, let’s make them static. For today, however, we’re focusing on scrollTo and scrollTop.

scrollTo

ScrollTo is analogous to window.scrollTo and when we dig into it you’ll see that I call it. We will check first to see if we have a window object. Failing that, we shortcut out. This is to allow for use within a prerendered component without having to have magic switches on the outside.

If our check for the window object passes we then use the window object and requestAnimationFrame. Here is where we’ll make some calculations and handle body top padding if necessary. Top padding is common for floating/fixed headers.

Last, we will call window.scrollTo based on our calculated position and intended behavior.

I’m having a hard time remembering where all the inspiration for this method came from. If it looks familiar let me know so I can give credit where/if due.

Code
  static scrollTo(element, delay = null, behavior = "smooth") {
    if (typeof window === "undefined") {
      // console.log("no window");
      return;
    }

    window.requestAnimationFrame(() => {
      let offset = element.offsetTop;
      try {
        let bodyRect = document.body.getBoundingClientRect();
        let bodyStyle = window.getComputedStyle(document.body, null);

        // need to handle the padding for the top of the body
        let paddingTop = parseFloat(bodyStyle.getPropertyValue("padding-top"));

        let elementRect = element.getBoundingClientRect();
        offset = elementRect.top - paddingTop - bodyRect.top;
      } catch (err) {
        console.log("oh noes!");
      }

      if (delay) {
        setTimeout(() => {
          window.scrollTo({ top: offset, left: 0, behavior });
        }, delay);
      } else {
        window.scrollTo({ top: offset, left: 0, behavior });
      }
    });
  }

scrollTop

Next in our toolbelt is scrollTop. This method is infinitely more simple. As with scrollTo we first check to make sure window is around. If so, we call window.requestAnimationFrame and inside there call window.scroll. Here we pass in 0 for top and left edges and our desired scrolling behavior.

Code
  static scrollTop(delay = null, behavior = "smooth") {
    if (typeof window === "undefined") return;

    window.requestAnimationFrame(() => {
      if (delay) {
        setTimeout(() => {
          window.scroll({ top: 0, left: 0, behavior });
        }, delay);
      } else window.scroll({ top: 0, left: 0, behavior });
    });
  }

AutoScroll

Now that we have our basis for scrolling we have a couple ways to approach this. Since we’re using React it could seem appropriate to use a Component for it. Let’s do that.

Our AutoScroll component is going to have a single property in state. This property will track whether or not it should scroll. Now, this could (and likely should) be done as a stateless (functional) component. For now, however, we’ll do it as a component.

Let’s discuss the parameters of this component. It needs to know whether or not it should trigger the scroll. The component also needs to somehow know where to scroll to. Lastly, it needs to scroll. To make it more interesting, maybe the presence of this component should force a scrollTop.

Where to Scroll

I know this is out of order but it becomes important sooner than later. In order to know where to scroll our component can target self or top. The default is self. You will see that in our render statement we include a ref="autoscroll" on a div. Our trigger will make use of that in self mode. In top mode it is not used.

When to Scroll

Our component will initially render with shouldScroll as true. Upon mounting we check to see if it is true and, if so, move on to triggering the scroll. One very important thing to note is since we’re jumping out of the React lifecycle we can “break” our scroll if we unmount the component too quickly. Our componentDidMount can optionally call back to the parent via a mounted func prop. You’ll want a delay on this. I have it tied to the delay on the overall component but mileage may vary. Feel free to change it to a separate prop or code a value.

Triggering the Scroll

Inside componentDidMount if we are in scroll mode we will scroll in one of two ways. If we are targeting self we will call our scroll-helper and instruct it to scrollTo our ref with an optional delay. I highly recommend the delay and found in a lot of cases it is necessary to overcome delay between updating the virtual DOM and updating the browser. Play around with values, I found 500 (that’s milliseconds) to be a decent value.

Triggering the scroll in top mode will call our scroll-helper scrollTop method. Delay is less important here since the top will already be there. We’ll support it anyway.

Code

import * as React from "react";
import PropTypes from "prop-types";

import ScrollHelpers from "../helpers/scroll-helpers";
import { isNullOrUndefined } from "../helpers/object-utils";

export default class AutoScroll extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      shouldScroll: true
    };
  }

  componentDidMount() {
    if (this.state.shouldScroll === false) return;

    const { delay, mode } = this.props;

    if (mode === "self") {
      ScrollHelpers.scrollTo(this.refs.autoScroll, delay);
    } else {
      ScrollHelpers.scrollTop(delay);
    }

    setTimeout(() => {
      this.setState({ shouldScroll: false }, () => {
        if (!isNullOrUndefined(this.props.mounted)) this.props.mounted();
      });
    }, delay);
  }

  render() {
    return (
      <div>
        <div ref="autoScroll" />
      </div>
    );
  }
}

AutoScroll.propTypes = {
  delay: PropTypes.number,
  mode: PropTypes.oneOf(["self", "top"]),
  mounted: PropTypes.func
};

AutoScroll.defaultProps = {
  delay: null,
  mode: "self"
};

I see I threw in another helper we didn’t previously talk about. In real life I got sick of having to check if something was null or undefined. In order to clean that up I made a couple helpers. I’ve pasted those into the project–isNullOrUndefined and isNullOrEmpty. We’re not using isNullOrEmpty in our examples today.

Demo

If you haven’t already pulled up the codesandbox full project, you can view just a demo of it here. You’ll want a sufficiently small enough window, <680px tall. I did add a small media-query to check for a tall window but didn’t want to spend too much time here.

For the demo I have set up a few different components:

  • demo-container.jsx: Main demo container that will turn on/off the demo1 and demo2 components. Also references the filler content and has a “Back to Top” button. Back to top makes use of ScrollHelper.scrollTop.
  • demo1.jsx: Demo component wrapping the AutoScroll component. It uses self mode and a delay of 500.
  • demo2.jsx: Demo component making direct use of ScrollHelper.scrollTo to demonstrate scrolling to an element on the component.

Conclusion

Automatically scrolling React components into view is a fairly simple process. It does require us to jump outside the lifecycle a little and the results may vary. I’m certain there are many other ways to accomplish this task, this is just how I chose to handle it.

Credits

Photo by Thought Catalog from Burst