Popstate and history API — the missing part
Recently, I had to deal with a use case in which a user clicks on the (native) back button- and the page, whose state is changed by the pushstate method, does not respond as expected.
First, let’s understand what is the life cycle of the HTML DOM (yeah, angular and react are not invited…)
The lifecycle of an HTML page has three important events-
(Taken from javascript.info)
- DOMContentLoaded — the browser fully loaded HTML, and the DOM tree are built, but external resources like pictures <img> and stylesheets maybe not yet loaded.
- load — not only HTML is loaded, but also all the external resources: images, styles, etc.
- beforeunload/unload — the user is leaving the page.
Each event may be useful:
- DOMContentLoaded event — DOM is ready, so the handler can lookup DOM nodes, initialize the interface.
- load event — external resources are loaded, so styles are applied, image sizes are known, etc.
- beforeunload event — the user is leaving: we can check if the user saved the changes and ask them whether they really want to leave.
- unload — the user almost left, but we still can initiate some operations, such as sending out statistics.
For events handling you can read here or in the MDS doc.
Now — Let’s take a quick look into the history API:
The history API is there to give us more control over the site’s state. before the history API, we had to handle it by concatenating a hash to the URL, a questionable solution since the hash was coupled to an anchor with id in the document itself.
There are some main methods in the history API — the most common being:
history.back() — go back in the browser history.
history. forward() — you can guess what this method does…
history.go(<number>) — the number represents the number of steps backward (negative number) or forward (positive number).
Now Let’s focus on the advanced method:
History.pushState(<data> , <title>, <url>) — in this method we replace the state of the website — and it will be reflected in the window.location.href a.k.a the URL.
<data> — any data you want to save in history.state ( by default — when the browser has loaded the history.state === null )
<title> — the title in the document itself.
<url> — this is the interesting part — you can change the url without navigating to any page.
After we push the state to the history — we can check the history.length.
history.replaceState(<data>, <title>, <url>) — parameters are the same, but the behavior is a little bit different — instead of adding state to the history — you replace the current state.
Time to talk about popstate.
The popstate event is being fired either when history.back is called and state is removed from the stack, or when a user presses on the native back button.
The interesting part, and the purpose of this article is to introduce you to this section of the MDN doc :
In my case, I added an EventListener to the pop state event — and something went wrong …. The URL changed but the event callback was not called …- and here is the tricky part — the ‘popstate’ was not fired until the ‘load’ event did!
I’ll write it again — the popstate was not fired until the ‘load’ event — which means — if you have a heavy page with a lot of resources, the popstate event will be greatly delayed.
Let’s see an example here :
link to the code- (codeSandBox)
Say I have a react app with a heavy image that takes time to load,
I manually replace the state with pushState method — and right away call history.back() — as the default back button does —
also in this code, I console log the ‘DOMContentLoaded’,’load’ and ‘popstate’ events (you can find it the index.html — at the public folder)-
This is the result in the console: (with fast 3d test / normal network)
As you can see, the react application states that the DOM is ready — but not all the content is loaded…
Summary:
If you have a site with a lot of resources that takes a long time to load, and you make changes to the history state, when the user will press on the back button, the UI will not respond as expected.
So these are the options -for solutions:
- Delay the state changes until the ‘load’ event is called
- Add an interval that checks on the location.href changes — and respond to these changes (don’t forget to clean it when the ‘load’ event is called 😉)
Thanks for staying that long — I hope this article will help you