11import { useState , useEffect , useCallback , useRef } from 'react'
22import Cookies from 'js-cookie'
3+ import isEqual from 'lodash/isEqual'
34
45let isLocalStorageAvailable : boolean | null = null
56
@@ -37,73 +38,73 @@ function usePersistentStorage<T>(
3738) {
3839 const { enableCookieFallback = false } = options
3940
40- // Read value once during initialization
41- const [ storedValue , setStoredValue ] = useState < T > ( ( ) => {
42- if ( typeof window === 'undefined' ) return initialValue
41+ // Initialize state with initialValue to avoid hydration mismatches.
42+ // The actual stored value will be loaded in a useEffect after mounting.
43+ const [ storedValue , setStoredValue ] = useState < T > ( initialValue )
4344
44- try {
45- const isLocalAvailable = checkLocalStorage ( )
46- // Use cookies only if localStorage is unavailable AND fallback is enabled
47- const useCookie = ! isLocalAvailable && enableCookieFallback
45+ // Use refs to avoid unnecessary re-renders or effect loops
46+ const initialValueRef = useRef ( initialValue )
47+ const keyRef = useRef ( key )
48+ const isHydrated = useRef ( false )
4849
49- let item : string | undefined | null = null
50+ // Handle hydration and key/initialValue changes
51+ useEffect ( ( ) => {
52+ const isLocalAvailable = checkLocalStorage ( )
53+ const useCookie = ! isLocalAvailable && enableCookieFallback
5054
51- if ( isLocalAvailable ) {
52- item = window . localStorage . getItem ( key )
53- } else if ( useCookie ) {
54- item = Cookies . get ( key )
55- }
55+ const loadFromStorage = ( ) => {
56+ try {
57+ let item : string | undefined | null = null
58+ if ( isLocalAvailable ) {
59+ item = window . localStorage . getItem ( key )
60+ } else if ( useCookie ) {
61+ item = Cookies . get ( key )
62+ }
5663
57- if ( item ) {
58- const parsed = JSON . parse ( item )
64+ if ( item ) {
65+ const parsed = JSON . parse ( item )
66+ let valueToUse = parsed
5967
60- if ( isPlainObject ( parsed ) && isPlainObject ( initialValue ) ) {
61- const merged = { ...initialValue , ...parsed } as T
62- // Sync merged value back to storage if using localStorage
63- if ( isLocalAvailable ) {
64- window . localStorage . setItem ( key , JSON . stringify ( merged ) )
68+ if ( isPlainObject ( parsed ) && isPlainObject ( initialValue ) ) {
69+ const merged = { ...initialValue , ...parsed } as T
70+ valueToUse = merged
71+
72+ if ( isLocalAvailable ) {
73+ window . localStorage . setItem ( key , JSON . stringify ( merged ) )
74+ }
6575 }
66- return merged
76+
77+ setStoredValue ( ( current ) => {
78+ if ( ! isEqual ( valueToUse , current ) ) {
79+ return valueToUse
80+ }
81+ return current
82+ } )
6783 }
68- return parsed
84+ } catch ( error ) {
85+ console . error (
86+ `Error reading or parsing persistent storage key “${ key } ”` ,
87+ error
88+ )
6989 }
70- } catch ( error ) {
71- console . error (
72- `Error reading or parsing persistent storage key “${ key } ”` ,
73- error
74- )
7590 }
76- return initialValue
77- } )
78-
79- // Use refs to avoid unnecessary re-renders or effect loops
80- const initialValueRef = useRef ( initialValue )
81- const keyRef = useRef ( key )
82- const isMounted = useRef ( false )
8391
84- // Handle key changes or external initialValue changes
85- useEffect ( ( ) => {
86- if ( ! isMounted . current ) {
87- isMounted . current = true
92+ if ( ! isHydrated . current ) {
93+ isHydrated . current = true
94+ loadFromStorage ( )
8895 return
8996 }
9097
9198 // Only run if key changed OR initialValue changed significantly
9299 const keyChanged = keyRef . current !== key
93- const initialValueChanged =
94- JSON . stringify ( initialValueRef . current ) !== JSON . stringify ( initialValue )
100+ const initialValueChanged = ! isEqual ( initialValueRef . current , initialValue )
95101
96- if ( ! keyChanged && ! initialValueChanged ) return
102+ if ( keyChanged || initialValueChanged ) {
103+ keyRef . current = key
104+ initialValueRef . current = initialValue
97105
98- keyRef . current = key
99- initialValueRef . current = initialValue
100-
101- if ( typeof window === 'undefined' ) return
102-
103- try {
104106 const isLocalAvailable = checkLocalStorage ( )
105107 const useCookie = ! isLocalAvailable && enableCookieFallback
106-
107108 let item : string | undefined | null = null
108109
109110 if ( isLocalAvailable ) {
@@ -113,39 +114,12 @@ function usePersistentStorage<T>(
113114 }
114115
115116 if ( item ) {
116- const parsed = JSON . parse ( item )
117- let valueToUse = parsed
118-
119- if ( isPlainObject ( parsed ) && isPlainObject ( initialValue ) ) {
120- const merged = { ...initialValue , ...parsed } as T
121- valueToUse = merged
122-
123- if ( isLocalAvailable ) {
124- window . localStorage . setItem ( key , JSON . stringify ( merged ) )
125- }
126- }
127-
128- // eslint-disable-next-line react-hooks/set-state-in-effect
129- setStoredValue ( ( current ) => {
130- if ( JSON . stringify ( valueToUse ) !== JSON . stringify ( current ) ) {
131- return valueToUse
132- }
133- return current
134- } )
117+ loadFromStorage ( )
135118 } else {
136- // If no item in storage, use initialValue
137- setStoredValue ( ( current ) => {
138- if ( JSON . stringify ( initialValue ) !== JSON . stringify ( current ) ) {
139- return initialValue
140- }
141- return current
142- } )
119+ // Safe to call setStoredValue here because the effect body executes once when dependencies change
120+ // eslint-disable-next-line react-hooks/set-state-in-effect
121+ setStoredValue ( initialValue )
143122 }
144- } catch ( error ) {
145- console . error (
146- `Error reading or parsing persistent storage key “${ key } ”` ,
147- error
148- )
149123 }
150124 } , [ key , initialValue , enableCookieFallback ] )
151125
@@ -192,7 +166,7 @@ function usePersistentStorage<T>(
192166 try {
193167 const parsed = JSON . parse ( e . newValue )
194168 setStoredValue ( ( current ) => {
195- if ( JSON . stringify ( parsed ) !== JSON . stringify ( current ) ) {
169+ if ( ! isEqual ( parsed , current ) ) {
196170 return parsed
197171 }
198172 return current
0 commit comments