react-wire-persisted 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,80 +1,87 @@
1
1
  {
2
- "name": "react-wire-persisted",
3
- "version": "2.0.0",
4
- "author": "Wesley Bliss <wesley.bliss@gmail.com>",
5
- "license": "MIT",
6
- "type": "module",
7
- "main": "./dist/react-wire-persisted.umd.cjs",
8
- "module": "./dist/index.js",
9
- "files": [
10
- "src",
11
- "dist",
12
- "client.js",
13
- "client.mjs",
14
- "nextjs.js"
15
- ],
16
- "exports": {
17
- ".": {
18
- "import": "./dist/react-wire-persisted.js",
19
- "require": "./dist/react-wire-persisted.umd.cjs"
2
+ "name": "react-wire-persisted",
3
+ "version": "2.1.0",
4
+ "author": "Wesley Bliss <wesley.bliss@gmail.com>",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/react-wire-persisted.umd.cjs",
8
+ "module": "./dist/index.js",
9
+ "files": [
10
+ "src",
11
+ "dist",
12
+ "client.js",
13
+ "client.mjs",
14
+ "nextjs.js"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/react-wire-persisted.js",
19
+ "require": "./dist/react-wire-persisted.umd.cjs"
20
+ },
21
+ "./client": {
22
+ "import": "./client.mjs",
23
+ "require": "./client.js"
24
+ },
25
+ "./nextjs": {
26
+ "import": "./nextjs.js",
27
+ "require": "./nextjs.js"
28
+ }
29
+ },
30
+ "scripts": {
31
+ "dev": "vite --host --config config/vite.config.development.js",
32
+ "build": "vite build --config config/vite.config.production.js && cp dist/react-wire-persisted.js dist/index.js",
33
+ "lint": "biome check --write",
34
+ "lint:format": "biome format --write",
35
+ "lint:lint": "biome lint --write",
36
+ "test:unit": "NODE_ENV=test vitest run",
37
+ "test:unit:only": "NODE_ENV=test vitest run -t ",
38
+ "test:unit:coverage": "NODE_ENV=test vitest run --no-color --reporter=junit --coverage --outputFile=coverage/report.xml",
39
+ "yalc": "pnpm build && yalc push"
40
+ },
41
+ "devDependencies": {
42
+ "@biomejs/biome": "^2.3.14",
43
+ "@forminator/react-wire": "^0.7.0",
44
+ "@testing-library/dom": "^10.4.1",
45
+ "@testing-library/jest-dom": "^6.8.0",
46
+ "@testing-library/react": "^16.3.0",
47
+ "@testing-library/user-event": "^14.6.1",
48
+ "@vitejs/plugin-react": "^5.0.3",
49
+ "@vitest/coverage-v8": "^4.0.18",
50
+ "browserslist": "^4.26.2",
51
+ "dotenv": "^17.2.2",
52
+ "esbuild": "^0.25.10",
53
+ "jest": "^30.1.3",
54
+ "jest-environment-jsdom": "^30.1.2",
55
+ "np": "^10.2.0",
56
+ "react": "^19.1.1",
57
+ "react-dom": "^19.1.1",
58
+ "rollup-plugin-inject-process-env": "^1.3.1",
59
+ "snapshot-diff": "^0.10.0",
60
+ "vite": "7.3.1",
61
+ "vite-jest": "^0.1.4",
62
+ "vitest": "^4.0.18"
20
63
  },
21
- "./client": {
22
- "import": "./client.mjs",
23
- "require": "./client.js"
64
+ "peerDependencies": {
65
+ "@forminator/react-wire": "^0.7.0",
66
+ "react": "^18.3.1 || ^19.0.0",
67
+ "react-dom": "^18.3.1 || ^19.0.0"
24
68
  },
25
- "./nextjs": {
26
- "import": "./nextjs.js",
27
- "require": "./nextjs.js"
69
+ "browserslist": {
70
+ "production": [
71
+ ">0.2%",
72
+ "not dead",
73
+ "not op_mini all"
74
+ ],
75
+ "development": [
76
+ "last 1 chrome version",
77
+ "last 1 firefox version",
78
+ "last 1 safari version, not dead"
79
+ ]
80
+ },
81
+ "pnpm": {
82
+ "neverBuiltDependencies": []
83
+ },
84
+ "dependencies": {
85
+ "react-wire-persisted": "^2.0.1"
28
86
  }
29
- },
30
- "scripts": {
31
- "dev": "vite --host --config config/vite.config.development.js",
32
- "build": "vite build --config config/vite.config.production.js && cp dist/react-wire-persisted.js dist/index.js",
33
- "test:unit": "NODE_ENV=test vitest run",
34
- "test:unit:only": "NODE_ENV=test vitest run -t ",
35
- "test:unit:coverage": "NODE_ENV=test vitest run --no-color --reporter=junit --coverage --outputFile=coverage/report.xml",
36
- "yalc": "pnpm build && yalc push"
37
- },
38
- "devDependencies": {
39
- "@forminator/react-wire": "^0.7.0",
40
- "@testing-library/jest-dom": "^6.8.0",
41
- "@testing-library/react": "^16.3.0",
42
- "@testing-library/user-event": "^14.6.1",
43
- "@vitejs/plugin-react": "^5.0.3",
44
- "@vitest/coverage-c8": "^0.33.0",
45
- "browserslist": "^4.26.2",
46
- "c8": "^10.1.3",
47
- "dotenv": "^17.2.2",
48
- "esbuild": "^0.25.10",
49
- "jest": "^30.1.3",
50
- "jest-environment-jsdom": "^30.1.2",
51
- "np": "^10.2.0",
52
- "react": "^19.1.1",
53
- "react-dom": "^19.1.1",
54
- "rollup-plugin-inject-process-env": "^1.3.1",
55
- "snapshot-diff": "^0.10.0",
56
- "vite": "^7.1.7",
57
- "vite-jest": "^0.1.4",
58
- "vitest": "^3.2.4"
59
- },
60
- "peerDependencies": {
61
- "@forminator/react-wire": "^0.5.0-alpha.1",
62
- "react": "^18.3.1",
63
- "react-dom": "^18.3.1"
64
- },
65
- "browserslist": {
66
- "production": [
67
- ">0.2%",
68
- "not dead",
69
- "not op_mini all"
70
- ],
71
- "development": [
72
- "last 1 chrome version",
73
- "last 1 firefox version",
74
- "last 1 safari version"
75
- ]
76
- },
77
- "pnpm": {
78
- "neverBuiltDependencies": []
79
- }
80
87
  }
@@ -1,8 +1,8 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useRef } from 'react'
4
- import { upgradeStorage } from '../react-wire-persisted'
5
- import { getIsClient, getHasHydrated } from '../utils'
4
+ import { upgradeStorage } from '../react-wire-persisted.js'
5
+ import { getHasHydrated, getIsClient } from '../utils/index.js'
6
6
 
7
7
  /**
8
8
  * A Next.js App Router compatible component that handles automatic storage upgrade
@@ -42,4 +42,4 @@ export function HydrationProvider({ children, onUpgrade, autoUpgrade = true }) {
42
42
  }, [autoUpgrade, onUpgrade])
43
43
 
44
44
  return children
45
- }
45
+ }
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useHydration } from '../hooks/useHydration'
3
+ import { useHydration } from '../hooks/useHydration.js'
4
4
 
5
5
  /**
6
6
  * A Next.js App Router compatible component that handles automatic storage upgrade
@@ -14,4 +14,4 @@ import { useHydration } from '../hooks/useHydration'
14
14
  export function HydrationProvider({ children, onUpgrade, autoUpgrade = true }) {
15
15
  useHydration({ onUpgrade, autoUpgrade })
16
16
  return children
17
- }
17
+ }
@@ -1 +1 @@
1
- export { HydrationProvider } from './HydrationProvider.js'
1
+ export { HydrationProvider } from './HydrationProvider.js'
@@ -1,8 +1,8 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useRef } from 'react'
4
- import { upgradeStorage } from '../react-wire-persisted'
5
- import { getIsClient, getHasHydrated } from '../utils'
4
+ import { upgradeStorage } from '../react-wire-persisted.js'
5
+ import { getHasHydrated, getIsClient } from '../utils/index.js'
6
6
 
7
7
  /**
8
8
  * React hook that handles automatic storage upgrade after hydration
@@ -13,46 +13,34 @@ import { getIsClient, getHasHydrated } from '../utils'
13
13
  * @param {Function} options.onUpgrade Callback called when storage is upgraded
14
14
  */
15
15
  export const useHydration = (options = {}) => {
16
-
17
- const {
18
- autoUpgrade = true,
19
- onUpgrade
20
- } = options
21
-
16
+ const { autoUpgrade = true, onUpgrade } = options
17
+
22
18
  const hasUpgraded = useRef(false)
23
-
19
+
24
20
  useEffect(() => {
25
-
26
- if (!autoUpgrade || hasUpgraded.current || !getIsClient())
27
- return
28
-
21
+ if (!autoUpgrade || hasUpgraded.current || !getIsClient()) return
22
+
29
23
  const attemptUpgrade = () => {
30
-
31
24
  if (getHasHydrated() && !hasUpgraded.current) {
32
-
33
25
  const upgraded = upgradeStorage()
34
-
26
+
35
27
  if (upgraded) {
36
28
  hasUpgraded.current = true
37
29
  onUpgrade?.()
38
30
  }
39
-
40
31
  }
41
-
42
32
  }
43
-
33
+
44
34
  // Try to upgrade immediately if already hydrated
45
35
  attemptUpgrade()
46
-
36
+
47
37
  // Also try after a short delay to ensure DOM is ready
48
38
  const timeoutId = setTimeout(attemptUpgrade, 0)
49
-
39
+
50
40
  return () => clearTimeout(timeoutId)
51
-
52
41
  }, [autoUpgrade, onUpgrade])
53
-
42
+
54
43
  return {
55
- hasUpgraded: hasUpgraded.current
44
+ hasUpgraded: hasUpgraded.current,
56
45
  }
57
-
58
46
  }
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as utils from './utils'
2
2
 
3
3
  export { utils }
4
- export * from './react-wire-persisted'
5
- export * from './hooks/useHydration'
6
4
  export * from './components'
5
+ export * from './hooks/useHydration'
6
+ export * from './react-wire-persisted'
@@ -1,23 +1,21 @@
1
- import StorageProvider from './StorageProvider'
2
- import { fakeLocalStorage, isPrimitive, isLocalStorageAvailable } from '../utils'
1
+ import { fakeLocalStorage, isLocalStorageAvailable, isPrimitive } from '../utils/index.js'
2
+ import StorageProvider from './StorageProvider.js'
3
3
 
4
4
  /**
5
5
  * A storage provider for `localStorage`
6
6
  * @see `StorageProvider.js` for documentation
7
7
  */
8
8
  class LocalStorageProvider extends StorageProvider {
9
-
10
9
  constructor(namespace = null, registry = {}) {
11
-
12
10
  super(namespace, registry)
13
11
 
14
- this.storage = this.getStorage()
15
- this._isUsingFakeStorage = !isLocalStorageAvailable()
16
-
12
+ // Always start with fake storage to prevent hydration mismatches
13
+ // Will be upgraded to real storage after hydration via upgradeToRealStorage()
14
+ this.storage = fakeLocalStorage
15
+ this._isUsingFakeStorage = true
17
16
  }
18
-
19
- getStorage() {
20
17
 
18
+ getStorage() {
21
19
  // Use the isomorphic utility to check localStorage availability
22
20
  if (isLocalStorageAvailable()) {
23
21
  return window.localStorage
@@ -25,124 +23,91 @@ class LocalStorageProvider extends StorageProvider {
25
23
 
26
24
  // Fallback to fake localStorage for SSR or when localStorage is disabled
27
25
  return fakeLocalStorage
28
-
29
26
  }
30
-
27
+
31
28
  setNamespace(namespace) {
32
-
33
29
  if (!this.namespace) {
34
30
  this.namespace = namespace
35
31
  return
36
32
  }
37
-
38
- if (this.namespace === namespace)
39
- return
40
-
33
+
34
+ if (this.namespace === namespace) return
35
+
41
36
  const items = JSON.parse(JSON.stringify(this.getAll()))
42
-
37
+
43
38
  this.removeAll()
44
-
39
+
45
40
  for (const [key, value] of Object.entries(items)) {
46
41
  const newKey = key.replace(this.namespace, namespace)
47
42
  this.setItem(newKey, value)
48
43
  }
49
-
44
+
50
45
  this.namespace = namespace
51
-
52
46
  }
53
-
47
+
54
48
  getItem(key) {
55
-
56
49
  const val = this.storage.getItem(key)
57
-
58
- if (val === undefined || val === null)
59
- return null
60
-
50
+
51
+ if (val === undefined || val === null) return null
52
+
61
53
  try {
62
54
  return JSON.parse(val)
63
- } catch (e) {
55
+ } catch (_) {
64
56
  return val
65
57
  }
66
-
67
58
  }
68
-
59
+
69
60
  setItem(key, value) {
70
-
71
61
  let val = value
72
-
62
+
73
63
  // Don't allow "null" & similar values to be stringified
74
- if (val !== undefined && val !== null)
75
- val = isPrimitive(value) ? value : JSON.stringify(value)
76
-
64
+ if (val !== undefined && val !== null) val = isPrimitive(value) ? value : JSON.stringify(value)
65
+
77
66
  return this.storage.setItem(key, val)
78
-
79
67
  }
80
-
68
+
81
69
  removeItem(key, fromRegistry = false) {
82
-
83
- if (fromRegistry)
84
- delete this.registry[key]
85
-
70
+ if (fromRegistry) delete this.registry[key]
71
+
86
72
  return this.storage.removeItem(key)
87
-
88
73
  }
89
-
74
+
90
75
  getAll() {
91
-
92
76
  const prefixNs = `${this.namespace}.`
93
-
77
+
94
78
  return Object.keys(this.storage).reduce((acc, it) => {
95
-
96
- if (this.namespace ? it.startsWith(prefixNs) : true)
97
- acc[it] = this.storage.getItem(it)
98
-
79
+ if (this.namespace ? it.startsWith(prefixNs) : true) acc[it] = this.storage.getItem(it)
80
+
99
81
  return acc
100
-
101
82
  }, {})
102
-
103
83
  }
104
-
105
- _resetAll(
106
- useInitialValues = true,
107
- excludedKeys = [],
108
- clearRegistry = false
109
- ) {
110
84
 
85
+ _resetAll(useInitialValues = true, excludedKeys = [], clearRegistry = false) {
111
86
  const prefixNs = `${this.namespace}.`
112
87
 
113
- Object.keys(this.storage).forEach(it => {
114
-
88
+ Object.keys(this.storage).forEach((it) => {
115
89
  const isAppKey = this.namespace ? it.startsWith(prefixNs) : true
116
90
  const isExcluded = excludedKeys?.includes(it) || false
117
91
 
118
92
  if (!isAppKey || isExcluded) return
119
93
 
120
94
  if (useInitialValues) {
95
+ const isRegistered = Object.hasOwn(this.registry, it)
121
96
 
122
- const isRegistered = Object.prototype.hasOwnProperty.call(this.registry, it)
123
-
124
- if (isRegistered)
125
- this.storage.setItem(it, this.registry[it])
126
- else
127
- this.storage.removeItem(it)
128
-
97
+ if (isRegistered) this.storage.setItem(it, this.registry[it])
98
+ else this.storage.removeItem(it)
129
99
  } else {
130
-
131
100
  this.storage.removeItem(it)
132
101
 
133
- if (clearRegistry)
134
- delete this.registry[it]
135
-
102
+ if (clearRegistry) delete this.registry[it]
136
103
  }
137
-
138
104
  })
139
-
140
105
  }
141
-
106
+
142
107
  resetAll(excludedKeys = [], clearRegistry = false) {
143
108
  this._resetAll(true, excludedKeys || [], clearRegistry)
144
109
  }
145
-
110
+
146
111
  removeAll(excludedKeys = [], clearRegistry = false) {
147
112
  this._resetAll(false, excludedKeys || [], clearRegistry)
148
113
  }
@@ -152,7 +117,6 @@ class LocalStorageProvider extends StorageProvider {
152
117
  * This is useful for hydration scenarios
153
118
  */
154
119
  upgradeToRealStorage() {
155
-
156
120
  if (!this._isUsingFakeStorage) {
157
121
  return false // Already using real storage
158
122
  }
@@ -161,24 +125,11 @@ class LocalStorageProvider extends StorageProvider {
161
125
  return false // Real storage still not available
162
126
  }
163
127
 
164
- const fakeData = { ...this.storage }
128
+ // Simply switch to real storage - don't migrate fake data
129
+ // The existing persisted data in localStorage should be preserved
165
130
  this.storage = window.localStorage
166
131
  this._isUsingFakeStorage = false
167
132
 
168
- // Migrate data from fake storage to real storage
169
- Object.keys(fakeData).forEach(key => {
170
- if (key !== '__IS_FAKE_LOCAL_STORAGE__' && fakeData[key] != null) {
171
- try {
172
- this.storage.setItem(key, fakeData[key])
173
- } catch (e) {
174
- // If we can't write to localStorage, revert to fake storage
175
- this.storage = fakeLocalStorage
176
- this._isUsingFakeStorage = true
177
- return false
178
- }
179
- }
180
- })
181
-
182
133
  return true
183
134
  }
184
135
 
@@ -188,7 +139,6 @@ class LocalStorageProvider extends StorageProvider {
188
139
  isUsingFakeStorage() {
189
140
  return this._isUsingFakeStorage
190
141
  }
191
-
192
142
  }
193
143
 
194
144
  export default LocalStorageProvider
@@ -1,20 +1,14 @@
1
- import LocalStorageProvider from './LocalStorageProvider'
2
- import { fakeLocalStorage } from '../utils'
1
+ import { fakeLocalStorage } from '../utils/index.js'
2
+ import LocalStorageProvider from './LocalStorageProvider.js'
3
3
 
4
4
  class MemoryStorageProvider extends LocalStorageProvider {
5
-
6
5
  constructor(namespace = null, registry = {}) {
7
-
8
6
  super(namespace, registry)
9
-
10
7
  }
11
-
8
+
12
9
  getStorage() {
13
-
14
10
  return fakeLocalStorage
15
-
16
11
  }
17
-
18
12
  }
19
13
 
20
14
  export default MemoryStorageProvider
@@ -1,26 +1,23 @@
1
-
2
1
  /**
3
2
  * Base class to allow storage access
4
3
  * @see `LocalStorageProvider.js` for an example implementation
5
4
  */
5
+ /** biome-ignore-all lint/correctness/noUnusedFunctionParameters: WIP next PR will switch to TypeScript */
6
6
  class StorageProvider {
7
-
8
7
  /**
9
8
  * Initializes the class
10
9
  * @param {String} namespace Namespace to prefix all keys with. Mostly used for the logging & reset functions
11
10
  * @param {Object} registry (Optional) Initialize the storage provider with an existing registry
12
11
  */
13
12
  constructor(namespace, registry) {
14
-
15
13
  // Simulate being an abstract class
16
14
  if (new.target === StorageProvider)
17
15
  throw TypeError(`StorageProvider is abstract. Extend this class to implement it`)
18
-
16
+
19
17
  this.namespace = namespace || null
20
18
  this.registry = registry || /* istanbul ignore next */ {}
21
-
22
19
  }
23
-
20
+
24
21
  /**
25
22
  * Sets the namespace for this storage provider, and migrates
26
23
  * all stored values to the new namespace
@@ -28,7 +25,7 @@ class StorageProvider {
28
25
  */
29
26
  /* istanbul ignore next */
30
27
  setNamespace(namespace) {}
31
-
28
+
32
29
  /**
33
30
  * Registers an item with it's initial value. This is used for logging, resetting, etc.
34
31
  * @param {String} key Storage item's key
@@ -37,14 +34,14 @@ class StorageProvider {
37
34
  register(key, initialValue) {
38
35
  this.registry[key] = initialValue
39
36
  }
40
-
37
+
41
38
  /**
42
39
  * Reads an item from storage
43
40
  * @param {String} key Key for the item to retrieve
44
41
  */
45
42
  /* istanbul ignore next */
46
43
  getItem(key) {}
47
-
44
+
48
45
  /**
49
46
  * Stores a value
50
47
  * @param {String} key Item's storage key
@@ -52,7 +49,7 @@ class StorageProvider {
52
49
  */
53
50
  /* istanbul ignore next */
54
51
  setItem(key, value) {}
55
-
52
+
56
53
  /**
57
54
  * Removes an item from storage
58
55
  * @param {String} key Item's storage key
@@ -60,27 +57,23 @@ class StorageProvider {
60
57
  */
61
58
  /* istanbul ignore next */
62
59
  removeItem(key, fromRegistry = false) {}
63
-
60
+
64
61
  /**
65
62
  * Gets all stored keys & values
66
63
  * If a `namespace` was set, only keys prefixed with the namespace will be returned
67
64
  */
68
65
  /* istanbul ignore next */
69
66
  getAll() {}
70
-
67
+
71
68
  /**
72
- *
69
+ *
73
70
  * @param {Boolean} useInitialValues If values should be replaced with their initial values. If false, keys are removed
74
71
  * @param {String[]} excludedKeys (Optional) List of keys to exclude
75
72
  * @param {Boolean} clearRegistry (Optional) If the registry should also be cleared
76
73
  */
77
74
  /* istanbul ignore next */
78
- _resetAll(
79
- useInitialValues = true,
80
- excludedKeys = [],
81
- clearRegistry = false
82
- ) {}
83
-
75
+ _resetAll(useInitialValues = true, excludedKeys = [], clearRegistry = false) {}
76
+
84
77
  /**
85
78
  * Resets all values to their initial values
86
79
  * If a `namespace` is set, only keys prefixed with the namespace will be reset
@@ -88,7 +81,7 @@ class StorageProvider {
88
81
  */
89
82
  /* istanbul ignore next */
90
83
  resetAll(excludedKeys = []) {}
91
-
84
+
92
85
  /**
93
86
  * Removes all items from local storage.
94
87
  * If a `namespace` is set, only keys prefixed with the namespace will be removed
@@ -96,7 +89,6 @@ class StorageProvider {
96
89
  */
97
90
  /* istanbul ignore next */
98
91
  removeAll(excludedKeys = []) {}
99
-
100
92
  }
101
93
 
102
94
  export default StorageProvider