The Zulip web application has a nice system of hash (#) URLs that can
be used to deep-link into the application and allow the browser’s
“back” functionality to let the user navigate between parts of the UI.
Some examples are:
/#settings/your-bots: Bots section of the settings overlay.
/#streams: Streams overlay, where the user manages streams
/#streams/11/announce: Streams overlay with stream ID 11 (called
/#narrow/stream/42-android/topic/fun: Message feed showing stream
“android” and topic “fun”. (The
42 represents the id of the
The main module in the frontend that manages this all is
hash_util.js for all the parsing
code), which is unfortunately one of our thorniest modules. Part of
the reason that it’s thorny is that it needs to support a lot of
The user clicking on an in-app link, which in turn opens an overlay.
For example the streams overlay opens when the user clicks the small
cog symbol on the left sidebar, which is in fact a link to
/#streams. This makes it easy to have simple links around the app
without custom click handlers for each one.
The user uses the “back” button in their browser (basically
equivalent to the previous one, as a link out of the browser history
will be visited).
The user clicking some in-app click handler (e.g. “Stream settings”
for an individual stream), that potentially does
several UI-manipulating things including e.g. loading the streams
overlay, and needs to update the hash without re-triggering the open
Within an overlay like the streams overlay, the user clicks to
another part of the overlay, which should update the hash but not
re-trigger loading the overlay (which would result in a confusing
The user is in a part of the web app, and reloads their browser window.
Ideally the reloaded browser window should return them to their
A server-initiated browser reload (done after a new version is
deployed, or when a user comes back after being idle for a while,
see notes below), where we try to preserve
extra state (e.g. content of compose box, scroll position within a
narrow) using the
/#reload hash prefix.
When making changes to the hashchange system, it is essential to
test all of these flows, since we don’t have great automated tests for
all of this (would be a good project to add them to the
Puppeteer suite) and there’s enough complexity
that it’s easy to accidentally break something.
The main external API lives in
browser_history.update is used to update the browser
history, and it should be called when the app code is taking care
of updating the UI directly
browser_history.go_to_location is used when you want the
module to actually dispatch building the next page
Internally you have these functions:
hashchange.hashchanged is the function used to handle the hash,
whether it’s changed by the browser (e.g. by clicking on a link to
a hash or using the back button) or triggered internally.
hashchange.do_hashchange_normal handles most cases, like loading the main
page (but maybe with a specific URL if you are narrowed to a
stream or topic or direct messages, etc.).
hashchange.do_hashchange_overlay handles overlay cases. Overlays have
some minor complexity related to remembering the page from
which the overlay was launched, as well as optimizing in-page
transitions (i.e. don’t close/re-open the overlay if you can
easily avoid it).