11import type { Dispatch , SetStateAction } from 'react'
22import { useCallback , useEffect , useMemo , useRef , useState , useSyncExternalStore } from 'react'
33
4- // in memory fallback used then `sessionStorage` throws an error
4+ // in memory fallback used when `sessionStorage` throws an error
55export const inMemoryData = new Map < string , unknown > ( )
66
77export type SessionStorageOptions < T > = {
@@ -26,7 +26,7 @@ export type SessionStorageState<T> = [
2626
2727export default function useSessionStorageState (
2828 key : string ,
29- options ?: Omit < SessionStorageOptions < unknown > , 'defaultValue' > ,
29+ options ?: SessionStorageOptions < undefined > ,
3030) : SessionStorageState < unknown >
3131export default function useSessionStorageState < T > (
3232 key : string ,
@@ -40,27 +40,8 @@ export default function useSessionStorageState<T = undefined>(
4040 key : string ,
4141 options ?: SessionStorageOptions < T | undefined > ,
4242) : SessionStorageState < T | undefined > {
43- const [ defaultValue ] = useState ( options ?. defaultValue )
44-
45- // SSR support
46- // - on the server, return a constant value
47- // - this makes the implementation simpler and smaller because the `sessionStorage` object is
48- // `undefined` on the server
49- if ( typeof window === 'undefined' ) {
50- return [
51- defaultValue ,
52- ( ) : void => { } ,
53- {
54- isPersistent : true ,
55- removeItem : ( ) : void => { } ,
56- } ,
57- ]
58- }
59-
6043 const serializer = options ?. serializer
61- // disabling ESLint because the above if statement can be executed only on the server. the value
62- // of `window` can't change between calls.
63- // eslint-disable-next-line react-hooks/rules-of-hooks
44+ const [ defaultValue ] = useState ( options ?. defaultValue )
6445 return useBrowserSessionStorageState (
6546 key ,
6647 defaultValue ,
@@ -77,35 +58,18 @@ function useBrowserSessionStorageState<T>(
7758 parse : ( value : string ) => unknown = parseJSON ,
7859 stringify : ( value : unknown ) => string = JSON . stringify ,
7960) : SessionStorageState < T | undefined > {
80- // store default value in sessionStorage:
81- // - initial issue: https://github.com/astoilkov/use-local-storage-state/issues/26
82- // issues that were caused by incorrect initial and secondary implementations:
83- // - https://github.com/astoilkov/use-local-storage-state/issues/30
84- // - https://github.com/astoilkov/use-local-storage-state/issues/33
85- if (
86- ! inMemoryData . has ( key ) &&
87- defaultValue !== undefined &&
88- goodTry ( ( ) => sessionStorage . getItem ( key ) ) === null
89- ) {
90- // reasons for `sessionStorage` to throw an error:
91- // - maximum quota is exceeded
92- // - under Mobile Safari (since iOS 5) when the user enters private mode
93- // `sessionStorage.setItem()` will throw
94- // - trying to access sessionStorage object when cookies are disabled in Safari throws
95- // "SecurityError: The operation is insecure."
96- goodTry ( ( ) => sessionStorage . setItem ( key , stringify ( defaultValue ) ) )
97- }
98-
9961 // we keep the `parsed` value in a ref because `useSyncExternalStore` requires a cached version
100- const storageValue = useRef < { item : string | null ; parsed : T | undefined } > ( {
101- item : null ,
102- parsed : defaultValue ,
62+ const storageItem = useRef < { string : string | null ; parsed : T | undefined } > ( {
63+ string : null ,
64+ parsed : undefined ,
10365 } )
66+
10467 const value = useSyncExternalStore (
68+ // useSyncExternalStore.subscribe
10569 useCallback (
10670 ( onStoreChange ) => {
107- const onChange = ( localKey : string ) : void => {
108- if ( key === localKey ) {
71+ const onChange = ( sessionKey : string ) : void => {
72+ if ( key === sessionKey ) {
10973 onStoreChange ( )
11074 }
11175 }
@@ -117,40 +81,56 @@ function useBrowserSessionStorageState<T>(
11781 [ key ] ,
11882 ) ,
11983
120- // eslint-disable-next-line react-hooks/exhaustive-deps
84+ // useSyncExternalStore.getSnapshot
12185 ( ) => {
122- const item = goodTry ( ( ) => sessionStorage . getItem ( key ) ) ?? null
86+ const string = goodTry ( ( ) => sessionStorage . getItem ( key ) ) ?? null
12387
12488 if ( inMemoryData . has ( key ) ) {
125- storageValue . current = {
126- item,
127- parsed : inMemoryData . get ( key ) as T | undefined ,
128- }
129- } else if ( item !== storageValue . current . item ) {
89+ storageItem . current . parsed = inMemoryData . get ( key ) as T | undefined
90+ } else if ( string !== storageItem . current . string ) {
13091 let parsed : T | undefined
13192
13293 try {
133- parsed = item === null ? defaultValue : ( parse ( item ) as T )
94+ parsed = string === null ? defaultValue : ( parse ( string ) as T )
13495 } catch {
13596 parsed = defaultValue
13697 }
13798
138- storageValue . current = {
139- item,
140- parsed,
141- }
99+ storageItem . current . parsed = parsed
142100 }
143101
144- return storageValue . current . parsed
102+ storageItem . current . string = string
103+
104+ // store default value in sessionStorage:
105+ // - initial issue: https://github.com/astoilkov/use-local-storage-state/issues/26
106+ // issues that were caused by incorrect initial and secondary implementations:
107+ // - https://github.com/astoilkov/use-local-storage-state/issues/30
108+ // - https://github.com/astoilkov/use-local-storage-state/issues/33
109+ if ( string === null && defaultValue !== undefined ) {
110+ // reasons for `sessionStorage` to throw an error:
111+ // - maximum quota is exceeded
112+ // - under Mobile Safari (since iOS 5) when the user enters private mode
113+ // `sessionStorage.setItem()` will throw
114+ // - trying to access sessionStorage object when cookies are disabled in Safari throws
115+ // "SecurityError: The operation is insecure."
116+ // eslint-disable-next-line no-console
117+ goodTry ( ( ) => {
118+ const string = stringify ( defaultValue )
119+ sessionStorage . setItem ( key , string )
120+ storageItem . current = { string, parsed : defaultValue }
121+ } )
122+ }
123+
124+ return storageItem . current . parsed
145125 } ,
146126
147- // istanbul ignore next
127+ // useSyncExternalStore.getServerSnapshot
148128 ( ) => defaultValue ,
149129 )
150130 const setState = useCallback (
151131 ( newValue : SetStateAction < T | undefined > ) : void => {
152132 const value =
153- newValue instanceof Function ? newValue ( storageValue . current . parsed ) : newValue
133+ newValue instanceof Function ? newValue ( storageItem . current . parsed ) : newValue
154134
155135 // reasons for `sessionStorage` to throw an error:
156136 // - maximum quota is exceeded
@@ -226,7 +206,5 @@ function parseJSON(value: string): unknown {
226206function goodTry < T > ( tryFn : ( ) => T ) : T | undefined {
227207 try {
228208 return tryFn ( )
229- } catch {
230- return undefined
231- }
209+ } catch { }
232210}
0 commit comments