[whatwg] History API, pushState(), and related feedback

Mike Wilson mikewse at hotmail.com
Thu Jan 21 19:04:34 PST 2010

Ian Hickson wrote:
> On Wed, 19 Aug 2009, Mike Wilson wrote:
> > 
> > Currently, the design with an immutable state object and 
> the PopEvent 
> > and HashChange events triggering at somewhat insufficient 
> timings, makes 
> > it hard to build the things I'm thinking about.
> > 
> > IMO you need:
> > - an event each time you're *leaving* a history entry (in
> >   addition to HashChange triggered when entering one)
> > - allow updating existing state entries
> > - shift the whole state entry model so that state entries 
> >   belong to history entries, and are not logically inserted 
> >   "between" them (this is actually a very important 
> >   distinction)
> > 
> > I'm hoping Ian will come back with the use cases he has in 
> mind, as I am 
> > as of yet not clear on what he wants, or does not want, to 
> support with 
> > the pushState model.
> Does replaceState() make it possible to do the things you're
> thinking about? If not, could you elaborate on what is not 
> possible, or is awkward, in the current design?

Yes, replaceState solves bullets two and three above.

> On Sat, 22 Aug 2009, Mike Wilson wrote:
> > 
> > Great, this seems to be exactly what I want too. In particular
> > I note the following differences from the current spec:
> > - events both when entering and leaving a history entry (I 
> >   called them hashload and hashunload but I agree it is better 
> >   to avoid "hash" as we also have state-only history entries)
> > - the same processing for "fresh" (newly navigated to) history
> >   entries as for "historical" (navigated back/forward to)
> >   history entries
> > - removal of the popstate event and exposing a read/writable 
> >   state object during the whole history entry "session"
> > 
> > About "stateactivated" naming:
> > Activated/deactivated is a bit longish. Could
> > - stateload/stateunload
> > - stateenter/stateleave
> > or others be good alternatives?
> > Is "state" the desired keyword? Or should "history" or others
> > be considered?
> > Or something playing on the "pageshow/pagehide" naming?
> > 
> > About "pageStorage" lifetime:
> > Adding on to your description, assuming we are navigating from 
> > one page (/a) to another (/b) in history without bfcache, the 
> > following would be a suitable chain of events:
> > - /a statedeactivated event
> > - /a unload event
> > - /a browser saves form fields, scrollpos, and history state obj
> > - <browser swaps out /a and loads /b>
> > - /b browser restores history state obj before any script runs
> > - /b scripts are executed and form fields and scrollpos are 
> >      restored while document content is built
> > - /b load event
> > - /b stateactivated event
> > 
> > About "pageStorage" naming:
> > I think "page" makes you think more of Document than of history
> > entries. Looking at an overview of storage areas, ordered from
> > large scopes down to fine-grained scopes may spawn some ideas:
> > 
> > 
> >   Scope               Storage area / identifier
> >   -----               -------------------------
> >   user agent          window.localStorage
> >   browsing context    window.sessionStorage
> >   document            -
> >   history entry       window.pageStorage
> > 
> > If anticipating there could be a future storage area per 
> > Document, naming could be something like this:
> > 
> > 
> >   Scope               Storage area / identifier
> >   -----               -------------------------
> >   user agent          window.localStorage
> >   browsing context    window.sessionStorage
> >   document            document.documentStorage
> >   history entry       window.history.entryStorage
> I'm a little concerned about overgeneralisation and 
> overabstraction to the 
> point of making APIs that on paper seem logical but in practice don't 
> really work. I'm not really clear on what the above is 
> proposing to solve, 
> though. Could you describe the problem with the current API 
> that the above 
> solves, ideally with reference to use cases?

I'll keep this short as there is more recent discussion:
1) The additional event is discussed later in this mail.
2) The pageStorage object is one incarnation of [a key
   value store] solving the dependency problem that appears
   when different components want to save data to the single
   session history state object

And the later part is more about general properties of
API design:
3) If a key-value store is desired, then using the same API 
   as the other key-value stores is a strength and not an over-
   generalisation. The web doesn't need yet another API.
4) Thinking about possible future additions when choosing
   names is one part (of many) of a successful design.

> On Wed, 2 Dec 2009, Mike Wilson wrote:
> > 6.11.9, step 3.1.2:
> > 
> >   "The browsing context's browsing context name must be unset."
> > 
> >   It might be good with a note that the browsing context name
> >   is only switched by the user agent for history traversal to
> >   another origin, and not for navigation to a new origin.
> Navigation involves a history traversal step.

Right, missed that. Good, it seems correct that browser 
context name is cleared when navigating to a new origin.

> On Wed, 23 Dec 2009, Mike Wilson wrote:
> > 
> > - get state for current session history entry 
> >   [partial support by spec (popstate), *1]
> > *1: only available in popstate event, not during rest of
> >     history entry lifetime (getter needed)
> It's not really clear what the use case for this is. Could 
> you elaborate?

