diff --git a/packages/fluxible-router/docs/api/handleHistory.md b/packages/fluxible-router/docs/api/handleHistory.md
index b426a006..6fb8583f 100644
--- a/packages/fluxible-router/docs/api/handleHistory.md
+++ b/packages/fluxible-router/docs/api/handleHistory.md
@@ -9,6 +9,7 @@ The `handleHistory` higher-order component handles the browser history state man
| `checkRouteOnPageLoad` | `false` | Performs navigate on first page load |
| `enableScroll` | `true` | Saves scroll position in history state |
| `historyCreator` | [`History`](../../lib/History.js) | A factory for creating the history implementation |
+| `ignorePopstateOnPageLoad` | `false` | A boolean value or a function that returns a boolean value. [Browsers tend to handle the popstate event differently on page load. Chrome (prior to v34) and Safari always emit a popstate event on page load, but Firefox doesn't.](https://developer.mozilla.org/en-US/docs/Web/Events/popstate) This flag is for ignoring popstate event triggered on page load if that causes issue for your application, as reported in [issue #349](https://github.com/yahoo/fluxible/issues/349). |
## Example Usage
diff --git a/packages/fluxible-router/lib/handleHistory.js b/packages/fluxible-router/lib/handleHistory.js
index bc3368ed..8bce8a65 100644
--- a/packages/fluxible-router/lib/handleHistory.js
+++ b/packages/fluxible-router/lib/handleHistory.js
@@ -5,7 +5,7 @@
/*global window */
'use strict';
var React = require('react');
-var debug = require('debug')('RoutingContainer');
+var debug = require('debug')('FluxibleRouter:handleHistory');
var handleRoute = require('../lib/handleRoute');
var navigateAction = require('../lib/navigateAction');
var History = require('./History');
@@ -22,7 +22,8 @@ var defaultOptions = {
enableScroll: true,
historyCreator: function () {
return new History();
- }
+ },
+ ignorePopstateOnPageLoad: false
};
// Begin listening for popstate so they are not missed prior to instantiation
@@ -44,6 +45,14 @@ var historyCreated = false;
function createComponent(Component, opts) {
var options = Object.assign({}, defaultOptions, opts);
+ function shouldIgnorePopstateOnPageLoad() {
+ var ignore = options.ignorePopstateOnPageLoad;
+ if ('function' === typeof ignore) {
+ return ignore();
+ }
+ return !!ignore;
+ }
+
function HistoryHandler(props, context) {
React.Component.apply(this, arguments);
}
@@ -77,6 +86,15 @@ function createComponent(Component, opts) {
this._saveScrollPosition = this.constructor.prototype._saveScrollPosition.bind(this);
this._history = options.historyCreator();
+
+ this._ignorePageLoadPopstate = shouldIgnorePopstateOnPageLoad();
+ if (this._ignorePageLoadPopstate) {
+ // populate the state object, so that all pages loaded will have a non-null
+ // history.state object, which we can use later to distinguish pageload popstate
+ // event from regular popstate events
+ this._history.replaceState(this._history.getState() || {});
+ }
+
this._scrollTimer = null;
if (options.checkRouteOnPageLoad) {
@@ -119,22 +137,30 @@ function createComponent(Component, opts) {
this._scrollTimer = window.setTimeout(this._saveScrollPosition, 150);
},
_onHistoryChange: function (e) {
+ debug('history listener invoked', e);
+ if (this._ignorePageLoadPopstate) {
+ // 1) e.state (null) and history.state (not null)
+ // -- this is popstate triggered on pageload in Safari browser.
+ // history.state is not null, because if _ignorePageLoadPopstate
+ // is true, we replaceState in componentDidMount() to set state obj
+ // 2) e.state(not null) and history.state (not null)
+ // -- regular popstate triggered by forward/back button click and history.go(n)
+ // 3) history.state (null)
+ // -- this is not a valid scenario, as we update the state before
+ // _onHistoryChange gets invoked in componentDidMount()
+ var stateFromHistory = this._history.getState();
+ var isPageloadPopstate = e.state === null && !!stateFromHistory;
+ debug('history listener detecting pageload popstate', e.state, stateFromHistory);
+ if (isPageloadPopstate) {
+ debug('history listener skipped pageload popstate');
+ return;
+ }
+ }
var props = this.props;
var url = this._history.getUrl();
var currentRoute = props.currentRoute || {};
var nav = props.currentNavigate || {};
- // Add currentNavigate.externalUrl checking for https://github.com/yahoo/fluxible/issues/349:
- // "Safari popstate issue causing handleHistory.js to execute the navigateAction on page load".
- // This needs app to dispatch "externalUrl" as part of the payload for the NAVIGATE_START event
- // on server side, which contains the absolute url user sees in browser when the request is made.
- // For client side navigation, "externalUrl" field is not needed and is not set by fluxible-router.
- var externalUrl = nav.externalUrl;
- if (externalUrl && externalUrl === window.location.href.split('#')[0]) {
- // this is the initial page load, omit the popstate event erroneously fired by Safari browsers.
- return;
- }
-
var currentUrl = currentRoute.url;
var onBeforeUnloadText = typeof window.onbeforeunload === 'function' ? window.onbeforeunload() : '';
@@ -151,7 +177,7 @@ function createComponent(Component, opts) {
var pageTitle = navParams.pageTitle || null;
- debug('history listener invoked', e, url, currentUrl);
+ debug('history listener url, currentUrl:', url, currentUrl, this.props);
if (!confirmResult) {
// Pushes the previous history state back on top to set the correct url
@@ -249,6 +275,14 @@ function createComponent(Component, opts) {
* @param {boolean} opts.enableScroll=true Scrolls to saved scroll position in history state;
* scrolls to (0, 0) if there is no scroll position saved in history state.
* @param {function} opts.historyCreator A factory for creating the history implementation
+ * @param {boolean|function} opts.ignorePopstateOnPageLoad=false A boolean value or a function that
+ * returns a boolean value. Browsers tend to handle the popstate event
+ * differently on page load. Chrome (prior to v34) and Safari always emit
+ * a popstate event on page load, but Firefox doesn't
+ * (https://developer.mozilla.org/en-US/docs/Web/Events/popstate)
+ * This flag is for ignoring popstate event triggered on page load
+ * if that causes issue for your application, as reported in
+ * https://github.com/yahoo/fluxible/issues/349.
* @returns {React.Component}
*/
module.exports = function handleHistory(Component, opts) {
diff --git a/packages/fluxible-router/tests/unit/lib/handleHistory-test.js b/packages/fluxible-router/tests/unit/lib/handleHistory-test.js
index 54d1bbe0..a996d0b3 100644
--- a/packages/fluxible-router/tests/unit/lib/handleHistory-test.js
+++ b/packages/fluxible-router/tests/unit/lib/handleHistory-test.js
@@ -264,6 +264,55 @@ describe('handleHistory', function () {
done();
}, 10);
});
+ it('execute navigation action for pageload popstate when ignorePopstateOnPageLoad is false', function (done) {
+ var routeStore = mockContext.getStore('RouteStore');
+ routeStore._handleNavigateStart({url: '/foo', method: 'GET'});
+ var MockAppComponent = mockCreator({
+ checkRouteOnPageLoad: false,
+ historyCreator: function () {
+ return historyMock('/browserUrl', {a: 1});
+ },
+ ignorePopstateOnPageLoad: false
+ });
+
+ // simulate page load popstate
+ window.dispatchEvent(Object.assign(new Event('popstate'), {state: null}));
+
+ ReactTestUtils.renderIntoDocument(
+
+ );
+
+ setTimeout(function() {
+ expect(mockContext.executeActionCalls.length).to.equal(1);
+ expect(mockContext.executeActionCalls[0].action).to.be.a('function');
+ expect(mockContext.executeActionCalls[0].payload.type).to.equal('popstate');
+ expect(mockContext.executeActionCalls[0].payload.url).to.equal('/browserUrl');
+ done();
+ }, 150);
+ });
+ it('skip navigation action for pageload popstate when ignorePopstateOnPageLoad is true', function (done) {
+ var routeStore = mockContext.getStore('RouteStore');
+ routeStore._handleNavigateStart({url: '/foo', method: 'GET'});
+ var MockAppComponent = mockCreator({
+ checkRouteOnPageLoad: false,
+ historyCreator: function () {
+ return historyMock('/browserUrl', {a: 1});
+ },
+ ignorePopstateOnPageLoad: true
+ });
+
+ // simulate page load popstate
+ window.dispatchEvent(Object.assign(new Event('popstate'), {state: null}));
+
+ ReactTestUtils.renderIntoDocument(
+
+ );
+
+ setTimeout(function() {
+ expect(mockContext.executeActionCalls.length).to.equal(0);
+ done();
+ }, 150);
+ });
describe('window.onbeforeunload', function () {
beforeEach(function () {
global.window.confirm = function () { return false; };