@trackunit/react-components 1.24.4 → 1.24.6
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
CHANGED
|
@@ -11133,6 +11133,13 @@ const useSessionStorage = (options) => useWebStorage(globalThis.sessionStorage,
|
|
|
11133
11133
|
*/
|
|
11134
11134
|
const useSessionStorageReducer = (options) => useWebStorageReducer(globalThis.sessionStorage, options);
|
|
11135
11135
|
|
|
11136
|
+
/**
|
|
11137
|
+
* Default maximum full URL length after adding one persisted search param
|
|
11138
|
+
* before the URL write is skipped (localStorage still persists the full state).
|
|
11139
|
+
*
|
|
11140
|
+
* Sized to stay safely below the AWS WAF managed-rule default
|
|
11141
|
+
* `SizeRestrictions_QUERYSTRING` cap of 5000 bytes for the entire url host + path + query + hash string
|
|
11142
|
+
*/
|
|
11136
11143
|
const MAX_URL_LENGTH = 5000;
|
|
11137
11144
|
/**
|
|
11138
11145
|
* Syncs an encoded string value with a URL search parameter via Tanstack Router.
|
|
@@ -11227,7 +11234,16 @@ const useSearchParamSync = ({ key, enabled = true, onExternalChange, replace: re
|
|
|
11227
11234
|
replace: shouldReplace,
|
|
11228
11235
|
});
|
|
11229
11236
|
});
|
|
11230
|
-
}, [
|
|
11237
|
+
}, [
|
|
11238
|
+
enabled,
|
|
11239
|
+
navigate,
|
|
11240
|
+
key,
|
|
11241
|
+
replaceOption,
|
|
11242
|
+
location.hash,
|
|
11243
|
+
getUrlLengthWithSearchParam,
|
|
11244
|
+
currentSearchValue,
|
|
11245
|
+
router.state.status,
|
|
11246
|
+
]);
|
|
11231
11247
|
return react.useMemo(() => ({ searchValue: currentSearchValue, updateSearchParam }), [currentSearchValue, updateSearchParam]);
|
|
11232
11248
|
};
|
|
11233
11249
|
|
|
@@ -11249,9 +11265,13 @@ const useStorageKey = (key, userId) => {
|
|
|
11249
11265
|
* Generic persistence hook that loads state from URL search params (with
|
|
11250
11266
|
* localStorage fallback) and writes changes to both.
|
|
11251
11267
|
*
|
|
11252
|
-
* On mount the hook
|
|
11253
|
-
*
|
|
11254
|
-
*
|
|
11268
|
+
* On mount the hook resolves the initial state as follows:
|
|
11269
|
+
* - If a URL search param exists, decode it and pass through `validate`.
|
|
11270
|
+
* - If localStorage is enabled, read and validate its value.
|
|
11271
|
+
* - When `mergeWithStorageOnRead` is `false` (default): URL state wins
|
|
11272
|
+
* entirely when present, otherwise fall back to localStorage.
|
|
11273
|
+
* - When `mergeWithStorageOnRead` is `true`: shallow-merge URL fields over
|
|
11274
|
+
* localStorage so fields stripped by `toUrlValue` survive via storage.
|
|
11255
11275
|
*
|
|
11256
11276
|
* `persistState` writes the state to localStorage (via `serialize`, default
|
|
11257
11277
|
* `JSON.stringify`) and syncs to the URL (via `toUrlValue` + encoding).
|
|
@@ -11264,11 +11284,15 @@ const useStorageKey = (key, userId) => {
|
|
|
11264
11284
|
* @param options.toUrlValue - Transform applied before URL encoding (default: encode state as-is).
|
|
11265
11285
|
* @param options.fromUrlValue - Transform applied after URL decoding, before validation (default: identity).
|
|
11266
11286
|
* @param options.enabled - When `false` the URL is neither read nor written (default `true`).
|
|
11287
|
+
* @param options.localStorageEnabled - When `false` localStorage is neither read nor written (default `true`).
|
|
11267
11288
|
* @param options.onExternalChange - Fired when the URL param changes externally (e.g. browser back).
|
|
11268
11289
|
* @param options.replace - Forwarded to `useSearchParamSync`.
|
|
11269
11290
|
* @param options.clientSideUserId - The user ID to use for the localStorage key.
|
|
11291
|
+
* @param options.mergeWithStorageOnRead - Merge URL state on top of localStorage
|
|
11292
|
+
* state on read (URL wins on shared keys). Use when `toUrlValue` deliberately
|
|
11293
|
+
* omits fields so they still survive via localStorage. Default `false`.
|
|
11270
11294
|
*/
|
|
11271
|
-
const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue, enabled = true, onExternalChange, replace, clientSideUserId, }) => {
|
|
11295
|
+
const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue, enabled = true, localStorageEnabled = true, onExternalChange, replace, clientSideUserId, mergeWithStorageOnRead = false, }) => {
|
|
11272
11296
|
const { encode, decode } = useCustomEncoding();
|
|
11273
11297
|
const { searchValue, updateSearchParam } = useSearchParamSync({
|
|
11274
11298
|
key,
|
|
@@ -11277,51 +11301,96 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
11277
11301
|
replace,
|
|
11278
11302
|
});
|
|
11279
11303
|
const storageKey = useStorageKey(key, clientSideUserId);
|
|
11280
|
-
const validateRef = react.useRef(validate);
|
|
11281
|
-
react.useEffect(() => {
|
|
11282
|
-
validateRef.current = validate;
|
|
11283
|
-
}, [validate]);
|
|
11284
11304
|
const toUrlValueRef = react.useRef(toUrlValue);
|
|
11285
11305
|
react.useEffect(() => {
|
|
11286
11306
|
toUrlValueRef.current = toUrlValue;
|
|
11287
11307
|
}, [toUrlValue]);
|
|
11308
|
+
const serializeRef = react.useRef(serialize);
|
|
11309
|
+
react.useEffect(() => {
|
|
11310
|
+
serializeRef.current = serialize;
|
|
11311
|
+
}, [serialize]);
|
|
11312
|
+
const validateRef = react.useRef(validate);
|
|
11313
|
+
react.useEffect(() => {
|
|
11314
|
+
validateRef.current = validate;
|
|
11315
|
+
}, [validate]);
|
|
11288
11316
|
const fromUrlValueRef = react.useRef(fromUrlValue);
|
|
11289
11317
|
react.useEffect(() => {
|
|
11290
11318
|
fromUrlValueRef.current = fromUrlValue;
|
|
11291
11319
|
}, [fromUrlValue]);
|
|
11292
|
-
const
|
|
11320
|
+
const searchValueRef = react.useRef(searchValue);
|
|
11293
11321
|
react.useEffect(() => {
|
|
11294
|
-
|
|
11295
|
-
}, [
|
|
11296
|
-
const
|
|
11297
|
-
|
|
11322
|
+
searchValueRef.current = searchValue;
|
|
11323
|
+
}, [searchValue]);
|
|
11324
|
+
const readPersistedState = react.useCallback(({ valueFromUrl, validateState, transformFromUrlValue, }) => {
|
|
11325
|
+
let urlState;
|
|
11326
|
+
if (enabled && valueFromUrl) {
|
|
11298
11327
|
try {
|
|
11299
|
-
const decoded = decode(
|
|
11300
|
-
const transformed =
|
|
11301
|
-
|
|
11302
|
-
if (validated !== undefined) {
|
|
11303
|
-
return { initialState: validated, loadedFromStorage: false };
|
|
11304
|
-
}
|
|
11328
|
+
const decoded = decode(valueFromUrl);
|
|
11329
|
+
const transformed = transformFromUrlValue ? transformFromUrlValue(decoded) : decoded;
|
|
11330
|
+
urlState = validateState(transformed);
|
|
11305
11331
|
}
|
|
11306
11332
|
catch {
|
|
11307
11333
|
// fall through to localStorage
|
|
11308
11334
|
}
|
|
11309
11335
|
}
|
|
11310
|
-
|
|
11311
|
-
|
|
11312
|
-
|
|
11313
|
-
|
|
11314
|
-
|
|
11315
|
-
|
|
11336
|
+
// Without the merge option, URL state wins entirely when present —
|
|
11337
|
+
// this preserves the original "URL > localStorage" priority.
|
|
11338
|
+
if (urlState !== undefined && !mergeWithStorageOnRead) {
|
|
11339
|
+
return { initialState: urlState, loadedFromStorage: false };
|
|
11340
|
+
}
|
|
11341
|
+
let storageState;
|
|
11342
|
+
if (localStorageEnabled) {
|
|
11343
|
+
try {
|
|
11344
|
+
const raw = localStorage.getItem(storageKey);
|
|
11345
|
+
if (raw) {
|
|
11346
|
+
const parsed = storageSerializer.deserialize(raw);
|
|
11347
|
+
storageState = validateState(parsed);
|
|
11348
|
+
}
|
|
11316
11349
|
}
|
|
11350
|
+
catch {
|
|
11351
|
+
// no valid stored state
|
|
11352
|
+
}
|
|
11353
|
+
}
|
|
11354
|
+
if (urlState !== undefined && storageState !== undefined) {
|
|
11355
|
+
// Shallow merge — URL fields take precedence; storage fills in
|
|
11356
|
+
// whatever the URL omits (e.g. fields stripped by `toUrlValue`).
|
|
11357
|
+
return { initialState: { ...storageState, ...urlState }, loadedFromStorage: false };
|
|
11317
11358
|
}
|
|
11318
|
-
|
|
11319
|
-
|
|
11359
|
+
if (urlState !== undefined) {
|
|
11360
|
+
return { initialState: urlState, loadedFromStorage: false };
|
|
11361
|
+
}
|
|
11362
|
+
if (storageState !== undefined) {
|
|
11363
|
+
return { initialState: storageState, loadedFromStorage: true };
|
|
11320
11364
|
}
|
|
11321
11365
|
return { initialState: undefined, loadedFromStorage: false };
|
|
11322
|
-
});
|
|
11366
|
+
}, [decode, enabled, localStorageEnabled, storageKey, mergeWithStorageOnRead]);
|
|
11367
|
+
const [persistedStateSnapshot, setPersistedStateSnapshot] = react.useState(() => readPersistedState({
|
|
11368
|
+
valueFromUrl: searchValue,
|
|
11369
|
+
validateState: validate,
|
|
11370
|
+
transformFromUrlValue: fromUrlValue,
|
|
11371
|
+
}));
|
|
11372
|
+
const persistedStateReadKey = `${enabled}:${localStorageEnabled}:${mergeWithStorageOnRead}:${storageKey}`;
|
|
11373
|
+
const persistedStateReadKeyRef = react.useRef(persistedStateReadKey);
|
|
11374
|
+
react.useEffect(() => {
|
|
11375
|
+
if (persistedStateReadKeyRef.current === persistedStateReadKey) {
|
|
11376
|
+
return;
|
|
11377
|
+
}
|
|
11378
|
+
persistedStateReadKeyRef.current = persistedStateReadKey;
|
|
11379
|
+
setPersistedStateSnapshot(readPersistedState({
|
|
11380
|
+
valueFromUrl: searchValueRef.current,
|
|
11381
|
+
validateState: validateRef.current,
|
|
11382
|
+
transformFromUrlValue: fromUrlValueRef.current,
|
|
11383
|
+
}));
|
|
11384
|
+
}, [persistedStateReadKey, readPersistedState]);
|
|
11385
|
+
const { initialState, loadedFromStorage } = persistedStateSnapshot;
|
|
11323
11386
|
const lastPersistedRef = react.useRef(initialState);
|
|
11387
|
+
react.useEffect(() => {
|
|
11388
|
+
lastPersistedRef.current = initialState;
|
|
11389
|
+
}, [initialState, localStorageEnabled, storageKey]);
|
|
11324
11390
|
const hasRestoredUrlRef = react.useRef(false);
|
|
11391
|
+
react.useEffect(() => {
|
|
11392
|
+
hasRestoredUrlRef.current = false;
|
|
11393
|
+
}, [enabled, localStorageEnabled, storageKey]);
|
|
11325
11394
|
react.useEffect(() => {
|
|
11326
11395
|
if (hasRestoredUrlRef.current) {
|
|
11327
11396
|
return;
|
|
@@ -11331,18 +11400,22 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
11331
11400
|
}
|
|
11332
11401
|
hasRestoredUrlRef.current = true;
|
|
11333
11402
|
const urlValue = toUrlValueRef.current ? toUrlValueRef.current(initialState) : initialState;
|
|
11334
|
-
|
|
11403
|
+
const encoded = encode(urlValue);
|
|
11404
|
+
updateSearchParam(encoded, { replace: true });
|
|
11335
11405
|
}, [enabled, loadedFromStorage, initialState, searchValue, updateSearchParam, encode]);
|
|
11336
11406
|
const persistState = react.useCallback((state) => {
|
|
11337
11407
|
if (dequal.dequal(lastPersistedRef.current, state)) {
|
|
11338
11408
|
return;
|
|
11339
11409
|
}
|
|
11340
11410
|
lastPersistedRef.current = state;
|
|
11341
|
-
|
|
11342
|
-
|
|
11411
|
+
if (localStorageEnabled) {
|
|
11412
|
+
const serialized = serializeRef.current ? serializeRef.current(state) : storageSerializer.serialize(state);
|
|
11413
|
+
localStorage.setItem(storageKey, serialized);
|
|
11414
|
+
}
|
|
11343
11415
|
const urlValue = toUrlValueRef.current ? toUrlValueRef.current(state) : state;
|
|
11344
|
-
|
|
11345
|
-
|
|
11416
|
+
const encoded = encode(urlValue);
|
|
11417
|
+
updateSearchParam(encoded);
|
|
11418
|
+
}, [storageKey, encode, localStorageEnabled, updateSearchParam]);
|
|
11346
11419
|
return react.useMemo(() => ({ initialState, persistState }), [initialState, persistState]);
|
|
11347
11420
|
};
|
|
11348
11421
|
|
package/index.esm.js
CHANGED
|
@@ -11131,6 +11131,13 @@ const useSessionStorage = (options) => useWebStorage(globalThis.sessionStorage,
|
|
|
11131
11131
|
*/
|
|
11132
11132
|
const useSessionStorageReducer = (options) => useWebStorageReducer(globalThis.sessionStorage, options);
|
|
11133
11133
|
|
|
11134
|
+
/**
|
|
11135
|
+
* Default maximum full URL length after adding one persisted search param
|
|
11136
|
+
* before the URL write is skipped (localStorage still persists the full state).
|
|
11137
|
+
*
|
|
11138
|
+
* Sized to stay safely below the AWS WAF managed-rule default
|
|
11139
|
+
* `SizeRestrictions_QUERYSTRING` cap of 5000 bytes for the entire url host + path + query + hash string
|
|
11140
|
+
*/
|
|
11134
11141
|
const MAX_URL_LENGTH = 5000;
|
|
11135
11142
|
/**
|
|
11136
11143
|
* Syncs an encoded string value with a URL search parameter via Tanstack Router.
|
|
@@ -11225,7 +11232,16 @@ const useSearchParamSync = ({ key, enabled = true, onExternalChange, replace: re
|
|
|
11225
11232
|
replace: shouldReplace,
|
|
11226
11233
|
});
|
|
11227
11234
|
});
|
|
11228
|
-
}, [
|
|
11235
|
+
}, [
|
|
11236
|
+
enabled,
|
|
11237
|
+
navigate,
|
|
11238
|
+
key,
|
|
11239
|
+
replaceOption,
|
|
11240
|
+
location.hash,
|
|
11241
|
+
getUrlLengthWithSearchParam,
|
|
11242
|
+
currentSearchValue,
|
|
11243
|
+
router.state.status,
|
|
11244
|
+
]);
|
|
11229
11245
|
return useMemo(() => ({ searchValue: currentSearchValue, updateSearchParam }), [currentSearchValue, updateSearchParam]);
|
|
11230
11246
|
};
|
|
11231
11247
|
|
|
@@ -11247,9 +11263,13 @@ const useStorageKey = (key, userId) => {
|
|
|
11247
11263
|
* Generic persistence hook that loads state from URL search params (with
|
|
11248
11264
|
* localStorage fallback) and writes changes to both.
|
|
11249
11265
|
*
|
|
11250
|
-
* On mount the hook
|
|
11251
|
-
*
|
|
11252
|
-
*
|
|
11266
|
+
* On mount the hook resolves the initial state as follows:
|
|
11267
|
+
* - If a URL search param exists, decode it and pass through `validate`.
|
|
11268
|
+
* - If localStorage is enabled, read and validate its value.
|
|
11269
|
+
* - When `mergeWithStorageOnRead` is `false` (default): URL state wins
|
|
11270
|
+
* entirely when present, otherwise fall back to localStorage.
|
|
11271
|
+
* - When `mergeWithStorageOnRead` is `true`: shallow-merge URL fields over
|
|
11272
|
+
* localStorage so fields stripped by `toUrlValue` survive via storage.
|
|
11253
11273
|
*
|
|
11254
11274
|
* `persistState` writes the state to localStorage (via `serialize`, default
|
|
11255
11275
|
* `JSON.stringify`) and syncs to the URL (via `toUrlValue` + encoding).
|
|
@@ -11262,11 +11282,15 @@ const useStorageKey = (key, userId) => {
|
|
|
11262
11282
|
* @param options.toUrlValue - Transform applied before URL encoding (default: encode state as-is).
|
|
11263
11283
|
* @param options.fromUrlValue - Transform applied after URL decoding, before validation (default: identity).
|
|
11264
11284
|
* @param options.enabled - When `false` the URL is neither read nor written (default `true`).
|
|
11285
|
+
* @param options.localStorageEnabled - When `false` localStorage is neither read nor written (default `true`).
|
|
11265
11286
|
* @param options.onExternalChange - Fired when the URL param changes externally (e.g. browser back).
|
|
11266
11287
|
* @param options.replace - Forwarded to `useSearchParamSync`.
|
|
11267
11288
|
* @param options.clientSideUserId - The user ID to use for the localStorage key.
|
|
11289
|
+
* @param options.mergeWithStorageOnRead - Merge URL state on top of localStorage
|
|
11290
|
+
* state on read (URL wins on shared keys). Use when `toUrlValue` deliberately
|
|
11291
|
+
* omits fields so they still survive via localStorage. Default `false`.
|
|
11268
11292
|
*/
|
|
11269
|
-
const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue, enabled = true, onExternalChange, replace, clientSideUserId, }) => {
|
|
11293
|
+
const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue, enabled = true, localStorageEnabled = true, onExternalChange, replace, clientSideUserId, mergeWithStorageOnRead = false, }) => {
|
|
11270
11294
|
const { encode, decode } = useCustomEncoding();
|
|
11271
11295
|
const { searchValue, updateSearchParam } = useSearchParamSync({
|
|
11272
11296
|
key,
|
|
@@ -11275,51 +11299,96 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
11275
11299
|
replace,
|
|
11276
11300
|
});
|
|
11277
11301
|
const storageKey = useStorageKey(key, clientSideUserId);
|
|
11278
|
-
const validateRef = useRef(validate);
|
|
11279
|
-
useEffect(() => {
|
|
11280
|
-
validateRef.current = validate;
|
|
11281
|
-
}, [validate]);
|
|
11282
11302
|
const toUrlValueRef = useRef(toUrlValue);
|
|
11283
11303
|
useEffect(() => {
|
|
11284
11304
|
toUrlValueRef.current = toUrlValue;
|
|
11285
11305
|
}, [toUrlValue]);
|
|
11306
|
+
const serializeRef = useRef(serialize);
|
|
11307
|
+
useEffect(() => {
|
|
11308
|
+
serializeRef.current = serialize;
|
|
11309
|
+
}, [serialize]);
|
|
11310
|
+
const validateRef = useRef(validate);
|
|
11311
|
+
useEffect(() => {
|
|
11312
|
+
validateRef.current = validate;
|
|
11313
|
+
}, [validate]);
|
|
11286
11314
|
const fromUrlValueRef = useRef(fromUrlValue);
|
|
11287
11315
|
useEffect(() => {
|
|
11288
11316
|
fromUrlValueRef.current = fromUrlValue;
|
|
11289
11317
|
}, [fromUrlValue]);
|
|
11290
|
-
const
|
|
11318
|
+
const searchValueRef = useRef(searchValue);
|
|
11291
11319
|
useEffect(() => {
|
|
11292
|
-
|
|
11293
|
-
}, [
|
|
11294
|
-
const
|
|
11295
|
-
|
|
11320
|
+
searchValueRef.current = searchValue;
|
|
11321
|
+
}, [searchValue]);
|
|
11322
|
+
const readPersistedState = useCallback(({ valueFromUrl, validateState, transformFromUrlValue, }) => {
|
|
11323
|
+
let urlState;
|
|
11324
|
+
if (enabled && valueFromUrl) {
|
|
11296
11325
|
try {
|
|
11297
|
-
const decoded = decode(
|
|
11298
|
-
const transformed =
|
|
11299
|
-
|
|
11300
|
-
if (validated !== undefined) {
|
|
11301
|
-
return { initialState: validated, loadedFromStorage: false };
|
|
11302
|
-
}
|
|
11326
|
+
const decoded = decode(valueFromUrl);
|
|
11327
|
+
const transformed = transformFromUrlValue ? transformFromUrlValue(decoded) : decoded;
|
|
11328
|
+
urlState = validateState(transformed);
|
|
11303
11329
|
}
|
|
11304
11330
|
catch {
|
|
11305
11331
|
// fall through to localStorage
|
|
11306
11332
|
}
|
|
11307
11333
|
}
|
|
11308
|
-
|
|
11309
|
-
|
|
11310
|
-
|
|
11311
|
-
|
|
11312
|
-
|
|
11313
|
-
|
|
11334
|
+
// Without the merge option, URL state wins entirely when present —
|
|
11335
|
+
// this preserves the original "URL > localStorage" priority.
|
|
11336
|
+
if (urlState !== undefined && !mergeWithStorageOnRead) {
|
|
11337
|
+
return { initialState: urlState, loadedFromStorage: false };
|
|
11338
|
+
}
|
|
11339
|
+
let storageState;
|
|
11340
|
+
if (localStorageEnabled) {
|
|
11341
|
+
try {
|
|
11342
|
+
const raw = localStorage.getItem(storageKey);
|
|
11343
|
+
if (raw) {
|
|
11344
|
+
const parsed = storageSerializer.deserialize(raw);
|
|
11345
|
+
storageState = validateState(parsed);
|
|
11346
|
+
}
|
|
11314
11347
|
}
|
|
11348
|
+
catch {
|
|
11349
|
+
// no valid stored state
|
|
11350
|
+
}
|
|
11351
|
+
}
|
|
11352
|
+
if (urlState !== undefined && storageState !== undefined) {
|
|
11353
|
+
// Shallow merge — URL fields take precedence; storage fills in
|
|
11354
|
+
// whatever the URL omits (e.g. fields stripped by `toUrlValue`).
|
|
11355
|
+
return { initialState: { ...storageState, ...urlState }, loadedFromStorage: false };
|
|
11315
11356
|
}
|
|
11316
|
-
|
|
11317
|
-
|
|
11357
|
+
if (urlState !== undefined) {
|
|
11358
|
+
return { initialState: urlState, loadedFromStorage: false };
|
|
11359
|
+
}
|
|
11360
|
+
if (storageState !== undefined) {
|
|
11361
|
+
return { initialState: storageState, loadedFromStorage: true };
|
|
11318
11362
|
}
|
|
11319
11363
|
return { initialState: undefined, loadedFromStorage: false };
|
|
11320
|
-
});
|
|
11364
|
+
}, [decode, enabled, localStorageEnabled, storageKey, mergeWithStorageOnRead]);
|
|
11365
|
+
const [persistedStateSnapshot, setPersistedStateSnapshot] = useState(() => readPersistedState({
|
|
11366
|
+
valueFromUrl: searchValue,
|
|
11367
|
+
validateState: validate,
|
|
11368
|
+
transformFromUrlValue: fromUrlValue,
|
|
11369
|
+
}));
|
|
11370
|
+
const persistedStateReadKey = `${enabled}:${localStorageEnabled}:${mergeWithStorageOnRead}:${storageKey}`;
|
|
11371
|
+
const persistedStateReadKeyRef = useRef(persistedStateReadKey);
|
|
11372
|
+
useEffect(() => {
|
|
11373
|
+
if (persistedStateReadKeyRef.current === persistedStateReadKey) {
|
|
11374
|
+
return;
|
|
11375
|
+
}
|
|
11376
|
+
persistedStateReadKeyRef.current = persistedStateReadKey;
|
|
11377
|
+
setPersistedStateSnapshot(readPersistedState({
|
|
11378
|
+
valueFromUrl: searchValueRef.current,
|
|
11379
|
+
validateState: validateRef.current,
|
|
11380
|
+
transformFromUrlValue: fromUrlValueRef.current,
|
|
11381
|
+
}));
|
|
11382
|
+
}, [persistedStateReadKey, readPersistedState]);
|
|
11383
|
+
const { initialState, loadedFromStorage } = persistedStateSnapshot;
|
|
11321
11384
|
const lastPersistedRef = useRef(initialState);
|
|
11385
|
+
useEffect(() => {
|
|
11386
|
+
lastPersistedRef.current = initialState;
|
|
11387
|
+
}, [initialState, localStorageEnabled, storageKey]);
|
|
11322
11388
|
const hasRestoredUrlRef = useRef(false);
|
|
11389
|
+
useEffect(() => {
|
|
11390
|
+
hasRestoredUrlRef.current = false;
|
|
11391
|
+
}, [enabled, localStorageEnabled, storageKey]);
|
|
11323
11392
|
useEffect(() => {
|
|
11324
11393
|
if (hasRestoredUrlRef.current) {
|
|
11325
11394
|
return;
|
|
@@ -11329,18 +11398,22 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
|
|
|
11329
11398
|
}
|
|
11330
11399
|
hasRestoredUrlRef.current = true;
|
|
11331
11400
|
const urlValue = toUrlValueRef.current ? toUrlValueRef.current(initialState) : initialState;
|
|
11332
|
-
|
|
11401
|
+
const encoded = encode(urlValue);
|
|
11402
|
+
updateSearchParam(encoded, { replace: true });
|
|
11333
11403
|
}, [enabled, loadedFromStorage, initialState, searchValue, updateSearchParam, encode]);
|
|
11334
11404
|
const persistState = useCallback((state) => {
|
|
11335
11405
|
if (dequal(lastPersistedRef.current, state)) {
|
|
11336
11406
|
return;
|
|
11337
11407
|
}
|
|
11338
11408
|
lastPersistedRef.current = state;
|
|
11339
|
-
|
|
11340
|
-
|
|
11409
|
+
if (localStorageEnabled) {
|
|
11410
|
+
const serialized = serializeRef.current ? serializeRef.current(state) : storageSerializer.serialize(state);
|
|
11411
|
+
localStorage.setItem(storageKey, serialized);
|
|
11412
|
+
}
|
|
11341
11413
|
const urlValue = toUrlValueRef.current ? toUrlValueRef.current(state) : state;
|
|
11342
|
-
|
|
11343
|
-
|
|
11414
|
+
const encoded = encode(urlValue);
|
|
11415
|
+
updateSearchParam(encoded);
|
|
11416
|
+
}, [storageKey, encode, localStorageEnabled, updateSearchParam]);
|
|
11344
11417
|
return useMemo(() => ({ initialState, persistState }), [initialState, persistState]);
|
|
11345
11418
|
};
|
|
11346
11419
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-components",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.6",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"migrations": "./migrations.json",
|
|
@@ -14,10 +14,10 @@
|
|
|
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.13.
|
|
18
|
-
"@trackunit/css-class-variance-utilities": "1.13.
|
|
19
|
-
"@trackunit/shared-utils": "1.15.
|
|
20
|
-
"@trackunit/ui-icons": "1.13.
|
|
17
|
+
"@trackunit/ui-design-tokens": "1.13.3",
|
|
18
|
+
"@trackunit/css-class-variance-utilities": "1.13.3",
|
|
19
|
+
"@trackunit/shared-utils": "1.15.3",
|
|
20
|
+
"@trackunit/ui-icons": "1.13.3",
|
|
21
21
|
"es-toolkit": "^1.39.10",
|
|
22
22
|
"@tanstack/react-virtual": "3.13.12",
|
|
23
23
|
"dequal": "^2.0.3",
|
|
@@ -5,9 +5,23 @@ type UsePersistedStateOptions<TState> = {
|
|
|
5
5
|
readonly toUrlValue?: (state: TState) => string | object | Array<object>;
|
|
6
6
|
readonly fromUrlValue?: (decoded: unknown) => unknown;
|
|
7
7
|
readonly enabled?: boolean;
|
|
8
|
+
readonly localStorageEnabled?: boolean;
|
|
8
9
|
readonly onExternalChange?: () => void;
|
|
9
10
|
readonly replace?: boolean;
|
|
10
11
|
readonly clientSideUserId?: string;
|
|
12
|
+
/**
|
|
13
|
+
* When `true` and both URL and localStorage have valid state, the read
|
|
14
|
+
* returns a shallow merge `{ ...storage, ...url }` where URL fields take
|
|
15
|
+
* precedence and storage fills in fields the URL omits.
|
|
16
|
+
*
|
|
17
|
+
* Use this when `toUrlValue` deliberately omits some fields from the URL
|
|
18
|
+
* (e.g. per-user layout details that shouldn't be shareable) so the
|
|
19
|
+
* omitted fields still survive reloads via localStorage.
|
|
20
|
+
*
|
|
21
|
+
* Default is `false`, preserving the original behavior where URL state
|
|
22
|
+
* fully replaces localStorage state on read.
|
|
23
|
+
*/
|
|
24
|
+
readonly mergeWithStorageOnRead?: boolean;
|
|
11
25
|
};
|
|
12
26
|
type UsePersistedStateReturn<TState> = {
|
|
13
27
|
readonly initialState: TState | undefined;
|
|
@@ -17,9 +31,13 @@ type UsePersistedStateReturn<TState> = {
|
|
|
17
31
|
* Generic persistence hook that loads state from URL search params (with
|
|
18
32
|
* localStorage fallback) and writes changes to both.
|
|
19
33
|
*
|
|
20
|
-
* On mount the hook
|
|
21
|
-
*
|
|
22
|
-
*
|
|
34
|
+
* On mount the hook resolves the initial state as follows:
|
|
35
|
+
* - If a URL search param exists, decode it and pass through `validate`.
|
|
36
|
+
* - If localStorage is enabled, read and validate its value.
|
|
37
|
+
* - When `mergeWithStorageOnRead` is `false` (default): URL state wins
|
|
38
|
+
* entirely when present, otherwise fall back to localStorage.
|
|
39
|
+
* - When `mergeWithStorageOnRead` is `true`: shallow-merge URL fields over
|
|
40
|
+
* localStorage so fields stripped by `toUrlValue` survive via storage.
|
|
23
41
|
*
|
|
24
42
|
* `persistState` writes the state to localStorage (via `serialize`, default
|
|
25
43
|
* `JSON.stringify`) and syncs to the URL (via `toUrlValue` + encoding).
|
|
@@ -32,9 +50,13 @@ type UsePersistedStateReturn<TState> = {
|
|
|
32
50
|
* @param options.toUrlValue - Transform applied before URL encoding (default: encode state as-is).
|
|
33
51
|
* @param options.fromUrlValue - Transform applied after URL decoding, before validation (default: identity).
|
|
34
52
|
* @param options.enabled - When `false` the URL is neither read nor written (default `true`).
|
|
53
|
+
* @param options.localStorageEnabled - When `false` localStorage is neither read nor written (default `true`).
|
|
35
54
|
* @param options.onExternalChange - Fired when the URL param changes externally (e.g. browser back).
|
|
36
55
|
* @param options.replace - Forwarded to `useSearchParamSync`.
|
|
37
56
|
* @param options.clientSideUserId - The user ID to use for the localStorage key.
|
|
57
|
+
* @param options.mergeWithStorageOnRead - Merge URL state on top of localStorage
|
|
58
|
+
* state on read (URL wins on shared keys). Use when `toUrlValue` deliberately
|
|
59
|
+
* omits fields so they still survive via localStorage. Default `false`.
|
|
38
60
|
*/
|
|
39
|
-
export declare const usePersistedState: <TState extends object>({ key, validate, serialize, toUrlValue, fromUrlValue, enabled, onExternalChange, replace, clientSideUserId, }: UsePersistedStateOptions<TState>) => UsePersistedStateReturn<TState>;
|
|
61
|
+
export declare const usePersistedState: <TState extends object>({ key, validate, serialize, toUrlValue, fromUrlValue, enabled, localStorageEnabled, onExternalChange, replace, clientSideUserId, mergeWithStorageOnRead, }: UsePersistedStateOptions<TState>) => UsePersistedStateReturn<TState>;
|
|
40
62
|
export {};
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default maximum full URL length after adding one persisted search param
|
|
3
|
+
* before the URL write is skipped (localStorage still persists the full state).
|
|
4
|
+
*
|
|
5
|
+
* Sized to stay safely below the AWS WAF managed-rule default
|
|
6
|
+
* `SizeRestrictions_QUERYSTRING` cap of 5000 bytes for the entire url host + path + query + hash string
|
|
7
|
+
*/
|
|
1
8
|
export declare const MAX_URL_LENGTH = 5000;
|
|
2
9
|
type UseSearchParamSyncOptions = {
|
|
3
10
|
readonly key: string;
|