@trackunit/react-components 1.25.4 → 1.26.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 +639 -55
- package/index.esm.js +638 -56
- package/package.json +1 -1
- package/src/hooks/persistence/hashFragment.d.ts +62 -0
- package/src/hooks/persistence/useHashParamSync.d.ts +55 -0
- package/src/hooks/persistence/useMigrateLegacySearchParam.d.ts +62 -0
- package/src/hooks/persistence/usePersistedState.d.ts +34 -9
- package/src/index.d.ts +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utilities for reading and writing single named entries inside a URL
|
|
3
|
+
* fragment (the part after `#`).
|
|
4
|
+
*
|
|
5
|
+
* # Format contract
|
|
6
|
+
*
|
|
7
|
+
* The fragment body (after the leading `#`) is treated as a sequence of
|
|
8
|
+
* `&`-separated tokens. A token may be:
|
|
9
|
+
* - **A named entry** — `<key>=<value>` (the first `=` separates key from
|
|
10
|
+
* value; later `=` chars are part of the value).
|
|
11
|
+
* - **Foreign content** — anything else (bare scroll anchors like
|
|
12
|
+
* `section`, hash routes, third-party data, malformed tokens). Foreign
|
|
13
|
+
* tokens are preserved byte-for-byte, in their original positions.
|
|
14
|
+
*
|
|
15
|
+
* # Single-key ownership
|
|
16
|
+
*
|
|
17
|
+
* Callers operate on **one** named key per invocation. Every token whose
|
|
18
|
+
* key does not match the supplied key is foreign — the utility never
|
|
19
|
+
* categorises or rewrites it. This mirrors how `useSearchParamSync`
|
|
20
|
+
* treats a single named search param.
|
|
21
|
+
*
|
|
22
|
+
* # Value-encoding invariant
|
|
23
|
+
*
|
|
24
|
+
* Values passed to {@link writeHashKey} must not contain `&`, `=`, or `#`.
|
|
25
|
+
* The standard producer for this codebase is `useCustomEncoding`, whose
|
|
26
|
+
* base64url alphabet (`[A-Za-z0-9_-]`) satisfies the invariant by
|
|
27
|
+
* construction. Keys are restricted by the existing persistence-key
|
|
28
|
+
* schema (camelCase, ≤15 chars), so they are likewise safe.
|
|
29
|
+
*
|
|
30
|
+
* # Anchor-scroll caveat
|
|
31
|
+
*
|
|
32
|
+
* Browsers locate scroll targets by matching the entire fragment string
|
|
33
|
+
* against element IDs. Appending a named entry to a fragment containing
|
|
34
|
+
* a bare anchor (e.g. `#section` becoming `#section&userTableTp=...`)
|
|
35
|
+
* therefore breaks anchor scrolling on that page. This is unavoidable
|
|
36
|
+
* for any hash-backed shared state. Consumers that rely on scroll
|
|
37
|
+
* anchors should not also persist state to the hash on the same route.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Reads the value of a single named entry from a URL fragment.
|
|
41
|
+
*
|
|
42
|
+
* @param hash - The fragment string, with or without the leading `#`.
|
|
43
|
+
* @param key - The entry key to look up.
|
|
44
|
+
* @returns {string | undefined} The raw value (everything after the first
|
|
45
|
+
* `=` in the matching token), or `undefined` when the key is absent. An
|
|
46
|
+
* entry whose token has no `=` is treated as foreign content and ignored.
|
|
47
|
+
*/
|
|
48
|
+
export declare const readHashKey: (hash: string, key: string) => string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Returns a new fragment string with the named entry written, replaced,
|
|
51
|
+
* or removed. Foreign tokens are preserved in their original order; the
|
|
52
|
+
* named entry retains its position if it already exists, and is appended
|
|
53
|
+
* to the end when newly inserted. Empty tokens (e.g. produced by
|
|
54
|
+
* consecutive `&`) are dropped on the way out.
|
|
55
|
+
*
|
|
56
|
+
* @param hash - The current fragment string, with or without the leading `#`.
|
|
57
|
+
* @param key - The entry key to write.
|
|
58
|
+
* @param value - The value to set, or `undefined` to remove the entry.
|
|
59
|
+
* @returns {string} The new fragment string, including a leading `#` when
|
|
60
|
+
* non-empty, or an empty string when the result has no tokens left.
|
|
61
|
+
*/
|
|
62
|
+
export declare const writeHashKey: (hash: string, key: string, value: string | undefined) => string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default maximum length of the resulting URL fragment after adding one
|
|
3
|
+
* persisted entry. Writes are skipped (localStorage continues to hold the
|
|
4
|
+
* full state) when adding the entry would push the fragment past this cap.
|
|
5
|
+
*
|
|
6
|
+
* The fragment is never sent to the server, so it is not subject to the
|
|
7
|
+
* AWS WAF managed-rule `SizeRestrictions_QUERYSTRING` cap that applies to
|
|
8
|
+
* search params (5000 bytes). 64 KB stays well below typical browser URL
|
|
9
|
+
* limits (Chromium ≳ 100 KB, Firefox ≳ 64 KB on the address bar) while
|
|
10
|
+
* still leaving generous headroom for any realistic table state.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_HASH_LENGTH: number;
|
|
13
|
+
type UseHashParamSyncOptions = {
|
|
14
|
+
readonly key: string;
|
|
15
|
+
readonly enabled?: boolean;
|
|
16
|
+
readonly onExternalChange?: () => void;
|
|
17
|
+
readonly replace?: boolean;
|
|
18
|
+
};
|
|
19
|
+
type UseHashParamSyncReturn = {
|
|
20
|
+
/**
|
|
21
|
+
* Mirrors the field name on `useSearchParamSync` so this hook is a
|
|
22
|
+
* drop-in replacement when only the URL location changes (search ↔ hash).
|
|
23
|
+
* Despite the name, this value is read from `location.hash`.
|
|
24
|
+
*/
|
|
25
|
+
readonly searchValue: string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Mirrors the function name on `useSearchParamSync`. Writes to
|
|
28
|
+
* `location.hash`, preserving any foreign fragment content untouched.
|
|
29
|
+
*/
|
|
30
|
+
readonly updateSearchParam: (encodedValue: string | undefined, options?: {
|
|
31
|
+
readonly replace?: boolean;
|
|
32
|
+
}) => void;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Syncs an encoded string value with a single named entry inside the URL
|
|
36
|
+
* fragment (`location.hash`) via Tanstack Router.
|
|
37
|
+
*
|
|
38
|
+
* Behaviour mirrors `useSearchParamSync`, with two differences:
|
|
39
|
+
* - Reads from and writes to `location.hash` instead of `location.search`.
|
|
40
|
+
* - Writes are guarded against {@link MAX_HASH_LENGTH} rather than the
|
|
41
|
+
* AWS-WAF-driven search-param cap.
|
|
42
|
+
*
|
|
43
|
+
* The fragment is treated as a sequence of `&`-separated tokens via the
|
|
44
|
+
* pure utilities in `./hashFragment`. Foreign tokens (bare anchors, other
|
|
45
|
+
* named entries) are preserved byte-for-byte on every write.
|
|
46
|
+
*
|
|
47
|
+
* @param options - Configuration for the hash entry sync.
|
|
48
|
+
* @param options.key - The named entry inside the fragment.
|
|
49
|
+
* @param options.enabled - Set to `false` to disable all URL interaction (default `true`).
|
|
50
|
+
* @param options.onExternalChange - Called when the entry changes externally
|
|
51
|
+
* (e.g. browser back/forward, pasted link, foreign router push).
|
|
52
|
+
* @param options.replace - `true` to always use `replaceState` instead of pushState.
|
|
53
|
+
*/
|
|
54
|
+
export declare const useHashParamSync: ({ key, enabled, onExternalChange, replace: replaceOption, }: UseHashParamSyncOptions) => UseHashParamSyncReturn;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
type UseMigrateLegacySearchParamOptions<TState> = {
|
|
2
|
+
readonly key: string;
|
|
3
|
+
readonly enabled: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Pure decoder for the raw search-param string. Should not throw — return
|
|
6
|
+
* `undefined` when the input can't be decoded.
|
|
7
|
+
*/
|
|
8
|
+
readonly decode: (rawSearchValue: string) => unknown;
|
|
9
|
+
/**
|
|
10
|
+
* Optional transform applied to the decoded payload before validation
|
|
11
|
+
* (mirrors `usePersistedState`'s `fromUrlValue`).
|
|
12
|
+
*/
|
|
13
|
+
readonly fromUrlValue?: (decoded: unknown) => unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Schema validator. Return the parsed state on success, `undefined`
|
|
16
|
+
* otherwise.
|
|
17
|
+
*/
|
|
18
|
+
readonly validate: (raw: unknown) => TState | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* When `true`, the hash already carries a valid value for this key. The
|
|
21
|
+
* hook still strips the legacy search param (so the URL is clean) but
|
|
22
|
+
* does NOT fire `onMigrated` — the hash is canonical and we must not
|
|
23
|
+
* stomp on it.
|
|
24
|
+
*/
|
|
25
|
+
readonly hashAlreadyHasValue: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Called at most once when a valid legacy search-param value is found
|
|
28
|
+
* and the hash is currently empty. Pipe through your normal
|
|
29
|
+
* `persistState` so the value ends up in both the hash and localStorage.
|
|
30
|
+
*/
|
|
31
|
+
readonly onMigrated: (state: TState) => void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* One-shot migration: if the URL still carries a legacy `?<key>=...`
|
|
35
|
+
* search param from before this hook moved table-style state to the
|
|
36
|
+
* fragment, decode it, hand it to the caller via `onMigrated`, then strip
|
|
37
|
+
* the search param via a `replace` navigation so back/forward history
|
|
38
|
+
* isn't polluted.
|
|
39
|
+
*
|
|
40
|
+
* Invariants:
|
|
41
|
+
* - The strip navigation fires at most once per mount (guarded by
|
|
42
|
+
* `hasStrippedRef`) so we don't loop on the navigation we ourselves
|
|
43
|
+
* triggered.
|
|
44
|
+
* - `onMigrated` is also fired at most once per mount (guarded by
|
|
45
|
+
* `hasMigratedRef`), but its gating is independent of the strip:
|
|
46
|
+
* if `hashAlreadyHasValue` flips `true → false` after the strip
|
|
47
|
+
* (e.g. an external actor clears the canonical hash mid-mount), the
|
|
48
|
+
* next render still gets a chance to migrate the legacy value into
|
|
49
|
+
* the hash. Conflating the two flags would have permanently lost
|
|
50
|
+
* the legacy state in that race.
|
|
51
|
+
* - Never throws — a decode/validate failure silently drops the value
|
|
52
|
+
* and still strips the search param. The legacy key is owned by this
|
|
53
|
+
* codebase's persistence convention (camelCase + `Tp` suffix), so
|
|
54
|
+
* stripping unparseable values cannot destroy third-party data.
|
|
55
|
+
* - When `hashAlreadyHasValue` is `true` the hash wins: the search
|
|
56
|
+
* param is stripped but `onMigrated` is not fired.
|
|
57
|
+
* - The `location.hash` is preserved verbatim across the strip
|
|
58
|
+
* navigation so any concurrent hash content (including a freshly
|
|
59
|
+
* written value) survives the cleanup.
|
|
60
|
+
*/
|
|
61
|
+
export declare const useMigrateLegacySearchParam: <TState>({ key, enabled, decode, fromUrlValue, validate, hashAlreadyHasValue, onMigrated, }: UseMigrateLegacySearchParamOptions<TState>) => void;
|
|
62
|
+
export {};
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selects which part of the URL backs the persisted state.
|
|
3
|
+
*
|
|
4
|
+
* - `"search"` (default): `?<key>=<encoded>`. Subject to the AWS WAF
|
|
5
|
+
* managed-rule cap (~5000 bytes) — large states may be silently
|
|
6
|
+
* truncated before they hit the browser bar.
|
|
7
|
+
* - `"hash"`: `#<key>=<encoded>`. Stays on the client, so the cap is
|
|
8
|
+
* driven only by browser URL limits. When this mode is selected,
|
|
9
|
+
* `usePersistedState` also performs a one-shot migration from any
|
|
10
|
+
* legacy `?<key>=...` value so previously shared links don't lose state.
|
|
11
|
+
*/
|
|
12
|
+
export type UrlPersistenceLocation = "search" | "hash";
|
|
1
13
|
type UsePersistedStateOptions<TState> = {
|
|
2
14
|
readonly key: string;
|
|
3
15
|
readonly validate: (raw: unknown) => TState | undefined;
|
|
@@ -22,17 +34,28 @@ type UsePersistedStateOptions<TState> = {
|
|
|
22
34
|
* fully replaces localStorage state on read.
|
|
23
35
|
*/
|
|
24
36
|
readonly mergeWithStorageOnRead?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Which slot of the URL backs the persisted value. Default `"search"`.
|
|
39
|
+
* See {@link UrlPersistenceLocation} for the trade-offs.
|
|
40
|
+
*/
|
|
41
|
+
readonly urlLocation?: UrlPersistenceLocation;
|
|
25
42
|
};
|
|
26
43
|
type UsePersistedStateReturn<TState> = {
|
|
27
44
|
readonly initialState: TState | undefined;
|
|
28
45
|
readonly persistState: (state: TState) => void;
|
|
29
46
|
};
|
|
30
47
|
/**
|
|
31
|
-
* Generic persistence hook that loads state from URL search
|
|
32
|
-
*
|
|
48
|
+
* Generic persistence hook that loads state from a URL slot (search param
|
|
49
|
+
* or hash fragment, depending on `urlLocation`) with localStorage fallback,
|
|
50
|
+
* and writes changes to both.
|
|
33
51
|
*
|
|
34
52
|
* On mount the hook resolves the initial state as follows:
|
|
35
|
-
* - If
|
|
53
|
+
* - If the canonical URL slot (search OR hash, per `urlLocation`) holds a
|
|
54
|
+
* value, decode it and pass through `validate`.
|
|
55
|
+
* - In `urlLocation: "hash"` mode, if the canonical hash slot is empty
|
|
56
|
+
* but a **legacy** `?<key>=...` search param is present, decode and
|
|
57
|
+
* validate that as a one-shot fallback. A migration effect then strips
|
|
58
|
+
* the legacy search param via a `replace` navigation.
|
|
36
59
|
* - If localStorage is enabled, read and validate its value.
|
|
37
60
|
* - When `mergeWithStorageOnRead` is `false` (default): URL state wins
|
|
38
61
|
* entirely when present, otherwise fall back to localStorage.
|
|
@@ -40,23 +63,25 @@ type UsePersistedStateReturn<TState> = {
|
|
|
40
63
|
* localStorage so fields stripped by `toUrlValue` survive via storage.
|
|
41
64
|
*
|
|
42
65
|
* `persistState` writes the state to localStorage (via `serialize`, default
|
|
43
|
-
* `
|
|
44
|
-
* Duplicate writes where the state has not
|
|
66
|
+
* `storageSerializer.serialize`) and syncs to the canonical URL slot (via
|
|
67
|
+
* `toUrlValue` + encoding). Duplicate writes where the state has not
|
|
68
|
+
* changed (deep equality) are skipped.
|
|
45
69
|
*
|
|
46
70
|
* @param options - Configuration for the persisted state.
|
|
47
|
-
* @param options.key - Unique identifier used for both the URL
|
|
71
|
+
* @param options.key - Unique identifier used for both the URL slot and the localStorage key.
|
|
48
72
|
* @param options.validate - Called with the decoded/parsed value; must return `TState` or `undefined`.
|
|
49
73
|
* @param options.serialize - Custom localStorage serialiser (default `storageSerializer.serialize`).
|
|
50
74
|
* @param options.toUrlValue - Transform applied before URL encoding (default: encode state as-is).
|
|
51
75
|
* @param options.fromUrlValue - Transform applied after URL decoding, before validation (default: identity).
|
|
52
76
|
* @param options.enabled - When `false` the URL is neither read nor written (default `true`).
|
|
53
77
|
* @param options.localStorageEnabled - When `false` localStorage is neither read nor written (default `true`).
|
|
54
|
-
* @param options.onExternalChange - Fired when the URL
|
|
55
|
-
* @param options.replace - Forwarded to
|
|
78
|
+
* @param options.onExternalChange - Fired when the URL value changes externally (e.g. browser back).
|
|
79
|
+
* @param options.replace - Forwarded to the underlying URL sync hook.
|
|
56
80
|
* @param options.clientSideUserId - The user ID to use for the localStorage key.
|
|
57
81
|
* @param options.mergeWithStorageOnRead - Merge URL state on top of localStorage
|
|
58
82
|
* state on read (URL wins on shared keys). Use when `toUrlValue` deliberately
|
|
59
83
|
* omits fields so they still survive via localStorage. Default `false`.
|
|
84
|
+
* @param options.urlLocation - Which URL slot backs the value (`"search"` or `"hash"`). Default `"search"`.
|
|
60
85
|
*/
|
|
61
|
-
export declare const usePersistedState: <TState extends object>({ key, validate, serialize, toUrlValue, fromUrlValue, enabled, localStorageEnabled, onExternalChange, replace, clientSideUserId, mergeWithStorageOnRead, }: UsePersistedStateOptions<TState>) => UsePersistedStateReturn<TState>;
|
|
86
|
+
export declare const usePersistedState: <TState extends object>({ key, validate, serialize, toUrlValue, fromUrlValue, enabled, localStorageEnabled, onExternalChange, replace, clientSideUserId, mergeWithStorageOnRead, urlLocation, }: UsePersistedStateOptions<TState>) => UsePersistedStateReturn<TState>;
|
|
62
87
|
export {};
|
package/src/index.d.ts
CHANGED
|
@@ -117,6 +117,7 @@ export * from "./hooks/localStorage/useLocalStorageReducer";
|
|
|
117
117
|
export * from "./hooks/localStorage/useSessionStorage";
|
|
118
118
|
export * from "./hooks/localStorage/useSessionStorageReducer";
|
|
119
119
|
export * from "./hooks/noPagination";
|
|
120
|
+
export * from "./hooks/persistence/useHashParamSync";
|
|
120
121
|
export * from "./hooks/persistence/usePersistedState";
|
|
121
122
|
export * from "./hooks/persistence/useSearchParamSync";
|
|
122
123
|
export * from "./hooks/persistence/useStorageKey";
|