“Scroll Restoration”, React Router and my custom solution for React Studyboard
Tags: react, javascript, webdev, showdev
Published: 2020-12-21 10:13:03

I keep working on improvements for [**React Studyboard**](https://dev.to/jesusramirezs/react-studyboard-react-hooks-redux-5h5p)

**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”:

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

```

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:

```js
    useEffect(()=>  {
        window.scrollTo(0, 0);
      },[]);
```

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](https://dev-to-uploads.s3.amazonaws.com/i/7hcre23btg3qu93a9xmu.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/vk18r9zs2mvai43cme49.png)

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.

```js
<VisibilitySensor scrollCheck={true} scrollThrottle={1} partialVisibility={true} onChange={visibilityChange(key)} >
```

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:

```js
    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);
    
    }
 

```

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

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

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:

```js
useSelector(state => selectArticleLastTextBlockId(article.articleId)(state), equality_selector);
```

And then scroll to the position of that paragraph:

```js
scrollRef.current.scrollIntoView({ behavior: 'smooth', block: 'start'}); 
```

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](https://twitter.com/jesusramirezs) or [LinkedIn](https://www.linkedin.com/in/jesusramirezserran/?locale=en_US)