Solving Closure Library's Html5history double event dispatch
11 Sep 2015Most Clojurescript apps that rely on browser routing are wired in some manner to either the Google Closure Library's HTML5 History module or - in an increasingly lower number of cases - the History module. While both pushState
- and fragment-based routing are supported, the module always dispatches two navigation events when opting for the latter, which can become a source of unexpected behavior. Here's how to fix it.
Understanding the problem
Opting for the hash based routing approach with Google Closure's Html5history module can be done with the following (simplistic) Clojurescript code:
;; instantiate an Html5History object
(let [history (goog.history.Html5History.)]
;; listen for navigation events
(goog.events/listen history
goog.history.EventType.NAVIGATE
#(.log js/console "Navigate event fired"))
;; opt for fragment routing and start using the module
;; also returns the instance for practical purposes
;; (e.g. for use in a function)
(doto history
(.setUseFragment true)
(.setEnabled true)))
Inspecting the (used above) setUseFragment
function internals reveals the following:
if (useFragment) {
goog.events.listen(this.window_, goog.events.EventType.HASHCHANGE,
this.onHistoryEvent_, false, this);
} else {
goog.events.unlisten(this.window_, goog.events.EventType.HASHCHANGE,
this.onHistoryEvent_, false, this);
}
However, looking at the object instantiation we see that the module also listens for the goog.events.EventType.POPSTATE
event. On browsers that don't support the pushState
API this represents absolutely no problem, since one should use goog.History
instead of goog.History.Html5History
anyway. But on browsers in which pushState is supported, we end up receiving two NAVIGATE
events. This can easily become the root of unexpected behavior.
Applying a solution
Since our focus is on using fragment routing, we don't really need to be listening to the popstate
browser event. On the other hand, we want to preserve popstate
behavior in case we switch to the pushState
API routing. To tackle this, I use the following approach:
;; only remove popstate event listener when using
;; fragment based routing
(if (.-useFragment_ history)
(events/unlisten (.-window_ history)
goog.events.EventType.POPSTATE
(.-onHistoryEvent_ history)
false
history))
It allows to unsubscribe from popstate
events while still preserving that behavior when not using fragments.
If you've got any feedback, don't hesitate to contact me or post in the comments below. Happy coding!