Use History API for Better UX

JavaScript MN, March 2013

Zachary Johnson — @zacharyjohnson
Zachstronaut LLC — zachstronaut.com

Use History API for Better UX

The History API can fix UX problems in your client-side JavaScript web applications and UI patterns.

First, a bit about the problems...

Then, we'll see what the History API is and how it can fix these problems.

Primary source of problems...

Since "AJAX" we've been able to create complete multiple-view experiences in the browser without ever having to leave the current page or change URL.

Browser URL is your App State

If your entire web site experience happens on a single URL, then you've just broken your users' browsers.

URL should represent current view / UI state / data loaded in view.

Problems with many views on one URL:

  • No direct linking
  • No back/forward button
  • Refresh/crash loses state

Please do

create amazing web UI that feels like "native" UI. (I <3 Google Docs, and I <3 JavaScript!)

Please don't

break my browser!

Your web UI may feel "native"...

But I still view it in my web browser!

(When you forget this you create problems for your users.)

What do Web Browsers got that native apps ain't got?

  • Back / Forward Buttons
  • Refresh Button
  • URL / Location Bar
  • Links / Bookmarks / Social APIs
  • Crash loses all state by default

Now, let's play a game called

Break-Zach's-Browser-and-Torment-Him-on-the-Daily

Coming soon to the AppStore

Infinite Scroll

Facebook, Twitter, Pinterest, and most sites using infinite scroll append new content to their main view but never change URL.

Auto-fetching the next page is awesome.
Breaking the browser is not.

e·pit·o·me

/iˈpitəmē/

Noun

1. A person or thing that is a perfect example of a particular quality or type.

Example

Infinite Scroll is the epitome of bad UX.

Let me tell you a quick [user] story...

I'm catching up on my Twitter feed, and infinite scroll has loaded an unknown number of new tweets, and then I click a link that takes me to a new page...

When I press my back button, I start over at the top of my feed.

TWITTER Y U NO REMEMBER STATE

Even more fun...

I especially love it when I've been scrolling and loading for 10 minutes and then my browser crashes.

Infinite scroll annoys me every single day.

I'm doing a whole session on fixing the many UX problems with infinite scroll at MinneWebCon.

Speaking of fixing things...

History API to the rescue!

The JavaScript History API lets you update the web browser's location / URL without leaving or reloading the current page. (The full URL, not just the hash.)

Boo! /puppies/#/monsters/frankenurl/

Yay! /monsters/frankenstein/

That gives me an idea...

What if when Infinite Scroll automatically loads page 2, it also automatically changes the browser location to the URL /page/2?

Then, if I leave from page 74, I can go back to page 74!

Infinite Scroll + History API = Win!

  • Back Button (works)
  • Bookmark (continue reading later)
  • Reload / Crash (no biggie)

The API

Object: window.history

  • history.pushState(data, title, url)
  • history.replaceState(data, title, url)

Event: window.onpopstate

window.location.href also reflects any changes.

Google: MDN History for more docs.

Better than #hash

  • Change any/all of location except domain
  • No /puppies/#/monsters/frankenurls/
    • /about/ /games/walrus-run/
    • vs. /about/ /about/#/games/walrus-run/
  • Real URL, server can populate directly on reload!

Can I use?

History API was added as part of HTML5 spec.

caniuse.com/#feat=history

GitHub: History.js polyfill*

* /hope/you/like/#/polyfills/

I think I get it!

So fixing Infinite Scroll was as easy as calling history.replaceState() with the URL of the next page being AJAX loaded, right?

Ehhhhhh... let's try that and see.


/**
 * https://github.com/paulirish/infinite-scroll
 * Infinite Scroll Load Success Callback
 */
function (elements, data, url) {
    if (window.history) {
        // We want to give people the ability to hit back to return to
        // home so they can more easily reload for new content at the
        // top of the infinite scroll experience
        if (window.location.href.match(fixConfig.homePattern)) {
            history.pushState('', '', url);

        } else {
            history.replaceState('', '', url);
        }
    }
}
                    

Manipulating History Creates Problems

(If you use the History API to kill a page's own grandfather...)

  1. History remembers scroll position
  2. History forgets DOM manipulation**
  3. Using Back/Forward between pushed states does not reload page

** Hey JSMN: Have browsers ever cached pages as-altered by JS? Or has it always been that just the results of the HTTP request response are cached?

To put it another way

When the user presses their back button, they'll go back to the same scroll position but not the same content.

D'oh.

History is a Double Edged Sword

When you load new content, you can change the URL with the History API.

If the user presses Back/Forward, that will also change the URL... but it won't change the content.

When the URL of our app changes, we must manage the view/content state ourselves!

Hello onpopstate

Goodbye History Paradox

Listen for the window object's popstate event:

  • immediately when your page is requested
    • don't wait for onload/onready!
  • and subsequently when user presses Back/Forward within your pushed states

/**
 * Execute immediately or you might miss the first event!
 * Don't wait for onload or dom ready!
 */
if (window.history) {
    var firstPop = true;
    window.addEventListener('popstate', function (e) {
        // The first pop happens when this page first loads.  If we
        // pop again then the vistor is using forward/back within our
        // site, so we must update the content accordingly.
        if (!firstPop && location.href.match(fixConfig.homePattern)) {
            // Scroll to top of homepage, and get latest posts.
            setTimeout(function () {window.scrollTo(0, 1);}, 0);
            setTimeout(function () {window.location.reload();}, 1);
        }
        firstPop = false;
    }, false);
}
                    

So close, and yet so far away

No longer totally b0rked, but still lacking UX finesse

  1. More we could do with scroll position
  2. And what about when user scrolls back up?

More Infinite Scroll UX improvements coming soon!
I'll post my slides and code after MinneWebCon.

Other History API Examples

NewDealDesign, Slaqr

My clients want maximum control over the visual experience of their sites, including during loading and transitions between pages.

They don't want flicker/redraw when a user clicks a link.

View Routing

  • user clicks/navigates to view
  • or, popstate event received
    • find view that matches window's new location
  • view's show method called
  • view publishes 'show' event
  • view is transitioned in
  • view publishes 'afterShow' event
  • history.pushState() the view's URL, if != location

Hey JSMN, do any JS frameworks do this for views, ie history and view events? Ember, Angular?

Closing Mantra: Browser URL is your App's State is your Browser URL

Build your JavaScript app with URLs that point to meaningful UI/view/data states. Use the History API to set those URLs to match your app's state.

Simple Tip: What URLs would your app have if it wasn't AJAX / didn't use JavaScript?

Benefits of History API Integration

  • Linking / Bookmarking / Social
  • Back / Forward support
  • Refresh / Error recovery
  • No /puppies/#/monsters/frankenurls/

Don't break people's browsers.

What are your questions?*

Zachary Johnson — @zacharyjohnson
Zachstronaut LLC — zachstronaut.com

* I've got one for you, JSMN. Is there something better than reveal.js? I want syntax highlighting and the ability to create a file I can upload to SlideShare.