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

Ian Hickson ian at hixie.ch
Wed Jan 27 15:46:33 PST 2010

On Fri, 22 Jan 2010, Mike Wilson wrote:
> I'll keep this short as there is more recent discussion:
> 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

I'm really not convinced this is a problem we need to solve. There are 
plenty of places where the Web platform doesn't cater for different parts 
of a page being written by different people who aren't coordinating. The 
whole DOM, for instance. Event handler attributes. All the storage APIs. 
Cookies. URLs. Huge parts of the platform assume that you should 
coordinate your code. I think doing so is good practice in general.

> 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.

I don't understand the above points.

> 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() {
>     History.replaceState({
>       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 updates:
>   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();
>     History.replaceState(data);
>   }
> 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(data);
>   };
>   MyHistoryWrapper.replaceState = function(data) {
>     this.cache = data;
>     History.replaceState(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.

This is just making the whole problem massively over-complicated.

Just have a global function for pushing state that takes the name of the 
module saving state, and have it keep a copy of the "current" state, so 
that it can just update the current state without reserialising 
everything, and can store the state of each module. Then when there's a 
popstate, it can see which of the modules changed states and invoke the 
relevant callback. This is a few dozen lines of code tops, and that's 
including error handling and so on.

Depending on exactly what you're doing, you can make it even simpler.

All the components have to coordinate _anyway_ to get the URL 
representation of the state into some sane form that all the components 
can understand. So I really don't think the state object needs special API 
functions to make this easier.

> [...] 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.

The problem is that you really want the URL to always be up to date, so 
that the user can copy it. And thus you really want to be calling 
pushState() whenever the state changes.

(The state stored in the state object shouldn't be especially hard to 
serialise; back/forward is a UI state navigation, not a data state 
navigation. It's not like in a word processor the user'll hit back and 
want the page to store the whole state of the user's writings into the 
history -- the text should be autosaving anyway. Hitting "back" would take 
the user into a previous state in the application, it wouldn't affect the 
user's work.

Ian Hickson               U+1047E                )\._.,--....,'``.    fL
http://ln.hixie.ch/       U+263A                /,   _.. \   _\  ;`._ ,.
Things that are impossible just take longer.   `._.-(,_..'--(,_..'`-.;.'

More information about the whatwg mailing list