Search code examples
javascripthtmlreactjsbrowserscroll

Server side rendered element should be scrolled to bottom on initial render


I’m building a React server side rendered application (Next.js) and have a scrollable <div> that I want to be scrolled to the bottom on initial load of the page before React has hydrated the component.

That means a useLayoutEffect() hook like the following is not sufficient because there will be a brief flash of the element scrolled to the top before React mounts and runs this hook.

useLayoutEffect(() => {
  const element = ref.current;
  element.scrollTop = element.scrollHeight - element.clientHeight;
});

The approach that I think will work to prevent this is adding a <script> tag in the server side rendered page. For example:

function MyComponent() {
  return (
    <>
      <div style={{overflowY: "scroll"}}>
        ...
      </div>
      <script>
        var element = document.currentScript.previousElementSibling;
        element.scrollTop = element.scrollHeight - element.clientHeight;
      </script>
    </>
  );
}

However, I’ve found that when the <script> executes the clientHeight of the <div> element is 0.

Presumably, I need to wait for the browser to finish loading the DOM so I added a DOMContentLoaded listener:

function MyComponent() {
  return (
    <>
      <div style={{overflowY: "scroll"}}>
        ...
      </div>
      <script>
        var element = document.currentScript.previousElementSibling;
        document.addEventListener("DOMContentLoaded", function() {
          element.scrollTop = element.scrollHeight - element.clientHeight;
        });
      </script>
    </>
  );
}

However, with this approach it appears the browser still does a paint with scrollTop set to 0 and then processes the DOMContentLoaded event handler.

How can I write a <script> here that executes the “scroll to bottom” behavior after DOM properties like element.clientHeight have loaded but before the first browser paint so the user doesn’t see a flash of the wrong scroll position.

Is there a better approach than this?


Solution

  • This version actually works in production. It doesn’t work in development because Next.js use’s Webpack’s style-loader in development which means styles don’t load until the JavaScript code has loaded and the JavaScript code is loaded at the end of the document <body>.

    So in production element.clientHeight will be correct (since the CSS was loaded in the <head>) but in development element.clientHeight will be 0 (since the CSS will be loaded at the end of <body>).

    function MyComponent() {
      return (
        <>
          <div style={{overflowY: "scroll"}}>
            ...
          </div>
          <script>
            var element = document.currentScript.previousElementSibling;
            element.scrollTop = element.scrollHeight - element.clientHeight;
          </script>
        </>
      );
    }