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 – automatically scrolling
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. This is one path to automatically scrolling React components into view!
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