This is about the current API forcing cooperation upon
different components that want to store state, and having
to carry an extra copy of the current state data.

Let's say that we have two components, A and B, that are
not aware of each other but both want to save state to the
history state.

The author could make an app-global save function that
connects to all update notifications from all components
and does:
  function saveItAll() {
      A: A.serializeState(),
      B: B.serializeState()

The above has two drawbacks:
- the global function has to be maintained when components
  are added or removed
- even if only one component's data is updated (triggering
  the save function), all component serialization has to
  be executed even if not changed

Trying to fix this, we would want to set each part of the
state separately so each component can handle its own

  A.saveMe: function() {...}
  B.saveMe: function() {...}

and no global coordination would be needed. But the problem
is that when A.saveMe wants to store its latest data, it
must also have all other data, and to be able to have the
latest state we need a getter:

  A.saveMe: function() {
    var data = History.getState();
    data.A = A.serializeState();

With this solution components can have a loose cooperation
by just agreeing on a general data format (f ex map).
Without the getter, components can no longer use the History
API directly, but instead have to call through a wrapper
that caches the latest state in a variable:

  A.saveMe: function() {
    var data = MyHistoryWrapper.getState();
    data.A = A.serializeState();
  MyHistoryWrapper.replaceState = function(data) {
    this.cache = data;

Now components not only have to cooperate on a data format,
but also on a non-standard API.

Of course this use case had been better served by a key-
value store like pageStorage mentioned above. But having a 
getter takes us at least halfway there.

> On Wed, 23 Dec 2009, Mike Wilson wrote:
> > 
> > - allow for self-contained components to handle own state
> >   [not supported by spec, *2]
> > *2: all page parts saving state must coordinate with a 
> >     shared data structure (key/value-store or similar 
> >     needed)
> This is supported, if the various components cooperate, either by all 
> registering onpopstate handlers and having a state object format that 
> allows for component ownership to be established, or by 
> having a wrapper 
> library that does dynamic dispatch of state objects. DOM APIs 
> and HTML in 
> general do not give any more than support for multiple components in 
> general -- for example, there's only one DOM, etc. Components 
> are expected 
> to be written in a cooperative way (and they often are -- there are 
> several modular frameworks for this).

The point here is that there are several frameworks for this,
and there will always be. This means that components that are
compatible with one framework and not the others, and there
is no longer a notion of a self-contained component that only
relies on standardized APIs. IMHO adding a getter, or making
the history state into a key-value store, is a cheap price to
pay API-wise to get self-contained components without any
strings attached.

> On Wed, 23 Dec 2009, Mike Wilson wrote:
> > 
> > - have a notification event when entering a history entry
> >   [almost full support in spec (popstate), *3]
> > *3: popstate event not fired for navigation with pushState
> >     (fire for navigation too needed)
> If you're calling the API, you know you are, so there's no 
> need to be told 
> that you are.
> On Wed, 23 Dec 2009, Mike Wilson wrote:
> >
> > Certainly the page code can update the state on every change of 
> > user-editable state, setting up a swarm of event handlers 
> not to miss 
> > anything important. My point is that this is not always practical, 
> > imagine f ex serializing a large tree or rich-text editor on every 
> > update event.
> I don't see why this would need a notification of calling pushState().

This is because you mixed up the sequence of replies, it is
about *leaving* history entry below:

> On Wed, 23 Dec 2009, Mike Wilson wrote:
> >
> > - have a notification event when leaving current history 
> >   entry 
> >   [not supported by spec, *4]
> > *4: there is no event that fires before upcoming history
> >     entry is activated (new event needed)
> On Wed, 23 Dec 2009, Justin Lebar wrote:
> > 
> > Is the use case here to allow pages to save their state 
> right before the 
> > browser navigates away?  This doesn't seem essential -- the 
> page could 
> > just call replaceState whenever the state changes -- but I 
> suppose it 
> > might be useful.
> It's unclear to me what the use case is also. Note that you 
> can already 
> detect every page navigation or transition by just hooking into the 
> popstate, hashchange, and unload events.

Text from above: "My point is that this is not always practical, 
imagine f ex serializing a large tree or rich-text editor on every 
update event."
Performance is key here; if you will be storing state that takes
a performance hit to prepare for storage, then you don't want to
save it on every user keystroke or similar, you want to know when
it is time to save and do it once.

Compare f ex with step 4 in History Traversal:
| update the current entry in the browsing context's Document 
| object's History object to reflect any state that the user agent 
| wishes to persist. [...] For example, some user agents might want 
| to persist the scroll position, or the values of form controls.

Ie, this data is persisted on demand at a certain point in the
history entry's life cycle, just as I am suggesting for the 
pushState state.
With the same reasoning as for current pushState, the spec would
instead suggest that scroll position and form control values were
persisted immediately when changed, instead of at the "leave
history entry" event.

Best regards
Mike Wilson

More information about the whatwg mailing list