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?
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>
</>
);
}