@trackunit/react-components 3.5.3 → 3.5.4-alpha-cb9408e01a9.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/index.cjs.js +99 -26
- package/index.esm.js +99 -26
- package/package.json +6 -6
- package/src/hooks/persistence/usePersistedState.d.ts +13 -0
package/index.cjs.js
CHANGED
|
@@ -14126,6 +14126,12 @@ const rawLocalStorage = {
|
|
|
14126
14126
|
* `toUrlValue` + encoding). Duplicate writes where the state has not
|
|
14127
14127
|
* changed (deep equality) are skipped.
|
|
14128
14128
|
*
|
|
14129
|
+
* `commitHydratedState` writes the hydrated value to localStorage only,
|
|
14130
|
+
* when it came from the URL and differs from what this identity has stored.
|
|
14131
|
+
* It does not update the URL. It returns `false` while the snapshot is
|
|
14132
|
+
* still for a previous read, so the caller can retry after identity
|
|
14133
|
+
* resolution. The caller decides which identity is allowed to commit.
|
|
14134
|
+
*
|
|
14129
14135
|
* `clearState` removes the entry from localStorage and from the URL slot,
|
|
14130
14136
|
* rather than writing an empty value. Use it when the state a consumer
|
|
14131
14137
|
* derives has become empty, so no leftover entry is stored for a value the
|
|
@@ -14216,7 +14222,11 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14216
14222
|
try {
|
|
14217
14223
|
const { value } = unwrapUrlOwner(decode(raw));
|
|
14218
14224
|
const transformed = transformFromUrlValue ? transformFromUrlValue(value) : value;
|
|
14219
|
-
|
|
14225
|
+
const state = validateState(transformed);
|
|
14226
|
+
if (state === undefined) {
|
|
14227
|
+
return undefined;
|
|
14228
|
+
}
|
|
14229
|
+
return { state, payload: value };
|
|
14220
14230
|
}
|
|
14221
14231
|
catch {
|
|
14222
14232
|
return undefined;
|
|
@@ -14238,17 +14248,21 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14238
14248
|
}
|
|
14239
14249
|
};
|
|
14240
14250
|
let urlState;
|
|
14251
|
+
let decodedPayload;
|
|
14241
14252
|
let canonicalUrlValid = false;
|
|
14242
14253
|
let usedLegacyUrl = false;
|
|
14243
14254
|
if (enabled && valueFromUrl) {
|
|
14244
|
-
|
|
14245
|
-
if (
|
|
14255
|
+
const decoded = tryDecode(valueFromUrl);
|
|
14256
|
+
if (decoded !== undefined) {
|
|
14257
|
+
urlState = decoded.state;
|
|
14258
|
+
decodedPayload = decoded.payload;
|
|
14246
14259
|
canonicalUrlValid = true;
|
|
14247
14260
|
}
|
|
14248
14261
|
}
|
|
14249
14262
|
if (urlState === undefined && enabled && legacyValueFromUrl) {
|
|
14250
|
-
|
|
14251
|
-
if (
|
|
14263
|
+
const decoded = tryDecode(legacyValueFromUrl);
|
|
14264
|
+
if (decoded !== undefined) {
|
|
14265
|
+
urlState = decoded.state;
|
|
14252
14266
|
usedLegacyUrl = true;
|
|
14253
14267
|
}
|
|
14254
14268
|
}
|
|
@@ -14258,6 +14272,7 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14258
14272
|
return {
|
|
14259
14273
|
initialState: urlState,
|
|
14260
14274
|
canonicalUrlHasValidState: canonicalUrlValid,
|
|
14275
|
+
canonicalUrlPayload: decodedPayload,
|
|
14261
14276
|
needsUrlRestore: usedLegacyUrl,
|
|
14262
14277
|
};
|
|
14263
14278
|
}
|
|
@@ -14268,6 +14283,7 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14268
14283
|
return {
|
|
14269
14284
|
initialState: { ...storageState, ...urlState },
|
|
14270
14285
|
canonicalUrlHasValidState: canonicalUrlValid,
|
|
14286
|
+
canonicalUrlPayload: decodedPayload,
|
|
14271
14287
|
needsUrlRestore: usedLegacyUrl,
|
|
14272
14288
|
};
|
|
14273
14289
|
}
|
|
@@ -14275,6 +14291,7 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14275
14291
|
return {
|
|
14276
14292
|
initialState: urlState,
|
|
14277
14293
|
canonicalUrlHasValidState: canonicalUrlValid,
|
|
14294
|
+
canonicalUrlPayload: decodedPayload,
|
|
14278
14295
|
needsUrlRestore: usedLegacyUrl,
|
|
14279
14296
|
};
|
|
14280
14297
|
}
|
|
@@ -14282,12 +14299,14 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14282
14299
|
return {
|
|
14283
14300
|
initialState: storageState,
|
|
14284
14301
|
canonicalUrlHasValidState: false,
|
|
14302
|
+
canonicalUrlPayload: undefined,
|
|
14285
14303
|
needsUrlRestore: true,
|
|
14286
14304
|
};
|
|
14287
14305
|
}
|
|
14288
14306
|
return {
|
|
14289
14307
|
initialState: undefined,
|
|
14290
14308
|
canonicalUrlHasValidState: false,
|
|
14309
|
+
canonicalUrlPayload: undefined,
|
|
14291
14310
|
needsUrlRestore: false,
|
|
14292
14311
|
};
|
|
14293
14312
|
}, [decode, enabled, localStorageEnabled, storageKey, mergeWithStorageOnRead]);
|
|
@@ -14297,27 +14316,33 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14297
14316
|
const urlValue = toUrlValueRef.current ? toUrlValueRef.current(state) : state;
|
|
14298
14317
|
return encode(currentUrlOwner === undefined ? urlValue : wrapUrlOwner(urlValue, currentUrlOwner));
|
|
14299
14318
|
}, [encode, currentUrlOwner]);
|
|
14300
|
-
const [persistedStateSnapshot, setPersistedStateSnapshot] = react.useState(() => readPersistedState({
|
|
14301
|
-
valueFromUrl: searchValue,
|
|
14302
|
-
legacyValueFromUrl: legacySearchValue,
|
|
14303
|
-
validateState: validate,
|
|
14304
|
-
transformFromUrlValue: fromUrlValue,
|
|
14305
|
-
}));
|
|
14306
14319
|
const persistedStateReadKey = `${enabled}:${localStorageEnabled}:${mergeWithStorageOnRead}:${urlLocation}:${storageKey}`;
|
|
14320
|
+
const [persistedStateSnapshot, setPersistedStateSnapshot] = react.useState(() => ({
|
|
14321
|
+
...readPersistedState({
|
|
14322
|
+
valueFromUrl: searchValue,
|
|
14323
|
+
legacyValueFromUrl: legacySearchValue,
|
|
14324
|
+
validateState: validate,
|
|
14325
|
+
transformFromUrlValue: fromUrlValue,
|
|
14326
|
+
}),
|
|
14327
|
+
readKey: persistedStateReadKey,
|
|
14328
|
+
}));
|
|
14307
14329
|
const persistedStateReadKeyRef = react.useRef(persistedStateReadKey);
|
|
14308
14330
|
react.useEffect(() => {
|
|
14309
14331
|
if (persistedStateReadKeyRef.current === persistedStateReadKey) {
|
|
14310
14332
|
return;
|
|
14311
14333
|
}
|
|
14312
14334
|
persistedStateReadKeyRef.current = persistedStateReadKey;
|
|
14313
|
-
setPersistedStateSnapshot(
|
|
14314
|
-
|
|
14315
|
-
|
|
14316
|
-
|
|
14317
|
-
|
|
14318
|
-
|
|
14335
|
+
setPersistedStateSnapshot({
|
|
14336
|
+
...readPersistedState({
|
|
14337
|
+
valueFromUrl: searchValueRef.current,
|
|
14338
|
+
legacyValueFromUrl: legacySearchValueRef.current,
|
|
14339
|
+
validateState: validateRef.current,
|
|
14340
|
+
transformFromUrlValue: fromUrlValueRef.current,
|
|
14341
|
+
}),
|
|
14342
|
+
readKey: persistedStateReadKey,
|
|
14343
|
+
});
|
|
14319
14344
|
}, [persistedStateReadKey, readPersistedState]);
|
|
14320
|
-
const { initialState, canonicalUrlHasValidState, needsUrlRestore } = persistedStateSnapshot;
|
|
14345
|
+
const { initialState, canonicalUrlHasValidState, canonicalUrlPayload, needsUrlRestore } = persistedStateSnapshot;
|
|
14321
14346
|
// Late-hydration recovery: TanStack Router's `useSearch` can return an
|
|
14322
14347
|
// empty object on the very first render of a route that's still in a
|
|
14323
14348
|
// loader-driven pending phase. The `useState` initializer above runs
|
|
@@ -14338,13 +14363,16 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14338
14363
|
if (legacySearchValue === undefined) {
|
|
14339
14364
|
return;
|
|
14340
14365
|
}
|
|
14341
|
-
setPersistedStateSnapshot(
|
|
14342
|
-
|
|
14343
|
-
|
|
14344
|
-
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
|
|
14366
|
+
setPersistedStateSnapshot({
|
|
14367
|
+
...readPersistedState({
|
|
14368
|
+
valueFromUrl: searchValueRef.current,
|
|
14369
|
+
legacyValueFromUrl: legacySearchValue,
|
|
14370
|
+
validateState: validateRef.current,
|
|
14371
|
+
transformFromUrlValue: fromUrlValueRef.current,
|
|
14372
|
+
}),
|
|
14373
|
+
readKey: persistedStateReadKey,
|
|
14374
|
+
});
|
|
14375
|
+
}, [legacySearchValue, initialState, canonicalUrlHasValidState, persistedStateReadKey, readPersistedState]);
|
|
14348
14376
|
const lastPersistedRef = react.useRef(initialState);
|
|
14349
14377
|
react.useEffect(() => {
|
|
14350
14378
|
lastPersistedRef.current = initialState;
|
|
@@ -14414,7 +14442,52 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14414
14442
|
}
|
|
14415
14443
|
updateSearchParam(undefined);
|
|
14416
14444
|
}, [localStorageEnabled, storageKey, updateSearchParam]);
|
|
14417
|
-
|
|
14445
|
+
const snapshotIsCurrent = persistedStateSnapshot.readKey === persistedStateReadKey;
|
|
14446
|
+
// Opening a shared URL should become this identity's saved layout. The
|
|
14447
|
+
// caller decides which identity is allowed to commit: this only writes
|
|
14448
|
+
// when the snapshot was read for the current storage key, so a late
|
|
14449
|
+
// identity resolution cannot persist the pre-merge value and drop fields
|
|
14450
|
+
// that live only in localStorage. A hash that is only the shareable
|
|
14451
|
+
// projection of what is already stored is left alone, so reloading this
|
|
14452
|
+
// account's own link does not replace hidden columns with the shorter URL form.
|
|
14453
|
+
const commitHydratedState = react.useCallback(() => {
|
|
14454
|
+
if (!snapshotIsCurrent || !localStorageEnabled) {
|
|
14455
|
+
return false;
|
|
14456
|
+
}
|
|
14457
|
+
if (!canonicalUrlHasValidState || initialState === undefined) {
|
|
14458
|
+
return true;
|
|
14459
|
+
}
|
|
14460
|
+
let stored;
|
|
14461
|
+
try {
|
|
14462
|
+
const raw = rawLocalStorage.read(storageKey);
|
|
14463
|
+
if (raw) {
|
|
14464
|
+
stored = validateRef.current(sharedUtils.storageSerializer.deserialize(raw));
|
|
14465
|
+
}
|
|
14466
|
+
}
|
|
14467
|
+
catch {
|
|
14468
|
+
stored = undefined;
|
|
14469
|
+
}
|
|
14470
|
+
if (stored !== undefined) {
|
|
14471
|
+
const projected = toUrlValueRef.current ? toUrlValueRef.current(stored) : stored;
|
|
14472
|
+
if (dequal.dequal(projected, canonicalUrlPayload)) {
|
|
14473
|
+
return true;
|
|
14474
|
+
}
|
|
14475
|
+
}
|
|
14476
|
+
const serialized = serializeRef.current
|
|
14477
|
+
? serializeRef.current(initialState)
|
|
14478
|
+
: sharedUtils.storageSerializer.serialize(initialState);
|
|
14479
|
+
rawLocalStorage.write(storageKey, serialized);
|
|
14480
|
+
lastPersistedRef.current = initialState;
|
|
14481
|
+
return true;
|
|
14482
|
+
}, [
|
|
14483
|
+
canonicalUrlHasValidState,
|
|
14484
|
+
canonicalUrlPayload,
|
|
14485
|
+
initialState,
|
|
14486
|
+
localStorageEnabled,
|
|
14487
|
+
snapshotIsCurrent,
|
|
14488
|
+
storageKey,
|
|
14489
|
+
]);
|
|
14490
|
+
return react.useMemo(() => ({ initialState, persistState, clearState, commitHydratedState }), [initialState, persistState, clearState, commitHydratedState]);
|
|
14418
14491
|
};
|
|
14419
14492
|
|
|
14420
14493
|
/**
|
package/index.esm.js
CHANGED
|
@@ -14125,6 +14125,12 @@ const rawLocalStorage = {
|
|
|
14125
14125
|
* `toUrlValue` + encoding). Duplicate writes where the state has not
|
|
14126
14126
|
* changed (deep equality) are skipped.
|
|
14127
14127
|
*
|
|
14128
|
+
* `commitHydratedState` writes the hydrated value to localStorage only,
|
|
14129
|
+
* when it came from the URL and differs from what this identity has stored.
|
|
14130
|
+
* It does not update the URL. It returns `false` while the snapshot is
|
|
14131
|
+
* still for a previous read, so the caller can retry after identity
|
|
14132
|
+
* resolution. The caller decides which identity is allowed to commit.
|
|
14133
|
+
*
|
|
14128
14134
|
* `clearState` removes the entry from localStorage and from the URL slot,
|
|
14129
14135
|
* rather than writing an empty value. Use it when the state a consumer
|
|
14130
14136
|
* derives has become empty, so no leftover entry is stored for a value the
|
|
@@ -14215,7 +14221,11 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14215
14221
|
try {
|
|
14216
14222
|
const { value } = unwrapUrlOwner(decode(raw));
|
|
14217
14223
|
const transformed = transformFromUrlValue ? transformFromUrlValue(value) : value;
|
|
14218
|
-
|
|
14224
|
+
const state = validateState(transformed);
|
|
14225
|
+
if (state === undefined) {
|
|
14226
|
+
return undefined;
|
|
14227
|
+
}
|
|
14228
|
+
return { state, payload: value };
|
|
14219
14229
|
}
|
|
14220
14230
|
catch {
|
|
14221
14231
|
return undefined;
|
|
@@ -14237,17 +14247,21 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14237
14247
|
}
|
|
14238
14248
|
};
|
|
14239
14249
|
let urlState;
|
|
14250
|
+
let decodedPayload;
|
|
14240
14251
|
let canonicalUrlValid = false;
|
|
14241
14252
|
let usedLegacyUrl = false;
|
|
14242
14253
|
if (enabled && valueFromUrl) {
|
|
14243
|
-
|
|
14244
|
-
if (
|
|
14254
|
+
const decoded = tryDecode(valueFromUrl);
|
|
14255
|
+
if (decoded !== undefined) {
|
|
14256
|
+
urlState = decoded.state;
|
|
14257
|
+
decodedPayload = decoded.payload;
|
|
14245
14258
|
canonicalUrlValid = true;
|
|
14246
14259
|
}
|
|
14247
14260
|
}
|
|
14248
14261
|
if (urlState === undefined && enabled && legacyValueFromUrl) {
|
|
14249
|
-
|
|
14250
|
-
if (
|
|
14262
|
+
const decoded = tryDecode(legacyValueFromUrl);
|
|
14263
|
+
if (decoded !== undefined) {
|
|
14264
|
+
urlState = decoded.state;
|
|
14251
14265
|
usedLegacyUrl = true;
|
|
14252
14266
|
}
|
|
14253
14267
|
}
|
|
@@ -14257,6 +14271,7 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14257
14271
|
return {
|
|
14258
14272
|
initialState: urlState,
|
|
14259
14273
|
canonicalUrlHasValidState: canonicalUrlValid,
|
|
14274
|
+
canonicalUrlPayload: decodedPayload,
|
|
14260
14275
|
needsUrlRestore: usedLegacyUrl,
|
|
14261
14276
|
};
|
|
14262
14277
|
}
|
|
@@ -14267,6 +14282,7 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14267
14282
|
return {
|
|
14268
14283
|
initialState: { ...storageState, ...urlState },
|
|
14269
14284
|
canonicalUrlHasValidState: canonicalUrlValid,
|
|
14285
|
+
canonicalUrlPayload: decodedPayload,
|
|
14270
14286
|
needsUrlRestore: usedLegacyUrl,
|
|
14271
14287
|
};
|
|
14272
14288
|
}
|
|
@@ -14274,6 +14290,7 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14274
14290
|
return {
|
|
14275
14291
|
initialState: urlState,
|
|
14276
14292
|
canonicalUrlHasValidState: canonicalUrlValid,
|
|
14293
|
+
canonicalUrlPayload: decodedPayload,
|
|
14277
14294
|
needsUrlRestore: usedLegacyUrl,
|
|
14278
14295
|
};
|
|
14279
14296
|
}
|
|
@@ -14281,12 +14298,14 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14281
14298
|
return {
|
|
14282
14299
|
initialState: storageState,
|
|
14283
14300
|
canonicalUrlHasValidState: false,
|
|
14301
|
+
canonicalUrlPayload: undefined,
|
|
14284
14302
|
needsUrlRestore: true,
|
|
14285
14303
|
};
|
|
14286
14304
|
}
|
|
14287
14305
|
return {
|
|
14288
14306
|
initialState: undefined,
|
|
14289
14307
|
canonicalUrlHasValidState: false,
|
|
14308
|
+
canonicalUrlPayload: undefined,
|
|
14290
14309
|
needsUrlRestore: false,
|
|
14291
14310
|
};
|
|
14292
14311
|
}, [decode, enabled, localStorageEnabled, storageKey, mergeWithStorageOnRead]);
|
|
@@ -14296,27 +14315,33 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14296
14315
|
const urlValue = toUrlValueRef.current ? toUrlValueRef.current(state) : state;
|
|
14297
14316
|
return encode(currentUrlOwner === undefined ? urlValue : wrapUrlOwner(urlValue, currentUrlOwner));
|
|
14298
14317
|
}, [encode, currentUrlOwner]);
|
|
14299
|
-
const [persistedStateSnapshot, setPersistedStateSnapshot] = useState(() => readPersistedState({
|
|
14300
|
-
valueFromUrl: searchValue,
|
|
14301
|
-
legacyValueFromUrl: legacySearchValue,
|
|
14302
|
-
validateState: validate,
|
|
14303
|
-
transformFromUrlValue: fromUrlValue,
|
|
14304
|
-
}));
|
|
14305
14318
|
const persistedStateReadKey = `${enabled}:${localStorageEnabled}:${mergeWithStorageOnRead}:${urlLocation}:${storageKey}`;
|
|
14319
|
+
const [persistedStateSnapshot, setPersistedStateSnapshot] = useState(() => ({
|
|
14320
|
+
...readPersistedState({
|
|
14321
|
+
valueFromUrl: searchValue,
|
|
14322
|
+
legacyValueFromUrl: legacySearchValue,
|
|
14323
|
+
validateState: validate,
|
|
14324
|
+
transformFromUrlValue: fromUrlValue,
|
|
14325
|
+
}),
|
|
14326
|
+
readKey: persistedStateReadKey,
|
|
14327
|
+
}));
|
|
14306
14328
|
const persistedStateReadKeyRef = useRef(persistedStateReadKey);
|
|
14307
14329
|
useEffect(() => {
|
|
14308
14330
|
if (persistedStateReadKeyRef.current === persistedStateReadKey) {
|
|
14309
14331
|
return;
|
|
14310
14332
|
}
|
|
14311
14333
|
persistedStateReadKeyRef.current = persistedStateReadKey;
|
|
14312
|
-
setPersistedStateSnapshot(
|
|
14313
|
-
|
|
14314
|
-
|
|
14315
|
-
|
|
14316
|
-
|
|
14317
|
-
|
|
14334
|
+
setPersistedStateSnapshot({
|
|
14335
|
+
...readPersistedState({
|
|
14336
|
+
valueFromUrl: searchValueRef.current,
|
|
14337
|
+
legacyValueFromUrl: legacySearchValueRef.current,
|
|
14338
|
+
validateState: validateRef.current,
|
|
14339
|
+
transformFromUrlValue: fromUrlValueRef.current,
|
|
14340
|
+
}),
|
|
14341
|
+
readKey: persistedStateReadKey,
|
|
14342
|
+
});
|
|
14318
14343
|
}, [persistedStateReadKey, readPersistedState]);
|
|
14319
|
-
const { initialState, canonicalUrlHasValidState, needsUrlRestore } = persistedStateSnapshot;
|
|
14344
|
+
const { initialState, canonicalUrlHasValidState, canonicalUrlPayload, needsUrlRestore } = persistedStateSnapshot;
|
|
14320
14345
|
// Late-hydration recovery: TanStack Router's `useSearch` can return an
|
|
14321
14346
|
// empty object on the very first render of a route that's still in a
|
|
14322
14347
|
// loader-driven pending phase. The `useState` initializer above runs
|
|
@@ -14337,13 +14362,16 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14337
14362
|
if (legacySearchValue === undefined) {
|
|
14338
14363
|
return;
|
|
14339
14364
|
}
|
|
14340
|
-
setPersistedStateSnapshot(
|
|
14341
|
-
|
|
14342
|
-
|
|
14343
|
-
|
|
14344
|
-
|
|
14345
|
-
|
|
14346
|
-
|
|
14365
|
+
setPersistedStateSnapshot({
|
|
14366
|
+
...readPersistedState({
|
|
14367
|
+
valueFromUrl: searchValueRef.current,
|
|
14368
|
+
legacyValueFromUrl: legacySearchValue,
|
|
14369
|
+
validateState: validateRef.current,
|
|
14370
|
+
transformFromUrlValue: fromUrlValueRef.current,
|
|
14371
|
+
}),
|
|
14372
|
+
readKey: persistedStateReadKey,
|
|
14373
|
+
});
|
|
14374
|
+
}, [legacySearchValue, initialState, canonicalUrlHasValidState, persistedStateReadKey, readPersistedState]);
|
|
14347
14375
|
const lastPersistedRef = useRef(initialState);
|
|
14348
14376
|
useEffect(() => {
|
|
14349
14377
|
lastPersistedRef.current = initialState;
|
|
@@ -14413,7 +14441,52 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
14413
14441
|
}
|
|
14414
14442
|
updateSearchParam(undefined);
|
|
14415
14443
|
}, [localStorageEnabled, storageKey, updateSearchParam]);
|
|
14416
|
-
|
|
14444
|
+
const snapshotIsCurrent = persistedStateSnapshot.readKey === persistedStateReadKey;
|
|
14445
|
+
// Opening a shared URL should become this identity's saved layout. The
|
|
14446
|
+
// caller decides which identity is allowed to commit: this only writes
|
|
14447
|
+
// when the snapshot was read for the current storage key, so a late
|
|
14448
|
+
// identity resolution cannot persist the pre-merge value and drop fields
|
|
14449
|
+
// that live only in localStorage. A hash that is only the shareable
|
|
14450
|
+
// projection of what is already stored is left alone, so reloading this
|
|
14451
|
+
// account's own link does not replace hidden columns with the shorter URL form.
|
|
14452
|
+
const commitHydratedState = useCallback(() => {
|
|
14453
|
+
if (!snapshotIsCurrent || !localStorageEnabled) {
|
|
14454
|
+
return false;
|
|
14455
|
+
}
|
|
14456
|
+
if (!canonicalUrlHasValidState || initialState === undefined) {
|
|
14457
|
+
return true;
|
|
14458
|
+
}
|
|
14459
|
+
let stored;
|
|
14460
|
+
try {
|
|
14461
|
+
const raw = rawLocalStorage.read(storageKey);
|
|
14462
|
+
if (raw) {
|
|
14463
|
+
stored = validateRef.current(storageSerializer.deserialize(raw));
|
|
14464
|
+
}
|
|
14465
|
+
}
|
|
14466
|
+
catch {
|
|
14467
|
+
stored = undefined;
|
|
14468
|
+
}
|
|
14469
|
+
if (stored !== undefined) {
|
|
14470
|
+
const projected = toUrlValueRef.current ? toUrlValueRef.current(stored) : stored;
|
|
14471
|
+
if (dequal(projected, canonicalUrlPayload)) {
|
|
14472
|
+
return true;
|
|
14473
|
+
}
|
|
14474
|
+
}
|
|
14475
|
+
const serialized = serializeRef.current
|
|
14476
|
+
? serializeRef.current(initialState)
|
|
14477
|
+
: storageSerializer.serialize(initialState);
|
|
14478
|
+
rawLocalStorage.write(storageKey, serialized);
|
|
14479
|
+
lastPersistedRef.current = initialState;
|
|
14480
|
+
return true;
|
|
14481
|
+
}, [
|
|
14482
|
+
canonicalUrlHasValidState,
|
|
14483
|
+
canonicalUrlPayload,
|
|
14484
|
+
initialState,
|
|
14485
|
+
localStorageEnabled,
|
|
14486
|
+
snapshotIsCurrent,
|
|
14487
|
+
storageKey,
|
|
14488
|
+
]);
|
|
14489
|
+
return useMemo(() => ({ initialState, persistState, clearState, commitHydratedState }), [initialState, persistState, clearState, commitHydratedState]);
|
|
14417
14490
|
};
|
|
14418
14491
|
|
|
14419
14492
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-components",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.4-alpha-cb9408e01a9.0",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"migrations": "./migrations.json",
|
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
"@floating-ui/react": "^0.26.25",
|
|
15
15
|
"string-ts": "^2.0.0",
|
|
16
16
|
"tailwind-merge": "^2.0.0",
|
|
17
|
-
"@trackunit/ui-design-tokens": "1.17.
|
|
18
|
-
"@trackunit/css-class-variance-utilities": "2.2.
|
|
19
|
-
"@trackunit/shared-utils": "1.18.
|
|
20
|
-
"@trackunit/ui-icons": "1.16.
|
|
17
|
+
"@trackunit/ui-design-tokens": "1.17.3-alpha-cb9408e01a9.0",
|
|
18
|
+
"@trackunit/css-class-variance-utilities": "2.2.3-alpha-cb9408e01a9.0",
|
|
19
|
+
"@trackunit/shared-utils": "1.18.3-alpha-cb9408e01a9.0",
|
|
20
|
+
"@trackunit/ui-icons": "1.16.3-alpha-cb9408e01a9.0",
|
|
21
21
|
"es-toolkit": "^1.52.0",
|
|
22
22
|
"@tanstack/react-virtual": "^3.14.13",
|
|
23
23
|
"dequal": "^2.0.3",
|
|
24
24
|
"fflate": "^0.8.3",
|
|
25
25
|
"zod": "^3.25.76",
|
|
26
|
-
"@trackunit/i18n-library-translation": "2.7.
|
|
26
|
+
"@trackunit/i18n-library-translation": "2.7.3-alpha-cb9408e01a9.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"react": "^19.0.0",
|
|
@@ -58,6 +58,13 @@ type UsePersistedStateReturn<TState> = {
|
|
|
58
58
|
readonly initialState: TState | undefined;
|
|
59
59
|
readonly persistState: (state: TState) => void;
|
|
60
60
|
readonly clearState: () => void;
|
|
61
|
+
/**
|
|
62
|
+
* Writes the hydrated state to localStorage when it came from the URL and
|
|
63
|
+
* differs from what this identity has stored. Returns `false` when the
|
|
64
|
+
* snapshot is still for a previous read (identity not resolved yet), so
|
|
65
|
+
* the caller can retry. A no-op success returns `true`.
|
|
66
|
+
*/
|
|
67
|
+
readonly commitHydratedState: () => boolean;
|
|
61
68
|
};
|
|
62
69
|
/**
|
|
63
70
|
* Generic persistence hook that loads state from a URL slot (search param
|
|
@@ -82,6 +89,12 @@ type UsePersistedStateReturn<TState> = {
|
|
|
82
89
|
* `toUrlValue` + encoding). Duplicate writes where the state has not
|
|
83
90
|
* changed (deep equality) are skipped.
|
|
84
91
|
*
|
|
92
|
+
* `commitHydratedState` writes the hydrated value to localStorage only,
|
|
93
|
+
* when it came from the URL and differs from what this identity has stored.
|
|
94
|
+
* It does not update the URL. It returns `false` while the snapshot is
|
|
95
|
+
* still for a previous read, so the caller can retry after identity
|
|
96
|
+
* resolution. The caller decides which identity is allowed to commit.
|
|
97
|
+
*
|
|
85
98
|
* `clearState` removes the entry from localStorage and from the URL slot,
|
|
86
99
|
* rather than writing an empty value. Use it when the state a consumer
|
|
87
100
|
* derives has become empty, so no leftover entry is stored for a value the
|