DEV Community

loading...
Cover image for “Scroll Restoration”, React Router and my custom solution for React Studyboard

“Scroll Restoration”, React Router and my custom solution for React Studyboard

jesusramirezs profile image jesusramirezs ・5 min read

I keep working on improvements for React Studyboard

Github repository: https://github.com/jesusramirezs/react-studyboard

I would like to write in this article about:

  1. “scrollRestoration” and React Router.
  2. My solution to resume reading a text at the point where it was last left.

1. “scrollRestoration” and React Router

According to developer.mozilla, “The scrollRestoration property of History interface allows web applications to explicitly set default scroll restoration behavior on history navigation.” (https://developer.mozilla.org/en-US/docs/Web/API/History/scrollRestoration).

This browser feature has raised some debate in the past when using React Router, especially when it comes to unwanted performance. For example, in a SPA (Single Application Page), when we navigate through React Router from one "page" to another, the browser keeps the scroll of the first page on the next one, instead of positioning itself at the beginning of the new page as it would be more logical and natural.

See, for example, the following conversation when some time ago, the problem was detected and where a solution is beginning to emerge:

https://github.com/ReactTraining/react-router/issues/252

There are times when it is desirable to maintain this performance and other times when it is not.

After some time trying to address the issue with partial solutions, officially, React Router has chosen not to offer support to control this property. According to the documentation:

"In earlier versions of React Router we provided out-of-the-box support for scroll restoration and people have been asking for it ever since....Because browsers are starting to handle the “default case” and apps have varying scrolling needs, we don’t ship with default scroll management."

(https://reactrouter.com/web/guides/scroll-restoration)

As a result, when it is desired to dispense with automatic scrolling, especially in SPA's, the developer must adapt his solution, as described in the same guide or examples like this one:

https://spectrum.chat/react/general/handling-scroll-position-on-route-changes~1e897a67-c05f-40c0-946b-d459d93615bf

2. My solution to resume reading a text at the point where it was last left

So, for example, in my case, to prevent this performance in the most reliable way, I have placed in the "header" component the following code to disable the “scrollRestauration” property of “window.history”:

    useEffect(()=>  {
        if ("scrollRestoration" in window.history) {
            window.history.scrollRestoration = "manual"
          }
    },[]);

Enter fullscreen mode Exit fullscreen mode

And for those components where I want the page to be displayed from a scrolling position at the top of the page, I use the following code:

    useEffect(()=>  {
        window.scrollTo(0, 0);
      },[]);
Enter fullscreen mode Exit fullscreen mode

But there is a particular case in which I find it necessary to maintain the browser scroll position when visiting a page for the second time: the article page, which is the essential page in the app. Thus, when I want to resume reading an article, which could be extended, I find it convenient that the browser positions me at the point where I left the reading for the last time, something like a virtual page mark.

Alt Text

I consider this functionality vital since it contributes to significantly improving the app's user experience by maintaining control over the reading and saving the reader time every time he or she returns to any of the articles.

Also, I think it is interesting that the list of articles in a category or section shows the progress made in reading each of them.

In addition, the solution adopted to this problem can be used so that when you click on annotations, the application not only navigates to the article but position us exactly in the paragraph to which it refers.

The solution seems simple; it could store in Redux (the status manager I use in the project) the last scroll position of each article since the last login to the page, reading, for example, the window.pageYOffset property, and when returning to the page, make a scrollTo to the previously stored position.

This window.pageYOffset property is monitored to show a thin reading progress bar at the top of the page.

But this simple solution has some problems:

  • The app allows you to modify the preferred font used in the articles' text and their size. If these properties are modified between two accesses to the same article, there is a possibility that the position of the scroll will not be correct since the height of each line will probably have changed.

  • If the author modifies the article's content between two reading sessions, adding new text or images, or something foreseen by future new features, the content is dynamically enriched by new content provided by other readers. Also, the reading position based on an offset will not be valid.

  • Afterward, it appears to make more sense to mark the last reading position based on the paragraphs visible in the browser at a given time rather than the offset.

In the article.component, the text is divided into “paragraphs” (which may contain text or other content such as images or video).

Each of these paragraphs is managed by TextBlock component (pending renaming to a more appropriate name).

The design decision is because this way, unrelated functionalities are separated, making the code more readable. This TextBlock component deals with things like highlighting text, formatting Markdown, and displaying or editing annotations.

Each TextBlock instance is embedded in a component called VisibilitySensor, provided by the “react-visibility-sensor” package.

Alt Text

This package provides a very useful feature for our purposes: it detects when a component becomes visible or invisible in the browser or inside another component depending on the scroll position.

<VisibilitySensor scrollCheck={true} scrollThrottle={1} partialVisibility={true} onChange={visibilityChange(key)} >
Enter fullscreen mode Exit fullscreen mode

Each time a change occurs in a component's display, we check whether it is due to an upward or downward scroll and thus determine which the first active paragraph on the page is:

    const visibilityChange = (key) => (isVisible) => {


      const previous_scroll = lastScroll.current;
      const new_scroll = (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0);

      if(new_scroll < previous_scroll) {

        if((!isVisible && new_scroll > previous_scroll) || (isVisible && new_scroll < previous_scroll)) {

          dispatch(updateProgressAtReadingStatus({articleId: article.articleId, progress: calcProgress(), textBlockId: key}));

          lastScrollTime.current = Date.now();
          lastScroll.current = new_scroll;
        }

      }
      lastScroll.current = (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0);

    }


Enter fullscreen mode Exit fullscreen mode

Then, the identificator of this new active paragraph is sent to Redux:

dispatch(updateProgressAtReadingStatus({articleId: article.articleId, progress: calcProgress(), textBlockId: key}));
Enter fullscreen mode Exit fullscreen mode

And here is the second part of all this. Once we resume reading the article, the first thing we do is reading the first active paragraph:

useSelector(state => selectArticleLastTextBlockId(article.articleId)(state), equality_selector);
Enter fullscreen mode Exit fullscreen mode

And then scroll to the position of that paragraph:

scrollRef.current.scrollIntoView({ behavior: 'smooth', block: 'start'}); 
Enter fullscreen mode Exit fullscreen mode

Here is a interesting discussión about scrollIntoView:

https://stackoverflow.com/questions/48634459/scrollintoview-block-vs-inline/48635751#48635751

My conclusion is that a seemingly simple feature requires some developmental effort and creativity. Thanks to the numerous components available, it is possible to arrive at acceptable solutions in a short time.

Thanks for reading this article. Any feedback will be greatly appreciated.

Connect with me on Twitter or LinkedIn

Discussion (0)

pic
Editor guide