One of my pet peeves in modern Javascript driven web development is breaking the user’s expectations how browsers work. The most common one is not using the URL for state. It is highly frustrating when you interact with a site and press the back button and it takes you waaay back discarding everything you did in between or it just does not restore the UI state when you came back to it after visiting some other page.
I’m not the only one, just ask her, she makes the point better than I ever could! 😁
These problems do not exists at all when using full server-side rendering without Javascript because the only practical way you can save the UI state is the the URL and a database. But why is it so common with Javascript framework driven implementations? My guess is that the memory based state containers like useState()
in React.js are so much easier to reach for than manipulating the URL.
Back/forward cache to the Rescue?
Heck, I think the amount of badly implement UIs one of the reasons why browser vendors have implemented the Back/forward cache: While the main point is to speed things up when using the back button but because it caches the page memory it will actually restore the memory based state containers like the useState()
in React.js too! This is really cool and somewhat solves a common case where you have a Javascript based search interface which renders links, clicking link makes a full page navigation and the cache restores the state when using the browser back button to return.
But as the name states: It is only a cache. It may be discarded if the user stays on the target page for too long or it might not be created at all in the first place if the conditions are not applicable. Also it does not help with the intermediate states because there is no page loads. At the time of writing the code browser on npmjs.com is an infamous example of this. On a package try navigating couple levels deep into the file tree and hit the back button: It takes you back to the package readme instead of the previous level!
Use the URL!
So instead of using useState()
or your favourite framework’s equivalent, take a look if the framework has a build-in tool to use the URL as the state instead. Plain React.js does not have one but consider using the useSearchParams() hook from React Router or the Next.js’ routing tools.
Scroll Position
Another pitfall developers ofter fall into is not handling the scroll position. On server-side rendered apps the browser will take care of it but with Javascript based ones it needs special handling when using client-side fetch()
to the load the data. The browser will try to restore the scroll position after you pressed the back button but if the UI renders only after the data is loaded the native scroll position restoring is broken because the content is not long enough to scroll yet on the page load.
It is possible handle this manually by saving the scroll position to the history state using history.replaceState()
history.replaceState(
{ myScrollTop: document.documentElement.scrollTop },
"",
location.href
);
and restoring it only after the full content is rendered
document.documentElement.scrollTop = history.state.myScrollTop;
This method can be used to handle other scrolling elements as well, like div-elements with overflow: scroll
.
Restoring infinite scrolling position
A popular modern user interface is infinitely scrolling lists that automatically load more data when the user approaches the end of the list, but it also gets a lot of hate and I think it is because it so easy to implement badly: Again not restoring the correct results and the scroll position when the coming back to it using the back button.
To implement it correctly you must save the scroll pagination index as well and on back navigation use that to re-fetch all the pages. Alternatively you could save the actual results with the added bonus of skipping the network request which is not only faster but prevents content flashing.
This is a lot of work!
My advice is to use your framework’s routing tools as much as possible. Like the route loaders and the ScrollRestoration component in React Router. When you have a single tool that manages the data loading and routing it can also handle the scroll position because it knows when the page is fully rendered with the data. Look for similar tools in the framework of your choice. Implementing this manually is hard and error prone.
As you might have already guessed, this is something we have spend a lot of time on at Findkit so you would not have to worry about it when using the Findkit UI library which also manages the data loading, its own subset of routing aaaand scroll positions. You are welcome! 😎 Just try it 👉