@alxia/react-router 0.17.0 → 0.18.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/README.md +2 -1
- package/dist/session/react/index.js +17 -6
- package/dist/session/react/index.js.map +3 -3
- package/dist/session/react/use-link-param.d.ts +12 -2
- package/dist/session/react/use-link-param.d.ts.map +1 -1
- package/docs/guide/session.md +18 -1
- package/docs/roadmap.md +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1144,7 +1144,7 @@ From `@alxia/react-router/session/react` (`react` and `react-router` are its onl
|
|
|
1144
1144
|
| --- | --- |
|
|
1145
1145
|
| `useSignOut({ action?, before? })` | `{ signOut, busy }`: posts `{}` as JSON to `action` (default `/sign-out`) after `before()`, whose failure never keeps the user signed in; `busy` until the navigation ends |
|
|
1146
1146
|
| `SIGN_OUT_PATH`, `SignOutOptions`, `signOutAfter(before, submit)` | `/sign-out`, the options, and the step `useSignOut` is built on |
|
|
1147
|
-
| `useLinkParam(name, { drop? }?)`, `LinkParamOptions` | a mail link's query parameter, read once (`null` when absent or empty); `drop: true` removes it from the address bar with `history.replaceState` |
|
|
1147
|
+
| `useLinkParam(name, { drop?, live? }?)`, `LinkParamOptions` | a mail link's query parameter, read once (`null` when absent or empty); `drop: true` removes it from the address bar with `history.replaceState`; `live: true` follows the URL, for a parameter a redirect adds to the same mounted route |
|
|
1148
1148
|
| `useCodeField(schema, refusal)`, `CodeField`, `CodeSchema` | a one-code field with no form library: `{ code, setCode, ref, error, check() }` over a synchronous Standard Schema; each new `refusal` object clears the refused code and focuses the input |
|
|
1149
1149
|
| `useOnRefusal(refusal, handler)` | the core under it, for any form library: `handler` runs once per new refusal, not for one present at mount |
|
|
1150
1150
|
|
|
@@ -1152,6 +1152,7 @@ From `@alxia/react-router/session/react` (`react` and `react-router` are its onl
|
|
|
1152
1152
|
import { useLinkParam, useSignOut } from '@alxia/react-router/session/react';
|
|
1153
1153
|
|
|
1154
1154
|
const token = useLinkParam('token', { drop: true });
|
|
1155
|
+
const linked = useLinkParam('state', { live: true }); // follows a redirect to this same route
|
|
1155
1156
|
const { signOut, busy } = useSignOut();
|
|
1156
1157
|
```
|
|
1157
1158
|
|
|
@@ -45,10 +45,19 @@ function useCodeField(schema, refusal) {
|
|
|
45
45
|
}
|
|
46
46
|
// src/session/react/use-link-param.ts
|
|
47
47
|
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
48
|
-
import { useSearchParams } from "react-router";
|
|
49
|
-
function useLinkParam(name, { drop = false } = {}) {
|
|
48
|
+
import { useLocation, useSearchParams } from "react-router";
|
|
49
|
+
function useLinkParam(name, { drop = false, live = false } = {}) {
|
|
50
50
|
const [params] = useSearchParams();
|
|
51
|
-
const
|
|
51
|
+
const { key } = useLocation();
|
|
52
|
+
const followed = live ? key : null;
|
|
53
|
+
const current = params.get(name) || null;
|
|
54
|
+
const [kept, setKept] = useState2(current);
|
|
55
|
+
const [seen, setSeen] = useState2(current);
|
|
56
|
+
if (live && current !== seen) {
|
|
57
|
+
setSeen(current);
|
|
58
|
+
if (!drop || current !== null)
|
|
59
|
+
setKept(current);
|
|
60
|
+
}
|
|
52
61
|
useEffect2(() => {
|
|
53
62
|
if (!drop)
|
|
54
63
|
return;
|
|
@@ -57,8 +66,10 @@ function useLinkParam(name, { drop = false } = {}) {
|
|
|
57
66
|
return;
|
|
58
67
|
url.searchParams.delete(name);
|
|
59
68
|
window.history.replaceState(window.history.state, "", `${url.pathname}${url.search}${url.hash}`);
|
|
60
|
-
}, [drop, name]);
|
|
61
|
-
|
|
69
|
+
}, [drop, name, followed]);
|
|
70
|
+
if (live && !drop)
|
|
71
|
+
return current;
|
|
72
|
+
return kept;
|
|
62
73
|
}
|
|
63
74
|
// src/session/react/use-sign-out.ts
|
|
64
75
|
import { useCallback as useCallback2, useState as useState3 } from "react";
|
|
@@ -90,5 +101,5 @@ export {
|
|
|
90
101
|
useSignOut
|
|
91
102
|
};
|
|
92
103
|
|
|
93
|
-
//# debugId=
|
|
104
|
+
//# debugId=D9CB7386EE263E4264756E2164756E21
|
|
94
105
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"sources": ["../src/session/react/use-code-field.ts", "../src/session/react/use-link-param.ts", "../src/session/react/use-sign-out.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import {\n\ttype RefObject,\n\tuseCallback,\n\tuseEffect,\n\tuseRef,\n\tuseState,\n} from 'react';\n\n/** The part of a Standard Schema (`~standard`) this hook reads; Zod, Valibot and ArkType satisfy it. */\nexport interface CodeSchema {\n\treadonly '~standard': {\n\t\tvalidate(value: unknown): unknown;\n\t};\n}\n\n/**\n * Calls `handler` once for each new `refusal` (a new object each time,\n * `undefined` when there is none). A refusal already there when the\n * component mounted is not new: it is the page's own, seen again.\n * Library-agnostic: pair it with any form library's `reset` and `focus`.\n */\nexport function useOnRefusal(\n\trefusal: object | undefined,\n\thandler: () => void,\n): void {\n\tconst handled = useRef(refusal);\n\tconst latest = useRef(handler);\n\tlatest.current = handler;\n\tuseEffect(() => {\n\t\tif (!refusal || handled.current === refusal) return;\n\t\thandled.current = refusal;\n\t\tlatest.current();\n\t}, [refusal]);\n}\n\nexport interface CodeField {\n\t/** The code typed so far. */\n\treadonly code: string;\n\treadonly setCode: (code: string) => void;\n\t/** On the input, so a refusal can focus it. */\n\treadonly ref: RefObject<HTMLInputElement | null>;\n\t/** The first issue's message of the last `check`, cleared by typing. */\n\treadonly error: string | undefined;\n\t/**\n\t * Validates the code against the schema: the parsed value, or\n\t * `undefined` and `error` set. Synchronous schemas only.\n\t */\n\treadonly check: () => unknown;\n}\n\n/**\n * A one-code field (an authenticator app's code, a recovery code) with no\n * form library: it holds the code, checks it against `schema`, and on each\n * new `refusal` clears the refused code and focuses the input for the next\n * try. With react-hook-form, use `useOnRefusal` and its `setValue` and\n * `setFocus` instead.\n */\nexport function useCodeField(\n\tschema: CodeSchema,\n\trefusal: object | undefined,\n): CodeField {\n\tconst [code, setCodeState] = useState('');\n\tconst [error, setError] = useState<string | undefined>();\n\tconst ref = useRef<HTMLInputElement | null>(null);\n\tconst setCode = useCallback((next: string) => {\n\t\tsetCodeState(next);\n\t\tsetError(undefined);\n\t}, []);\n\tuseOnRefusal(refusal, () => {\n\t\tsetCode('');\n\t\tref.current?.focus();\n\t});\n\tconst check = () => {\n\t\tconst result = schema['~standard'].validate(code);\n\t\tif (result instanceof Promise) {\n\t\t\tthrow new TypeError('useCodeField takes a synchronous schema');\n\t\t}\n\t\tconst { issues, value } = result as {\n\t\t\tissues?: ReadonlyArray<{ message: string }>;\n\t\t\tvalue?: unknown;\n\t\t};\n\t\tif (issues?.length) {\n\t\t\tsetError(issues[0]?.message);\n\t\t\treturn undefined;\n\t\t}\n\t\tsetError(undefined);\n\t\treturn value;\n\t};\n\treturn { code, setCode, ref, error, check };\n}\n",
|
|
6
|
-
"import { useEffect, useState } from 'react';\nimport { useSearchParams } from 'react-router';\n\nexport interface LinkParamOptions {\n\t/**\n\t * Remove the parameter from the address bar once read\n\t * (`history.replaceState`), so a token is not left in the history, a\n\t * bookmark or a shared screen. Default `false`.\n\t */\n\treadonly drop?: boolean;\n}\n\n/**\n * The query parameter a mail's link carries (`token`, `code`, `state`),\n * read once: the value of the first render is kept for the page's life, so\n * dropping it from the address bar never loses it. `null` when absent or\n * empty. Safe to render on the server, where nothing is dropped.\n */\nexport function useLinkParam(\n\tname: string,\n\t{ drop = false }: LinkParamOptions = {},\n): string | null {\n\tconst [params] = useSearchParams();\n\tconst
|
|
6
|
+
"import { useEffect, useState } from 'react';\nimport { useLocation, useSearchParams } from 'react-router';\n\nexport interface LinkParamOptions {\n\t/**\n\t * Remove the parameter from the address bar once read\n\t * (`history.replaceState`), so a token is not left in the history, a\n\t * bookmark or a shared screen. Default `false`.\n\t */\n\treadonly drop?: boolean;\n\t/**\n\t * Follow the URL instead of reading it once. A parameter that a redirect\n\t * or a link adds to the SAME mounted route (`?state=linked`) is seen, and\n\t * each new location is read again. Without `drop` the value is the\n\t * parameter of the current location. With `drop` the value is kept after\n\t * the address bar loses the parameter, until a location carries a new\n\t * one: a navigation that has none does not clear it. Default `false`.\n\t */\n\treadonly live?: boolean;\n}\n\n/**\n * The query parameter a mail's link carries (`token`, `code`, `state`),\n * read once: the value of the first render is kept for the page's life, so\n * dropping it from the address bar never loses it. With `live: true` it\n * follows the URL instead (see `LinkParamOptions`). `null` when absent or\n * empty. Safe to render on the server, where nothing is dropped.\n */\nexport function useLinkParam(\n\tname: string,\n\t{ drop = false, live = false }: LinkParamOptions = {},\n): string | null {\n\tconst [params] = useSearchParams();\n\tconst { key } = useLocation();\n\tconst followed = live ? key : null;\n\tconst current = params.get(name) || null;\n\tconst [kept, setKept] = useState(current);\n\tconst [seen, setSeen] = useState(current);\n\tif (live && current !== seen) {\n\t\tsetSeen(current);\n\t\tif (!drop || current !== null) setKept(current);\n\t}\n\tuseEffect(() => {\n\t\tif (!drop) return;\n\t\tconst url = new URL(window.location.href);\n\t\tif (!url.searchParams.has(name)) return;\n\t\turl.searchParams.delete(name);\n\t\twindow.history.replaceState(\n\t\t\twindow.history.state,\n\t\t\t'',\n\t\t\t`${url.pathname}${url.search}${url.hash}`,\n\t\t);\n\t}, [drop, name, followed]);\n\tif (live && !drop) return current;\n\treturn kept;\n}\n",
|
|
7
7
|
"import { useCallback, useState } from 'react';\nimport { useNavigation, useSubmit } from 'react-router';\n\n/** Where `signInFlow`'s sign-out action is mounted unless the app says otherwise. */\nexport const SIGN_OUT_PATH = '/sign-out';\n\nexport interface SignOutOptions {\n\t/** The sign-out route (default `/sign-out`). */\n\treadonly action?: string;\n\t/**\n\t * Runs before the post: clear the worker's caches, a local store. A\n\t * rejection never keeps the user signed in: the sign-out goes on.\n\t */\n\treadonly before?: () => Promise<unknown> | unknown;\n}\n\n/**\n * Runs `before`, then `submit`. A `before` that throws or rejects is\n * ignored, so a failed cleanup cannot keep a session alive.\n */\nexport async function signOutAfter(\n\tbefore: (() => Promise<unknown> | unknown) | undefined,\n\tsubmit: () => Promise<void> | void,\n): Promise<void> {\n\ttry {\n\t\tawait before?.();\n\t} catch {\n\t\t// Signing out matters more than the cleanup.\n\t}\n\tawait submit();\n}\n\n/**\n * The one way a page signs out, wherever the control is: posts `{}` as JSON\n * to the sign-out route (same-origin and JSON only, as `signInFlow`'s action\n * requires). `busy` from the call until the navigation it starts has ended.\n */\nexport function useSignOut(options: SignOutOptions = {}): {\n\tsignOut: () => void;\n\tbusy: boolean;\n} {\n\tconst { action = SIGN_OUT_PATH, before } = options;\n\tconst submit = useSubmit();\n\tconst [preparing, setPreparing] = useState(false);\n\tconst submitting = useNavigation().formAction === action;\n\tconst signOut = useCallback(() => {\n\t\tsetPreparing(true);\n\t\tvoid signOutAfter(before, () =>\n\t\t\tsubmit({}, { method: 'post', action, encType: 'application/json' }),\n\t\t).finally(() => setPreparing(false));\n\t}, [submit, action, before]);\n\treturn { signOut, busy: preparing || submitting };\n}\n"
|
|
8
8
|
],
|
|
9
|
-
"mappings": ";AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBO,SAAS,YAAY,CAC3B,SACA,SACO;AAAA,EACP,MAAM,UAAU,OAAO,OAAO;AAAA,EAC9B,MAAM,SAAS,OAAO,OAAO;AAAA,EAC7B,OAAO,UAAU;AAAA,EACjB,UAAU,MAAM;AAAA,IACf,IAAI,CAAC,WAAW,QAAQ,YAAY;AAAA,MAAS;AAAA,IAC7C,QAAQ,UAAU;AAAA,IAClB,OAAO,QAAQ;AAAA,KACb,CAAC,OAAO,CAAC;AAAA;AAyBN,SAAS,YAAY,CAC3B,QACA,SACY;AAAA,EACZ,OAAO,MAAM,gBAAgB,SAAS,EAAE;AAAA,EACxC,OAAO,OAAO,YAAY,SAA6B;AAAA,EACvD,MAAM,MAAM,OAAgC,IAAI;AAAA,EAChD,MAAM,UAAU,YAAY,CAAC,SAAiB;AAAA,IAC7C,aAAa,IAAI;AAAA,IACjB,SAAS,SAAS;AAAA,KAChB,CAAC,CAAC;AAAA,EACL,aAAa,SAAS,MAAM;AAAA,IAC3B,QAAQ,EAAE;AAAA,IACV,IAAI,SAAS,MAAM;AAAA,GACnB;AAAA,EACD,MAAM,QAAQ,MAAM;AAAA,IACnB,MAAM,SAAS,OAAO,aAAa,SAAS,IAAI;AAAA,IAChD,IAAI,kBAAkB,SAAS;AAAA,MAC9B,MAAM,IAAI,UAAU,yCAAyC;AAAA,IAC9D;AAAA,IACA,QAAQ,QAAQ,UAAU;AAAA,IAI1B,IAAI,QAAQ,QAAQ;AAAA,MACnB,SAAS,OAAO,IAAI,OAAO;AAAA,MAC3B;AAAA,IACD;AAAA,IACA,SAAS,SAAS;AAAA,IAClB,OAAO;AAAA;AAAA,EAER,OAAO,EAAE,MAAM,SAAS,KAAK,OAAO,MAAM;AAAA;;ACxF3C,sBAAS,wBAAW;AACpB;
|
|
10
|
-
"debugId": "
|
|
9
|
+
"mappings": ";AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBO,SAAS,YAAY,CAC3B,SACA,SACO;AAAA,EACP,MAAM,UAAU,OAAO,OAAO;AAAA,EAC9B,MAAM,SAAS,OAAO,OAAO;AAAA,EAC7B,OAAO,UAAU;AAAA,EACjB,UAAU,MAAM;AAAA,IACf,IAAI,CAAC,WAAW,QAAQ,YAAY;AAAA,MAAS;AAAA,IAC7C,QAAQ,UAAU;AAAA,IAClB,OAAO,QAAQ;AAAA,KACb,CAAC,OAAO,CAAC;AAAA;AAyBN,SAAS,YAAY,CAC3B,QACA,SACY;AAAA,EACZ,OAAO,MAAM,gBAAgB,SAAS,EAAE;AAAA,EACxC,OAAO,OAAO,YAAY,SAA6B;AAAA,EACvD,MAAM,MAAM,OAAgC,IAAI;AAAA,EAChD,MAAM,UAAU,YAAY,CAAC,SAAiB;AAAA,IAC7C,aAAa,IAAI;AAAA,IACjB,SAAS,SAAS;AAAA,KAChB,CAAC,CAAC;AAAA,EACL,aAAa,SAAS,MAAM;AAAA,IAC3B,QAAQ,EAAE;AAAA,IACV,IAAI,SAAS,MAAM;AAAA,GACnB;AAAA,EACD,MAAM,QAAQ,MAAM;AAAA,IACnB,MAAM,SAAS,OAAO,aAAa,SAAS,IAAI;AAAA,IAChD,IAAI,kBAAkB,SAAS;AAAA,MAC9B,MAAM,IAAI,UAAU,yCAAyC;AAAA,IAC9D;AAAA,IACA,QAAQ,QAAQ,UAAU;AAAA,IAI1B,IAAI,QAAQ,QAAQ;AAAA,MACnB,SAAS,OAAO,IAAI,OAAO;AAAA,MAC3B;AAAA,IACD;AAAA,IACA,SAAS,SAAS;AAAA,IAClB,OAAO;AAAA;AAAA,EAER,OAAO,EAAE,MAAM,SAAS,KAAK,OAAO,MAAM;AAAA;;ACxF3C,sBAAS,wBAAW;AACpB;AA2BO,SAAS,YAAY,CAC3B,QACE,OAAO,OAAO,OAAO,UAA4B,CAAC,GACpC;AAAA,EAChB,OAAO,UAAU,gBAAgB;AAAA,EACjC,QAAQ,QAAQ,YAAY;AAAA,EAC5B,MAAM,WAAW,OAAO,MAAM;AAAA,EAC9B,MAAM,UAAU,OAAO,IAAI,IAAI,KAAK;AAAA,EACpC,OAAO,MAAM,WAAW,UAAS,OAAO;AAAA,EACxC,OAAO,MAAM,WAAW,UAAS,OAAO;AAAA,EACxC,IAAI,QAAQ,YAAY,MAAM;AAAA,IAC7B,QAAQ,OAAO;AAAA,IACf,IAAI,CAAC,QAAQ,YAAY;AAAA,MAAM,QAAQ,OAAO;AAAA,EAC/C;AAAA,EACA,WAAU,MAAM;AAAA,IACf,IAAI,CAAC;AAAA,MAAM;AAAA,IACX,MAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AAAA,IACxC,IAAI,CAAC,IAAI,aAAa,IAAI,IAAI;AAAA,MAAG;AAAA,IACjC,IAAI,aAAa,OAAO,IAAI;AAAA,IAC5B,OAAO,QAAQ,aACd,OAAO,QAAQ,OACf,IACA,GAAG,IAAI,WAAW,IAAI,SAAS,IAAI,MACpC;AAAA,KACE,CAAC,MAAM,MAAM,QAAQ,CAAC;AAAA,EACzB,IAAI,QAAQ,CAAC;AAAA,IAAM,OAAO;AAAA,EAC1B,OAAO;AAAA;;ACtDR,wBAAS,0BAAa;AACtB;AAGO,IAAM,gBAAgB;AAgB7B,eAAsB,YAAY,CACjC,QACA,QACgB;AAAA,EAChB,IAAI;AAAA,IACH,MAAM,SAAS;AAAA,IACd,MAAM;AAAA,EAGR,MAAM,OAAO;AAAA;AAQP,SAAS,UAAU,CAAC,UAA0B,CAAC,GAGpD;AAAA,EACD,QAAQ,SAAS,eAAe,WAAW;AAAA,EAC3C,MAAM,SAAS,UAAU;AAAA,EACzB,OAAO,WAAW,gBAAgB,UAAS,KAAK;AAAA,EAChD,MAAM,aAAa,cAAc,EAAE,eAAe;AAAA,EAClD,MAAM,UAAU,aAAY,MAAM;AAAA,IACjC,aAAa,IAAI;AAAA,IACZ,aAAa,QAAQ,MACzB,OAAO,CAAC,GAAG,EAAE,QAAQ,QAAQ,QAAQ,SAAS,mBAAmB,CAAC,CACnE,EAAE,QAAQ,MAAM,aAAa,KAAK,CAAC;AAAA,KACjC,CAAC,QAAQ,QAAQ,MAAM,CAAC;AAAA,EAC3B,OAAO,EAAE,SAAS,MAAM,aAAa,WAAW;AAAA;",
|
|
10
|
+
"debugId": "D9CB7386EE263E4264756E2164756E21",
|
|
11
11
|
"names": []
|
|
12
12
|
}
|
|
@@ -5,12 +5,22 @@ export interface LinkParamOptions {
|
|
|
5
5
|
* bookmark or a shared screen. Default `false`.
|
|
6
6
|
*/
|
|
7
7
|
readonly drop?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Follow the URL instead of reading it once. A parameter that a redirect
|
|
10
|
+
* or a link adds to the SAME mounted route (`?state=linked`) is seen, and
|
|
11
|
+
* each new location is read again. Without `drop` the value is the
|
|
12
|
+
* parameter of the current location. With `drop` the value is kept after
|
|
13
|
+
* the address bar loses the parameter, until a location carries a new
|
|
14
|
+
* one: a navigation that has none does not clear it. Default `false`.
|
|
15
|
+
*/
|
|
16
|
+
readonly live?: boolean;
|
|
8
17
|
}
|
|
9
18
|
/**
|
|
10
19
|
* The query parameter a mail's link carries (`token`, `code`, `state`),
|
|
11
20
|
* read once: the value of the first render is kept for the page's life, so
|
|
12
|
-
* dropping it from the address bar never loses it. `
|
|
21
|
+
* dropping it from the address bar never loses it. With `live: true` it
|
|
22
|
+
* follows the URL instead (see `LinkParamOptions`). `null` when absent or
|
|
13
23
|
* empty. Safe to render on the server, where nothing is dropped.
|
|
14
24
|
*/
|
|
15
|
-
export declare function useLinkParam(name: string, { drop }?: LinkParamOptions): string | null;
|
|
25
|
+
export declare function useLinkParam(name: string, { drop, live }?: LinkParamOptions): string | null;
|
|
16
26
|
//# sourceMappingURL=use-link-param.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-link-param.d.ts","sourceRoot":"","sources":["../../../src/session/react/use-link-param.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAChC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACxB;AAED
|
|
1
|
+
{"version":3,"file":"use-link-param.d.ts","sourceRoot":"","sources":["../../../src/session/react/use-link-param.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAChC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC3B,IAAI,EAAE,MAAM,EACZ,EAAE,IAAY,EAAE,IAAY,EAAE,GAAE,gBAAqB,GACnD,MAAM,GAAG,IAAI,CAwBf"}
|
package/docs/guide/session.md
CHANGED
|
@@ -992,7 +992,7 @@ const { signOut } = useSignOut({
|
|
|
992
992
|
### `useLinkParam`
|
|
993
993
|
|
|
994
994
|
Reads the query parameter a mail's link carries, once: the first render's value
|
|
995
|
-
is kept for the page's life. With `drop: true` the parameter is then removed
|
|
995
|
+
is kept for the page's life (unless `live: true`, below). With `drop: true` the parameter is then removed
|
|
996
996
|
from the address bar (`history.replaceState`, keeping the history state and the
|
|
997
997
|
hash), so a token is not left in the history, a bookmark or a screenshot. It is
|
|
998
998
|
`null` when the parameter is absent or empty.
|
|
@@ -1017,6 +1017,23 @@ The router's own location is not updated by `replaceState`: a loader or
|
|
|
1017
1017
|
`useSearchParams` elsewhere still sees the token until the next navigation.
|
|
1018
1018
|
Read it with the hook once, and pass it down.
|
|
1019
1019
|
|
|
1020
|
+
#### `live: true`: a parameter set after the page mounted
|
|
1021
|
+
|
|
1022
|
+
By default the value is read once. A route that stays mounted while an action
|
|
1023
|
+
redirects back to it with a new parameter (`/settings` to
|
|
1024
|
+
`/settings?state=linked`) never sees it. `live: true` follows the URL: the
|
|
1025
|
+
value is the parameter of the current location, and each new location is read
|
|
1026
|
+
again.
|
|
1027
|
+
|
|
1028
|
+
```tsx
|
|
1029
|
+
const state = useLinkParam('state', { live: true }); // 'linked' | 'invalid' | null
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
With `drop: true` too, the value is kept after the address bar loses the
|
|
1033
|
+
parameter, until a location carries a new one: a navigation that has no such
|
|
1034
|
+
parameter does not clear it, since the drop removed it. Without `drop` the
|
|
1035
|
+
value is exactly the current location's parameter, `null` when it is gone.
|
|
1036
|
+
|
|
1020
1037
|
### `useCodeField` and `useOnRefusal`
|
|
1021
1038
|
|
|
1022
1039
|
A form that asks for one code (an authenticator app's, a recovery code) and
|
package/docs/roadmap.md
CHANGED
|
@@ -8,8 +8,8 @@ number on it. Every release, with each change it made, is in
|
|
|
8
8
|
## Now
|
|
9
9
|
|
|
10
10
|
- **`@alxia/react-router/session/react`: the page's hooks around the session
|
|
11
|
-
flows.** `useSignOut`, `useLinkParam` (a mail link's parameter, read once and
|
|
12
|
-
|
|
11
|
+
flows.** `useSignOut`, `useLinkParam` (a mail link's parameter, read once and optionally dropped
|
|
12
|
+
from the address bar, or with `live: true` following the URL) and `useCodeField` /
|
|
13
13
|
`useOnRefusal` (a one-code field that clears and refocuses on each refusal),
|
|
14
14
|
so an app stops carrying its own copies. See the
|
|
15
15
|
[session guide](guide/session.md#the-pages-hooks).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alxia/react-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "alxia as the server of a React Router framework app: server rendering behind the app's hooks, loaders reading its typed context, the client's assets served; the code with no UI an installable app shares (`/pwa`), its hooks (`/pwa/react`), `noCache` for the worker's script (`/pwa/server`), the service worker on Workbox (`/pwa/worker`) and its build (`/pwa/build`), an API's refusals worded for the page (`/errors`), the BFF session: cookies, sign-in, step-up and mail links (`/session`), its page hooks (`/session/react`) and what it shares with the page (`/session/model`), the page's CSP nonce and inline boot scripts (`/document`) and a streaming `entry.server` (`/entry`)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|