Handling Browser Navigation and bookmarkability in a single page web application
July 7, 2020
Andrei Ionescu

With the rise of advanced JavaScript frameworks like Angular or React, the concept of single page applications entered the centerstage of web development. A single page application (or SPA) is an app that uses a design approach where page content is generated dynamically through manipulating the DOM elements, rather than requesting new HTML files on each navigation event.

Arguments for using this idea are:

  • Loading speed: most resources are loaded on app startup, as a bundle containing all the HTML, styling and scripts gets loaded in memory. Even if this may lead to an initial longer wait time, subsequent navigation is much responsive
  • Simplified development and debugging: ontop of using certain design patterns imposed by the framework, the departure from building a separate HTML file for each section of a web app makes it easier to structure source files. By not navigating away from the loaded page, a developer can follow network calls and state of resources much easier.
  • Caching: since app resources are bundled, they can easily be stored locally and even used offline.

Some of the disadvantages:

  • Harder search engine optimisation: since content is generated on page load, web crawlers that search engines rely on to index pages don’t do a good job parsing the site’s content, unless the app is using the somewhat advanced technique of server-side rendering.
  • Browser navigation: single page applications often lose the ability of being controlled by external interfaces, like the browser backwards and forwards navigation.
  • Bookmarkability: since the whole application consists of only one page, it’s not possible to bookmark a certain state of the application for easy return to it.

During the development of our projects using React, we already faced the last two disadvantages and here's what we learned from this experience and how we made things work out.


  1. Attain an intuitive user experience, by representing each screen the user encounters within his interactions with the application as part of the browser navigation.
  2. Browser back and forward navigation working across all visited screens.
  3. Refresh working on all screens.
  4. Keep in line with browser navigation models where navigation is NOT deeply coupled to the underlying application but links are just identifiers for resources or affordances being presented to the user, meaning keep loose coupling between browser navigation and application semantics.
  5. Identify prerequisites and requirements to apply inter-module navigation and intra-module boundary-crossing navigation to individual "screens" in the application (for example backwards navigation to a certain screen requires the screen's forward action to work idempotently).


Flows with multiple steps

Each step must belong in a separate child route to enable browser history navigation (back / forward button) between steps.

When there is no child route we fall back to either the first step in the flow or a step provided by a backend state.

Accessing child routes without fulfilling all required criteria dictated by business requirements will redirect to a child route provided by the backend or, when there is none available, the first step.

Using the browser navigation to go back several steps and then changing the route via application action will result in the forward history entries being discarded (native browser behaviour).

Refreshing pages will not affect browser navigation (native browser behaviour).

Each flow should have a parent component that should only render child routes representing steps. This allows handling of side effects and inner lifecycle logic that applies to all steps. This is where route checks and redirects should happen.

When a redirect occurs due to unspecified child route or due to server state conditions, use the replace method on history to avoid re-executing lifecycle side effects when using the browser navigation. (ex https://app/feature is replaced with https://app/feature/first-step).

Use selectors instead of passing props between the parent component and the steps components. This makes the components decoupled from one another and only coupled to their corresponding state. The recommended structure for these would be to place the parent component in its corresponding feature folder and inside that folder create all the components required by the child routes inside a steps folder.

Stage Definition Framework

Some of the flows might have specific behaviour depending on 3 stages that we need to keep in mind when defining business requirements.

        1. Entrance

Should define what happens when entering a module screen or refreshing the page.

By default, when there are no specific requirements, the module screens consisting of multiple steps will replace the current browser history entry with the one pointing the user to the higher-most step that he is allowed to access or the first step when there is no step specified in the url. Module screens consisting of a single view will have no effect on browser history.

        2. Step change

Should define what happens when switching between steps. This only applies to module screens consisting of multiple steps.

By default, when there are no specific requirements, the module screen will push the next step in the browser history once completing the step. The same behaviour will apply when clicking the previous step button. Example: After a user goes from step 1 to step 2 using the next button and then from step 2 to step 1 using the previous button, when he clicks the browser back button he will go back to step 2 and clicking the browser back button again he will end up in step 1.

           3. Exit

Should define what happens when a user exists a module screen.

By default, when there are no specific requirements, this will have no effects on the browser navigation. Clicking back after completing a flow will have the same behaviour as entering the last step directly, so it will be dictated by the server side state conditions if we are allowed to access that step or we will be redirected to another. If the server were to redirect us to the first step instead, it means that the current route will be replaced in the browser history, not pushed, meaning that the forward history would be discarded.

Bookmarking abilities

Bookmarking might come in handy for certain areas of the app but it often signals a convoluted navigation and these cases should be rare. Given the fact that flows with multiple steps have their own routes and session parameters are kept in the url, anything can be bookmarked and restored if the server allows it.

The most important aspect of bookmarks is whether they make sense or not in screen modules with a single screen (not multiple steps flows). These screens could display or hide data depending on a local, volatile state which is reset on refresh. These could be buttons, tabs, accordions or other elements that have no impact on the url itself.

In order to restore the page in the exact state using bookmarks, it would mean that the url needs to change by using child routes. However this will affect the browser history and might be a source of confusion, when taking into account the following aspects:

  • Pushing the routes in the browser history

This has the consequence that the history will be polluted with entries and depending on the amount of route changes, going back to the previous screen might require clicking the back button the same amount of times but will be consistent with the user's action (such as switching tabs). Anything is bookmarkable.

  • Replacing the routes in the browser history

This has the advantage of matching the navigation flow diagram, meaning that when a user clicks the browser back button, he will end up in the previous screen. This has the consequence that the user might be tempted to believe that he can go back to a previous screen state (such as the previously selected tab) and instead end up in the previous module screen. Anything is bookmarkable.

The proposal for the default behaviour is the following: No route change on ui show/hide actions (such as tabs). Keep the ui state volatile and not interfering with the browser navigation at all. The page can be bookmarked but it will always load with the initial state defaults (such as starting on the first tab on reload when having tabs). The advantage is that it keeps the navigation intuitive but specific states cannot be bookmarked (such as tab selection).

Depending on the business needs special routing can be implemented. UX caveats have to be considered when choosing routes to solve a broader bookmarking granularity.


The debate between the advocates of SPAs and multi-page applications is still ongoing, with both sides still having strong arguments over the other. A lot of the initial problems of single page applications were solved or at least diminished during the evolution of web application frameworks.

Custom behavior can also be implemented to fit with business specifications. By combining proper handling of screen routes, correct building of components, enabling them to be re-initiated independently of the overall flow, or previous actions, a web app can combine the performance advantages of a SPA with the feeling of a multi-page app when it comes with the user experience, allowing proper browser navigation and bookmarkability.

Talk to the team