Building a sticky header component in React

Using React 16 refs and requestAnimationFrame for a highly-generic (hopefully not too terrible) component

Chris Garcia
TrendKite Dev
5 min readJul 2, 2018

--

My employer’s (TrendKite, more info at the bottom) stack uses loads of microservices and at least 3 git repositories that include customer-facing front-ends. We have even more if you count HTML email as a front-end or include internal tools. Not only that, but we use a mixture or React and older Angular in the app, for legacy reasons.

Because of the multiple front-end repos, our team has switched to building most of our UI out of re-usable, generic, React components. We use a private NPM repo to host a UI project, and voilà — all of our React apps at least use the same core components with minimal fuss.

Recently, I was tasked with building a sticky footer. I’d seen at least 2–3 places in our app where developers did various things with scroll-watching and setting styles to position: fixed, and I thought it was time to add a <Sticky/> component to our UI library.

Goals

Since this component is supposed to be super re-usable, I wanted it to:

  • stick to the top, bottom, left and/or right of it’s parent,
  • work even when it’s scroll parent isn’t the main window,
  • not make assumptions about what to do when “stuck” (for instance, some sticky headers have UI elements that shrink or change visibility when they’re stuck),
  • and, finally, have a super easy interface.

And right here, I’ll go ahead and try to define my interface. I know what I want the component to do, and I have a pretty good idea of what I should need to get there. Any bloat to this minimal interface will constitute a code smell.

Our team uses React PropTypes, so here’s a propType object showing the interface:

The interface

Consuming the “stuck” status

In our UI components, we use a higher-order component (HOC) called withModifiers. To use it, you need to follow this convention: your wrapped component must accept a className string prop, and add that className to the component’s base class. This HOC was pretty trivial to write, so I haven’t bothered providing it in the example code.

Our team uses a slightly modified BEM pattern to scope all our components’ styles, and the withModifiers component gives us a really easy way to turn an array of strings like ['small', 'warning'] into a string of classes. For a component <Button/>, for example, you’d end up with .button.button--small.button--warning. It’s not perfect, but it serves us well.

Our <Sticky/> component assumes that it can pass an array of strings into its children components as a modifiers prop.

Here’s an example showing how a component could consume its “stuck” status…

So the child gets a list of modifiers and loops over them. It adds each one to it’s base class (“ExampleStuckComponent”) as a class suffix, or modifier class in BEM parlance.

You don’t need to implement a HOC or BEM pattern to use this component! You just need to know that <Sticky/> will wrap your component and add 1–4 strings to it in a modifiers array.

The possible values are: “stuck-top”, “stuck-bottom”, “stuck-left”, and “stuck-right”. You can have more than one of them at once. Its up to you to figure out what to do with them.

The approach

So now I know my inputs and outputs. To work!

I know I need to watch for scroll actions, check for scroll position, and pass modifiers when my component is scrolled near an edge of its scrollable parent area.

These next few example files are stripped down versions of the real thing — read on for the finished product.

Watching for scroll actions

I like to stub out addEvents() and removeEvents() functions. I’ll run the first one in componentDidMount and the other in componentWillUnmount.

The only interesting part of this is consuming the ref passed as a scrollTarget conditionally. If the component doesn’t get a scrollTarget prop, we’ll just use the window object.

Handling the scroll is also pretty simple — I just take measurements of the scroll target and the current component and do basic math.

I use a React ref to get a reference to the current component’s root div, and compare that to either the window object or a custom scroll target’s ref. That introduces some funk because the way you get dimension/position for the window is a little different than it is for DOM elements. The two outputs are pretty easy to munge together, though.

You see where I created a scrollRect object. Its properties are a subset of the result of getBoundingClientRect() on a DOM element. If I get the window as a scroll object, I have to populate it manually with compatible values (x, y, width, height, etc.). Other than that, I’m just setting local state to an object with camel-cased equivalents of my modifier strings, stuckTop, etc.

Finally, there’s a render function. It turns the camel-cased state.stuckTop information into modifier strings that we use for styling. It also links the ref this.stickyDiv to the root div of the component.

So, on your child component, use the style pattern of your choice to add .div--stuck-top {position: fixed; top: 0; left: 0; right: 0;} or whatever else you need.

Twists and turns along the way

Once I had a pattern I liked, I immediately started reading old articles about scroll performance. Even back in the jQuery days, I remember scroll events firing at high rates and debouncing strategies coming into play.

I’ve been reading a little bit about window.requestAnimationFrame() and it seemed like the “right” way to handle debouncing with modern JavaScript.

Without going into a ton of detail, I removed direct references to handleScroll() from my event handlers, and replaced them with calls to a new function, debouncedScroll(). Then, in that function, I pass the handleScroll() function as a callback to requestAnimationFrame(). That tells the browser to schedule the DOM fetching, measuring, and state setting until the next frame. I do have to keep track of the last animation frame requested, so I can clear the last one in my removeEvents() method when I clear event handlers.

Result!

Here’s a working example on glitch.com (which is awesome).

What’s next?

For anyone who’s interested, you can find Sticky on GitHub. I’d welcome issues, requests, and documentation suggestions.

Performance

I suspect there are still performance enhancements to be done here. I frankly haven’t done a lot of performance benchmarking of UI elements, so there’s that.

Resize watching

This iteration of <Sticky/> also doesn’t watch for resize events on the scroll area, so, if you switch from landscape to portrait mode or switch between tabs inside the scroll area, you can cause a bad state until the user scrolls.

It needs a fix, but I’m not sure of the least invasive way to approach it. Component-level resize watching isn’t broadly supported, and the polyfills I’ve found do a lot of manipulation to the watched element.

I might try a vanilla ResizeObserver implementation, knowing it will only work on Chrome, while I look for better solutions.

Tests

As in, I haven’t written any, and I’m doing a lot of event watching, so it will be interesting getting unit test coverage.

I’m a front end developer at TrendKite, working on measuring and automating PR. We’re always hiring!

--

--