handling-browser-navigation-and-bookmarkability-in-a-single-page-web-application
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:
Some of the disadvantages:
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.
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.
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 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:
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.
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.