A solved problem back from the dead and the open-source library that took it on.

In all the fury and excitement for ditching server-generated web pages for client-side applications, we've found ourselves as web developers taking on more and more responsibilities previously delegated to the browser. Everything lives on a single page now after all, and for better or worse the browser can't help us out with all the nice semantics that come along with the concept of a "page". A prime example is keeping the URL in the nav bar synchronized with the actual page the user is viewing. Generally, we end up resorting to frameworks and libraries to fill the gap.

In developing our partner dashboard, we found ourselves nostalgic for yet another feature thrown out with the bathwater: reseting the scroll position to the top when a user switches pages. Without tender care, you end up with this:

Not Our Dashboard

This behavior makes my eyelid twitch. I expect and want to be transitioned to the top of the list when I click a link in the sidebar.

For users of React and react-router, there are some simple one-line fixes. They basically amount to resetting the scroll position if the URL path changes. We found this to be a bit too crude (for instance, with tabbed interfaces where each tab maps to a unique path) so we rolled our own solution and open sourced it as react-simple-scroll. It's simple and works well, but don't take our word for it:

I've never wanted to move to frontend development more thanks to react-simple-scroll.
— Tadas Vilkeliskis

Design

Our goals in a solution were roughly:

  1. By default, make all URL path changes reset the scroll position
  2. Exempt certain URL path changes from resetting the scroll position (e.g. tabbed interfaces)
  3. Integrate coherently with React and react-router
  4. Make it simple

Every goal except 2 is solved with a one-liner in a hook provided by react-router (basically, on route transition, reset the position). An easy solution to 2 would have been to, in the same hook, include imperative logic that exempted certain route transitions:


Of course, there are ways we can clean up a bit of the boilerplate, but a problem is crystalizing in front of us: to specify these exemptions, we're going to have to write logic for every possible route transition, i.e. Tab 1 to 2 and 3 to 2 and 2 to 3, ... wait about about Tab 1 to 1? With this imperative approach, we may end up declaring on the order of N^2 route transitions for N routes. With nested routes, things might get even messier to implement and maintain.

As an alternative, and in the spirit of declarative APIs, we've found decorating our existing routes allows us to simply express what we want without accounting for an exponential number of route transitions. It looks something like this:

A react-router v3 route element

In our route tree, we annotate the groupings of routes we consider "of the same page" with the scrollFrame prop. Any route's frame is found by starting with itself and looking up the tree for the closest route which has the scrollFrame property. If the app transitions from route A to route B and they have the same scroll frame, the scroll position is not touched. If they're different, the scroll position is reset to the top. In this example, if we move:

  • from /bloop/bleep to /bloop/bleep/blorp we won't reset
  • from /bloop/bleep to /bloop we will reset

We've moved away from thinking about route transitions and towards thinking about the properties of a grouping of routes. It's the same principle that has made React's API such a boon to writing client-side apps.

Installing

react-simple-scroll can be found anywhere npm packages are distributed.

$ yarn install react-simple-scroll

One important gotcha: this was built against an older version of react-router and we're to understand that a few APIs have shifted around since— so your mileage may vary. We think the core premise is still applicable and would be interested in seeing adaptations to new versions!

We welcome all comments, criticism, and contributions. Of course, if thinking about this sort of thing really lights your fire, we're always hiring.