Repository navigation
Update vs. replace, entry mutability, and keys #7
Description
(Moved from slightlyoff/history_api#21 by @StringEpsilon, summarized by me.)
Update vs. replace
The current proposal has appHistory.updateCurrentEntry({ state, url }). Although it doesn't state so explicitly, the implication of this method name is that the same AppHistoryEntry will have its state and url values updated. That is, it makes AppHistoryEntrys mutable, at least when they are the current entry. Notably, the key value would stay the same. Call this the "mutable history entry model".
An alternative approach would be to have appHistory.replaceCurrentEntry({ state, url }). This would generate a new AppHistoryEntry with the provided state and url. It would probably have a new key (but see below). Call this the "immutable history entry model".
Also, note that the mutable model could have appHistory.replaceCurrentEntry() as well as appHistory.updateCurrentEntry(). Probably that would just be confusing, but it makes conceptual sense.
(For the record: window.history takes a hybrid approach. The current window.history history entry's state is mutable (via history.state), but if you want to change the page's current URL, you need to replace the current history entry using history.replaceState().)
In general in programming, immutability can make things easier with less to worry about. That might be the case here, so I'm leaning toward switching to an immutable history entry model.
The meaning and uses of key
In @tbondwilkinson's work on creating nice application-level wrappers over window.history, they have (at least) two keys:
- A "unique identifier" for the history entry, which is changed whenever the entry is replaced (i.e. whenever the URL of the entry changes). (Does it change if the state changes??)
- A "slot key" for the history entry, which stays stable even when the entry is replaced.
His apps use the slot key to navigate around, using the equivalent of appHistory.navigateTo(slotKey). Whereas the unique identifier is used to cache resources that are too big to store in state, or to otherwise correlate a particular history entry with some out of band resource.
The app history proposal, with updateCurrentEntry(), currently has only the "slot key". If we changed to the immutable history entry model, with replaceCurrentEntry(), we could either:
- keep
keyas the slot key (so, the newAppHistoryEntrywould have the samekeyvalue as the old one), or - switch
keyto be a unique identifier (so, the newAppHistoryEntrywould generate a newkeyUUID).
We could also introduce both types of keys, or switch key to be a unique identifier and introduce index, or similar.
A potential issue with unique keys is that we need to define the behavior for what happens if you do something like
const uniqueKey = appHistory.currentEntry.key;
appHistory.replaceCurrentEntry();
appHistory.navigateTo(uniqueKey);i.e. if you try to navigate to a key which is no longer in the current history entries list. I think we'd just fail, probably in the same way appHistory.back() fails if you're at the beginning of the history list, or appHistory.navigateTo("non-existant-key") fails.
Replacing and the old entry
If we did switch to an immutable history model with appHistory.replaceCurrentEntry(), we could consider adding a feature like appHistory.currentEntry.replacedEntry pointing to the now-replaced entry. This would extend indefinitely (?), so you could do appHistory.currentEntry.replacedEntry.replacedEntry etc.
This also helps with the potential data loss, where calling replaceCurrentEntry(), or updateCurrentEntry(), will destroy any state value you had there. On the other hand, maybe this sort of "data loss" is intentional, in that it's better not to use up memory and storage space on replaced entries. Hmm.
Activity
esprehn commentedon Feb 3, 2021
I think we need a stable key or index to handle the react View key. For example if you navigate to /inbox and then /message we'll generate a react tree like:
If pushing a new state changes the key it would destroy that view. Should we be using the index in the entries() list as the key instead?
react-router creates a new object for each replace() even if the url is actually the same and the only thing that changed is the state. That's required us to manually memoize the url state to avoid huge re-renders of the entire page. Ideally the thing that contains the url would have stable object identity so you know you're on the same "view" and things that only care about the url don't update, even if the state object is changing.
domenic commentedon Feb 3, 2021
That concrete example is super-helpful; thank you!
So it sounds like most of your cases would be served by the current proposal, i.e. update instead of replace, with
keystaying the same? Do you see a use for a key that changes if you change the state or URL? It sounds like the immutability of react-router is mostly not-helpful for you; is that right, or does it have any positives?It seems like there's three potential keys here: the object identity, a
keyproperty, and theurlproperty. I'm pretty sold onurlnot being unique enough for this purpose, but can you expand on why you think the object identity is more useful than akeyproperty?Yay295 commentedon Feb 4, 2021
I think entry mutability would simplify a lot of common actions. The main downside I see is that it could lead to people writing code that mutates the history state all over the place. On the other hand you can do this anyways by replacing the state; it's just more tedious.
esprehn commentedon Feb 9, 2021
@domenic maybe I misunderstood the original post here, it seemed to suggest both removing object identity for Entries as well as changing the
keyon each mutation. The major issue we have with react-router is it seems to create a new object one very render even if the internal values are the same. Ideally we'd have a stable string key for the route entry itself, and the underlying data would be immutable and swap on mutation.Something like:
where entry's identity changes on all mutations, but the top level object and key don't change. I don't have a good name for that though.
tbondwilkinson commentedon Feb 11, 2021
@esprehn I think this is where a key that persists on replaces comes in handy.
Then in React:
I'm not super familiar with React, but I suspect this works?
I think it's undesirable to allow mutable history entries, because it implies things like this are synchronous:
This gets really confusing. If we allow SOME changes to Entries to be mutable, but other to be immutable because they trigger navigations (like changing the URL), then I think our API becomes pretty surprising.
My opinion would be that immutability is desirable, and replace() operations more closely align with the existing history API and would make interop and migrations from it easier.
esprehn commentedon Feb 11, 2021
@tbondwilkinson I don't understand what you're saying there, the state object is mutable already today.
and it looks mutable in the current spec too? Are you saying you want the platform to deeply freeze the state object? I'm not suggesting the url field on Entry is writable.
tbondwilkinson commentedon Feb 11, 2021
The state Object is "mutable" - this is actually a sore spot in the current API, I think.
Mutations to history.state are allowed but they don't actually change the serialized value that the browser uses. This is pretty surprising - only a replaceState or pushState would change the serialized value, even though the browser allows you to "modify" the state Object directly.
tbondwilkinson commentedon Feb 11, 2021
So to be clear, I would like to fix this in the new API to make it so that you cannot even modify the appHistory's currentEntry state.
domenic commentedon Feb 11, 2021
Hmm, that's an interesting proposal. It would definitely require more structure then; we couldn't use an arbitrary JavaScript value (since those are always mutable).
I tend to agree that's a sharp edge of the current API though, so I think fixing it is a good direction.
domenic commentedon Feb 11, 2021
To respond to
I think a lot of options are/were on the table, and some of them are orthogonal. I apologize for the over-long OP. Let me try a shorter proposal and get your impression.
I'm relatively convinced we need separate
key(stable, immutable) andid(changes on "mutation"). (Better names might be good...) But then we have a choice on how to model "mutation":AppHistoryEntryobject identity stays the same forever. Then we probably call the methodupdate({ url, state }). In this worldkeyis technically redundant, but probably still useful.AppHistoryEntryobject identity gets replaced on mutation. Then we probably call the methodreplace({ url, state }). In this worldidis technically redundant, but probably still useful.Do folks have thoughts between these two? (Note that I think either model could work with some immutable form of state, if we went that route.)
Yay295 commentedon Feb 12, 2021
If you replace the state I wouldn't really think of it as just the object identity being replaced, but the entire object. That is, the original object could still exist somewhere (at least until it's garbage-collected) and you're now dealing with a new object with its own identity. In this case you wouldn't really need the
keybecause you could just keep a reference to the entry object itself.I don't really see the benefit of invalidating a key. If you want a new history entry you can just create one.
As for state immutability, I think it's only the current entry that causes issues. Changing the values of a non-active entry shouldn't affect anything. Being able to change the current state and then not have it actually be saved is an issue, but I don't think that's the fault of it being mutable. We just have to say that changes to a history state need to be persisted.
domenic commentedon Feb 12, 2021
I moved the discussion on state immutability to #36.
tbondwilkinson commentedon Feb 17, 2021
This is my preference because I don't want someone to think that they can use
===to detect a URL change with a previous current history, when in fact a replace mutation might have occurred (and instead, I would prefer them to depend explicitly on the id field if they care about "slot" identity vs unique identity)domenic commentedon Mar 30, 2021
If you do
history.replaceState(newState), i.e. do not change the URL, should that change theAppHistoryEntryobject identity and itsid?Certainly
history.replaceState(newState, "", newURL)should update, but I'm less sure aboutwindow.history-only state changes impacting app history entries.domenic commentedon Mar 30, 2021
Also, we never really answered: should
idchange if onlystatechanges (and the URL stays the same)?tbondwilkinson commentedon Mar 30, 2021
I do think it should change the
AppHistoryEntryobject identity and its id.I think making a distinction between state and URL identities is potentially confusing, because I think "state" is just non-user-visible identifying state. If you want to link metadata about history that does not constitute the identity of an entry, you can put that elsewhere pretty easily
Add appHistoryEntry.id and clarify object identity
Yay295 commentedon Mar 30, 2021
If the ID changes when you update the state, what happens to the old ID? Does it just become invalid, or does the old state stick around, or does the old ID refer to the new state? What is the purpose of the ID supposed to be?
I think the object identity not changing could be difficult implementation wise, though I don't know exactly how browsers determine an objects identity.
tbondwilkinson commentedon Mar 31, 2021
I think the id is meant to provide a unique identifier to stash resources about that "entry". So, if you update the state, that's no longer the same entry, because maybe something about that state was influencing what UI you're displaying (like scroll positions, or images that you're caching), so you may want to drop them.
Add appHistoryEntry.id and clarify object identity