This is the design our team likes to follow, I haven't seen it documented anywhere (but I doubt it's new), so I'll write it here.
Essentially, we try to replicate traditional server side MVC thinking on the client side. Decent server side rendered apps that don't have the problems mentioned in the OP's post usually follow RESTful principles to some extent. This means that what you see on a page is a pure (ish) function of these things:
* The parameters in the URL
* The state of the database
And nothing else. Translated to the client side, it means that you want anything the user sees to be a function of: * The URL
* The state of the model layer (or the flux stores, whatever)
* Addendum: 100% of the model layer is an
eventually-consistent subset of the database
We use one exception to this rule: unimportant state is allowed to be right in the view (we use React, so that's component state). Stuff like "is the dropdown menu expanded" or "which of the items in a list are selected" is neither in the URL nor in the model layer (because we won't sync it back to the database). The rule of thumb is "if the user refreshes the page, is it a problem if this data is lost?". If the answer is "no", we can make it component state, as close to the action as possible (so not in the root component usually).All of this combined gets us a lot of stuff for free. For example, all our modal dialogs, and even "is the menu shown" is addressable from the URL. This might feel like overengineering, but it gets us lots of stuff for free.
For example, users on mobile phones expect to be able to close a modal or a menu by hitting the hardware back button. We get this for free, because when the user does an action that causes a modal to show, we redirect to some URL like example.com/wherever/?someModal=1. (all using the history api). Router picks it up, modal is shown. Model layer is not touched, this is 100% view layer work. Then, when the user hits "back", the browser restores the URL to example.com/wherever, router picks up the change again and the view renders the same page but without the modal.
We get all the usual browser features for free simply by following basic REST ideas. Don't put URL stuff in your stores. Don't put non-backend-synced stuff in your stores (just like you wouldn't put important state in your sessions in server side rendered apps). Put everything that matters in the URL.
Any routing library that encourages you to sync URL stuff into your model layer is, IMO, wrong by design.