Skip to content

Commit 8df1154

Browse files
feat: restore fallback for broken sessionStorage (#74)
The subset of the history of browser history stack entries within the current session is mirrored in sessionStorage so that it can be restored into Redux on page reload or on navigation from external history stack entries (using the borwser back/forward buttons, reload button, unfreezing, etc). There used to be a fallback for browsers where sessionStorage is not supported which instead stored the entries inside the current history state. This fallback was removed as part of #61. However, although all modern browsers officially support sessionStorage, there are various circumstances in which it fails to work: - if the user fills the entire quota (very unlikely that Rudy itself would do this, but other user code might) - in private browsing mode on iOS Safari - if all cookies are disabled in Chrome (and possibly also in some modified versions of chrome for android) This change restores that fallback. It comes with the same caveats that were there before: - since only the current history index is accessible, only the current and previous stack entries can be seen. This means that when returning to a stack entry in the middle of r the Rudy stack, the redux mirror of the stack entries will only include current and past entries, not future ones. - as a consequence, route callbacks that occur before a transition to a route will not work when navigating forward beyond the entries in the redux state.
1 parent 30e698c commit 8df1154

File tree

5 files changed

+73
-14
lines changed

5 files changed

+73
-14
lines changed

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
**/.gitignore
88
**/.flowconfig
99
**/.npmignore
10+
**/.eslintcache
1011

1112
# Ignored packages outside sub packages
1213
LICENSE

packages/rudy/src/history/utils/sessionStorage.js

+37-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// @flow
22
/* eslint-env browser */
33
import { toEntries } from '../../utils'
4+
import { supportsSessionStorage } from '@respond-framework/utils'
45

56
// API:
67

@@ -9,16 +10,16 @@ import { toEntries } from '../../utils'
910
// - `saveHistory` is called every time the history entries or index changes
1011
// - `restoreHistory` is called on startup obviously
1112

12-
// Essentially the idea is that if there is no `sessionStorage`, we maintain the entire
13-
// storage object on EACH AND EVERY history entry's `state`. I.e. `history.state` on
14-
// every page will have the `index` and `entries` array. That way when browsers disable
15-
// cookies/sessionStorage, we can still grab the data we need off off of history state :)
16-
//
17-
// It's a bit crazy, but it works very well, and there's plenty of space allowed for storing
18-
// things there to get a lot of mileage out of it. We store the minimum amount of data necessary.
19-
//
20-
// Firefox has the lowest limit of 640kb PER ENTRY. IE has 1mb and chrome has at least 10mb:
21-
// https://stackoverflow.com/questions/6460377/html5-history-api-what-is-the-max-size-the-state-object-can-be
13+
/**
14+
* If there is no `sessionStorage` (which happens e.g. in incognito mode in
15+
* iOS safari), we have a fallback which is to store the history of stack
16+
* entries inside the current browser history stack entry. Since we can only
17+
* access the current history stack entry, this means that if the user
18+
* returns to the middle of a set of entries within the app, then Rudy will
19+
* not be aware of the future entries. Navigation will still work, but the
20+
* entries in the redux state will not include future states, and callbacks
21+
* related to future states will therefore not work.
22+
*/
2223

2324
export const saveHistory = ({ entries }) => {
2425
entries = entries.map((e) => [e.location.url, e.state, e.location.key]) // one entry has the url, a state object, and a 6 digit key
@@ -36,17 +37,39 @@ export const restoreHistory = (api) => {
3637
}
3738

3839
export const clear = () => {
39-
window.sessionStorage.setItem(key(), '')
40+
if (supportsSessionStorage()) {
41+
window.sessionStorage.setItem(key(), '')
42+
} else {
43+
const state = window.history.state
44+
if (state) {
45+
delete state.stack
46+
window.history.replaceState(state, null)
47+
}
48+
}
4049
historySet({ index: 0, id: key() })
4150
}
4251

43-
const set = (val) => window.sessionStorage.setItem(key(), JSON.stringify(val))
52+
const set = (val) => {
53+
const json = JSON.stringify(val)
54+
if (supportsSessionStorage()) {
55+
window.sessionStorage.setItem(key(), json)
56+
} else {
57+
const state = window.history.state || {}
58+
state.stack = json
59+
window.history.replaceState(state, null)
60+
}
61+
}
4462

4563
export const get = () => {
64+
let json
65+
if (supportsSessionStorage()) {
66+
json = window.sessionStorage.getItem(key())
67+
} else {
68+
json = (window.history.state || {}).stack
69+
}
4670
try {
47-
const json = window.sessionStorage.getItem(key())
4871
return JSON.parse(json)
49-
} catch (error) {
72+
} catch {
5073
return null
5174
}
5275
}

packages/scroll-restorer/src/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
ScrollRestorer,
1414
ScrollRestorerCreator,
1515
} from '@respond-framework/types'
16+
import { supportsSessionStorage } from '@respond-framework/utils'
1617

1718
export { ScrollPosition } from 'scroll-behavior'
1819

@@ -57,6 +58,9 @@ export class RudyScrollRestorer<Action extends FluxStandardRoutingAction>
5758
key: string | null,
5859
value: ScrollPosition,
5960
): void => {
61+
if (!supportsSessionStorage()) {
62+
return
63+
}
6064
window.sessionStorage.setItem(
6165
this.makeStorageKey(entry, key),
6266
JSON.stringify(value),
@@ -67,6 +71,9 @@ export class RudyScrollRestorer<Action extends FluxStandardRoutingAction>
6771
entry: LocationEntry<Action>,
6872
key: string | null,
6973
): ScrollPosition | null => {
74+
if (!supportsSessionStorage()) {
75+
return null
76+
}
7077
const savedItem = window.sessionStorage.getItem(
7178
this.makeStorageKey(entry, key),
7279
)

packages/utils/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { default as isServer } from './isServer'
22
export { default as createSelector } from './createSelector'
3+
export { default as supportsSessionStorage } from './supportsSessionStorage'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/* eslint-env browser */
2+
3+
let _supportsSessionStorage: boolean
4+
5+
export default (): boolean => {
6+
if (_supportsSessionStorage !== undefined) {
7+
return _supportsSessionStorage
8+
}
9+
try {
10+
window.sessionStorage.setItem('rudytestitem', 'testvalue')
11+
if (window.sessionStorage.getItem('rudytestitem') === 'testvalue') {
12+
window.sessionStorage.removeItem('rudytestitem')
13+
_supportsSessionStorage = true
14+
} else {
15+
_supportsSessionStorage = false
16+
}
17+
} catch {
18+
_supportsSessionStorage = false
19+
}
20+
if (!_supportsSessionStorage) {
21+
// eslint-disable-next-line no-console
22+
console.warn(
23+
'[rudy]: WARNING: This browser does not support sessionStorage!',
24+
)
25+
}
26+
return _supportsSessionStorage
27+
}

0 commit comments

Comments
 (0)