Skip to content

Instantly share code, notes, and snippets.

@morten-olsen
Last active April 14, 2016 14:40
Show Gist options
  • Select an option

  • Save morten-olsen/b4da8f02e269cb4b90895109c7c73c6e to your computer and use it in GitHub Desktop.

Select an option

Save morten-olsen/b4da8f02e269cb4b90895109c7c73c6e to your computer and use it in GitHub Desktop.

One of the corner stone of creating universal web apps today is the history API. For those not aware what that is, this port is mostly not that relevant to you, but in short it allows a page to navigate async while still retain browser features such as a correct url, and respond correctly to browser navigation.

Most SPA framework today as well as custom solutions reply on this in order to navigate, and give the user a feeling of still using a browser.

One problem though, you can’t rely on this in the later versions of Safari, not on iOS not in OSX. The reason being that Apple has introduced a security feature in their current version, preventing more than 100 calls to history.replaceState or history.pushState. What this means is that when your user has visited a maximum of a 100 pages the application will break. Or in cases, as ours, where you keep the history state current by using history.replaceState when you make changes to your application state, a lot less.

To see what i mean, you can easily reproduce this “feature” by calling:

for (var i = 0; i < 101; i++) {
     console.log(i);
     history.replaceState({}, null, location.href);
}

This will in all modern versions of Safari (including latest technical preview) result in a Error: SecurityError: DOM Exception 18. This apart from being a very unhelpful error, means that all further calls to history.replaceState or history.pushState will fail, rendering the application broken.

So how do we fix this? The only way to replenish the history modification count it to reload the page, but this is most likely undesirable, as the user might at the given time not be in a clean state. They might have entered text into an input field, they might be at a specific scroll location, they might have modified the page in some way not re-creatable on a refresh.

Luckily we are using React with Redux, meaning that we have a singular representation of the current state, which can be JSON serialized. This means that we can try catch the history modification event, and if fails, detect that we need to do some tricks to enable further interaction.

In our case what we do is utilise the session store to actually hold the state change, while we refresh the state, and replenish our modification count.

try {
  history[`${type}State`](state, title, url);
} catch (ex) {
  sessionStorage.setItem('__safari_history_fix', JSON.stringify({
    type,
    state,
    title,
    url,
  }));
  location.reload();
}

So at each page reload we check if there is a session storage item containing a state change we tried to introduce to safari, but failed due to this limitation

let fix = sessionStorage.getItem('__safari_history_fix');
sessionStorage.removeItem('__safari_history_fix');
if (fix) {
  fix = JSON.parse(fix);
  if (fix.type === 'push') {
    history.replaceState(history.state, document.title, location.href);
    history.pushState(fix.state, fix.title, fix.location);
  } else {
    history.replaceState(fix.state, fix.title, fix.location);
  }
}

This solution defiantly is not going to work for everyone as it require a serializable state, since everything like event hookups will be lost, but at least in a React/Redux world this offers some kind of a solution (though it will still interrupt the user with a page refresh, but atlas the user will be returned to where they where).

For the entire solution example, have a look at http://codepen.io/mortenolsendk/pen/GZxQwL/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment