toiljs 0.0.14 → 0.0.16

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.
Files changed (225) hide show
  1. package/.babelrc +13 -13
  2. package/.gitattributes +2 -2
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +38 -38
  4. package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -90
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
  7. package/.github/PULL_REQUEST_TEMPLATE.md +43 -43
  8. package/.github/changelog-config.json +45 -45
  9. package/.github/dependabot.yml +27 -27
  10. package/.github/workflows/ci.yml +191 -191
  11. package/.prettierrc.json +11 -11
  12. package/.vscode/settings.json +9 -9
  13. package/CHANGELOG.md +5 -5
  14. package/LICENSE +187 -187
  15. package/README.md +339 -315
  16. package/as-pect.asconfig.json +34 -34
  17. package/as-pect.config.js +65 -65
  18. package/assets/logo.svg +36 -36
  19. package/build/backend/.tsbuildinfo +1 -1
  20. package/build/cli/.tsbuildinfo +1 -1
  21. package/build/cli/index.js +2926 -191
  22. package/build/client/.tsbuildinfo +1 -1
  23. package/build/client/dev/devtools.d.ts +6 -0
  24. package/build/client/dev/devtools.js +442 -0
  25. package/build/client/dev/error-overlay.d.ts +9 -0
  26. package/build/client/dev/error-overlay.js +19 -4
  27. package/build/client/head/metadata.d.ts +3 -1
  28. package/build/client/head/metadata.js +8 -0
  29. package/build/client/index.d.ts +4 -4
  30. package/build/client/index.js +2 -2
  31. package/build/client/navigation/navigation.d.ts +2 -0
  32. package/build/client/navigation/navigation.js +9 -1
  33. package/build/client/navigation/prefetch.d.ts +1 -0
  34. package/build/client/navigation/prefetch.js +35 -0
  35. package/build/client/routing/Router.js +1 -1
  36. package/build/client/routing/hooks.js +6 -2
  37. package/build/client/routing/loader.d.ts +25 -0
  38. package/build/client/routing/loader.js +53 -7
  39. package/build/client/routing/mount.js +4 -3
  40. package/build/compiler/.tsbuildinfo +1 -1
  41. package/build/compiler/config.d.ts +18 -0
  42. package/build/compiler/config.js +8 -0
  43. package/build/compiler/docs.js +16 -16
  44. package/build/compiler/generate.js +3 -0
  45. package/build/compiler/index.d.ts +2 -2
  46. package/build/compiler/index.js +3 -1
  47. package/build/compiler/plugin.js +156 -0
  48. package/build/compiler/prerender.d.ts +1 -0
  49. package/build/compiler/prerender.js +2 -1
  50. package/build/compiler/seo.d.ts +2 -2
  51. package/build/compiler/seo.js +8 -6
  52. package/build/compiler/ssg.d.ts +5 -0
  53. package/build/compiler/ssg.js +121 -0
  54. package/build/io/.tsbuildinfo +1 -1
  55. package/build/logger/.tsbuildinfo +1 -1
  56. package/build/shared/.tsbuildinfo +1 -1
  57. package/eslint.config.js +48 -48
  58. package/examples/basic/client/404.tsx +11 -11
  59. package/examples/basic/client/components/.gitkeep +1 -1
  60. package/examples/basic/client/global-error.tsx +13 -13
  61. package/examples/basic/client/layout.tsx +25 -25
  62. package/examples/basic/client/public/images/.gitkeep +1 -1
  63. package/examples/basic/client/public/images/logo.svg +36 -36
  64. package/examples/basic/client/public/robots.txt +2 -2
  65. package/examples/basic/client/routes/docs/[...slug].tsx +12 -12
  66. package/examples/basic/client/routes/features/error/error.tsx +16 -16
  67. package/examples/basic/client/routes/features/template/b.tsx +14 -14
  68. package/examples/basic/client/routes/files/[[...slug]].tsx +21 -21
  69. package/examples/basic/client/routes/gallery/layout.tsx +13 -13
  70. package/examples/basic/client/routes/io.tsx +24 -24
  71. package/examples/basic/client/routes/loader-demo/loading.tsx +13 -13
  72. package/examples/basic/client/routes/search.tsx +61 -61
  73. package/examples/basic/client/toil.tsx +5 -5
  74. package/package.json +155 -147
  75. package/presets/eslint.js +88 -88
  76. package/presets/no-uint8array-tostring.js +200 -200
  77. package/presets/prettier.json +18 -18
  78. package/presets/tsconfig.json +37 -37
  79. package/src/backend/index.ts +160 -160
  80. package/src/cli/proc.ts +50 -50
  81. package/src/cli/updates.ts +69 -69
  82. package/src/cli/validate.ts +31 -31
  83. package/src/client/channel/channel.ts +146 -146
  84. package/src/client/components/Form.tsx +65 -65
  85. package/src/client/components/Script.tsx +113 -113
  86. package/src/client/components/Slot.tsx +21 -21
  87. package/src/client/dev/devtools.tsx +973 -0
  88. package/src/client/dev/error-overlay.tsx +30 -4
  89. package/src/client/head/head.ts +167 -167
  90. package/src/client/head/metadata.ts +19 -1
  91. package/src/client/index.ts +19 -9
  92. package/src/client/navigation/NavLink.tsx +86 -86
  93. package/src/client/navigation/navigation.ts +25 -5
  94. package/src/client/navigation/prefetch.ts +169 -130
  95. package/src/client/navigation/scroll.ts +53 -53
  96. package/src/client/routing/Router.tsx +8 -2
  97. package/src/client/routing/action.ts +122 -122
  98. package/src/client/routing/error-boundary.tsx +43 -43
  99. package/src/client/routing/hooks.ts +21 -6
  100. package/src/client/routing/loader.ts +325 -225
  101. package/src/client/routing/match.ts +47 -47
  102. package/src/client/routing/mount.tsx +54 -52
  103. package/src/client/routing/params-context.ts +10 -10
  104. package/src/client/routing/slot-context.ts +7 -7
  105. package/src/client/search/search.ts +189 -189
  106. package/src/client/search/use-page-search.ts +73 -73
  107. package/src/client/types.ts +73 -73
  108. package/src/compiler/config.ts +47 -1
  109. package/src/compiler/docs.ts +228 -228
  110. package/src/compiler/generate.ts +394 -391
  111. package/src/compiler/index.ts +64 -54
  112. package/src/compiler/pages.ts +70 -70
  113. package/src/compiler/plugin.ts +170 -2
  114. package/src/compiler/prerender.ts +5 -1
  115. package/src/compiler/seo.ts +23 -7
  116. package/src/compiler/ssg.ts +162 -0
  117. package/src/io/BinaryReader.ts +340 -340
  118. package/src/io/BinaryWriter.ts +385 -385
  119. package/src/io/FastMap.ts +127 -127
  120. package/src/io/index.ts +11 -11
  121. package/src/io/lengths.ts +14 -14
  122. package/src/io/types.ts +18 -18
  123. package/src/logger/index.ts +22 -22
  124. package/src/server/index.ts +10 -10
  125. package/src/server/main.ts +13 -13
  126. package/src/server/tsconfig.json +4 -4
  127. package/src/shared/index.ts +10 -10
  128. package/std/client/index.d.ts +15 -15
  129. package/std/client/package.json +3 -3
  130. package/test/assembly/example.spec.ts +7 -7
  131. package/test/channel.test.ts +21 -21
  132. package/test/dom/Link.test.tsx +47 -47
  133. package/test/dom/NavLink.test.tsx +37 -37
  134. package/test/dom/error-overlay.test.tsx +44 -44
  135. package/test/dom/loader.test.tsx +121 -121
  136. package/test/dom/navigation.test.ts +59 -59
  137. package/test/dom/revalidate.test.tsx +38 -38
  138. package/test/dom/route-head.test.tsx +78 -78
  139. package/test/dom/router-loading.test.tsx +44 -44
  140. package/test/dom/scroll.test.ts +56 -56
  141. package/test/dom/use-metadata.test.tsx +58 -0
  142. package/test/io.test.ts +93 -93
  143. package/test/navlink.test.ts +28 -28
  144. package/test/placeholder.test.ts +9 -9
  145. package/test/routes.test.ts +76 -76
  146. package/test/seo.test.ts +175 -164
  147. package/test/slot-layouts.test.ts +69 -69
  148. package/test/ssg.test.ts +36 -0
  149. package/test/update.test.ts +44 -44
  150. package/test/validate.test.ts +42 -42
  151. package/toil-routes.d.ts +7 -0
  152. package/toilconfig.json +30 -30
  153. package/tsconfig.backend.json +13 -13
  154. package/tsconfig.base.json +35 -35
  155. package/tsconfig.cli.json +13 -13
  156. package/tsconfig.client.json +14 -14
  157. package/tsconfig.compiler.json +13 -13
  158. package/tsconfig.io.json +12 -12
  159. package/tsconfig.json +22 -22
  160. package/tsconfig.logger.json +12 -12
  161. package/tsconfig.server.json +10 -10
  162. package/tsconfig.shared.json +12 -12
  163. package/vitest.config.ts +26 -26
  164. package/.idea/codeStyles/Project.xml +0 -54
  165. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  166. package/.idea/inspectionProfiles/Project_Default.xml +0 -6
  167. package/.idea/modules.xml +0 -8
  168. package/.idea/prettier.xml +0 -7
  169. package/.idea/toiljs.iml +0 -8
  170. package/.idea/vcs.xml +0 -6
  171. package/.toil/entry.tsx +0 -9
  172. package/.toil/index.html +0 -12
  173. package/.toil/routes.ts +0 -9
  174. package/build/cli/configure.d.ts +0 -16
  175. package/build/cli/configure.js +0 -272
  176. package/build/cli/create.d.ts +0 -16
  177. package/build/cli/create.js +0 -420
  178. package/build/cli/diagnostics.d.ts +0 -55
  179. package/build/cli/diagnostics.js +0 -333
  180. package/build/cli/doctor.d.ts +0 -6
  181. package/build/cli/doctor.js +0 -249
  182. package/build/cli/features.d.ts +0 -25
  183. package/build/cli/features.js +0 -107
  184. package/build/cli/index.d.ts +0 -2
  185. package/build/cli/proc.d.ts +0 -6
  186. package/build/cli/proc.js +0 -31
  187. package/build/cli/ui.d.ts +0 -9
  188. package/build/cli/ui.js +0 -75
  189. package/build/cli/update.d.ts +0 -7
  190. package/build/cli/update.js +0 -117
  191. package/build/cli/updates.d.ts +0 -10
  192. package/build/cli/updates.js +0 -45
  193. package/build/cli/validate.d.ts +0 -4
  194. package/build/cli/validate.js +0 -19
  195. package/build/client/Link.d.ts +0 -8
  196. package/build/client/Link.js +0 -44
  197. package/build/client/NavLink.d.ts +0 -14
  198. package/build/client/NavLink.js +0 -37
  199. package/build/client/Router.d.ts +0 -7
  200. package/build/client/Router.js +0 -55
  201. package/build/client/channel.d.ts +0 -23
  202. package/build/client/channel.js +0 -94
  203. package/build/client/error-boundary.d.ts +0 -16
  204. package/build/client/error-boundary.js +0 -19
  205. package/build/client/head.d.ts +0 -26
  206. package/build/client/head.js +0 -87
  207. package/build/client/hooks.d.ts +0 -17
  208. package/build/client/hooks.js +0 -48
  209. package/build/client/lazy.d.ts +0 -16
  210. package/build/client/lazy.js +0 -53
  211. package/build/client/match.d.ts +0 -2
  212. package/build/client/match.js +0 -32
  213. package/build/client/mount.d.ts +0 -2
  214. package/build/client/mount.js +0 -13
  215. package/build/client/navigation.d.ts +0 -13
  216. package/build/client/navigation.js +0 -97
  217. package/build/client/params-context.d.ts +0 -2
  218. package/build/client/params-context.js +0 -2
  219. package/build/client/prefetch.d.ts +0 -11
  220. package/build/client/prefetch.js +0 -100
  221. package/build/client/runtime.d.ts +0 -31
  222. package/build/client/runtime.js +0 -112
  223. package/build/client/scroll.d.ts +0 -8
  224. package/build/client/scroll.js +0 -36
  225. package/toil-env.d.ts +0 -16
@@ -1,122 +1,122 @@
1
- /**
2
- * Mutations (writes), the counterpart to loaders (reads). A loader fetches data on navigation;
3
- * an action performs a write (save, delete, a server/WASM call) on demand, then revalidates the
4
- * affected loader data so the UI reflects the change. `useAction` tracks pending/error/result state;
5
- * `<Form>` is sugar over it for the form case.
6
- */
7
- import { useCallback, useEffect, useRef, useState } from 'react';
8
-
9
- import { invalidateLoaderData } from './loader.js';
10
- import { refresh } from '../navigation/navigation.js';
11
- import type { Href } from '../types.js';
12
-
13
- /**
14
- * Which loader data to refetch after an action succeeds:
15
- * - `true` (default), the current route.
16
- * - an `Href` (or array), those specific routes.
17
- * - `false`, nothing.
18
- */
19
- export type RevalidateTarget = boolean | Href | readonly Href[];
20
-
21
- /** Options for {@link useAction}. */
22
- export interface UseActionOptions<TData> {
23
- /** Loader data to revalidate after success. Default `true` (the current route). */
24
- readonly revalidate?: RevalidateTarget;
25
- /** Called after a successful run, with the action's return value. */
26
- readonly onSuccess?: (data: TData) => void;
27
- /** Called when the action throws. */
28
- readonly onError?: (error: unknown) => void;
29
- }
30
-
31
- /** Live state of an action. */
32
- export interface ActionState<TData> {
33
- /** True while a run is in flight. */
34
- readonly pending: boolean;
35
- /** The error from the last failed run, or `undefined`. */
36
- readonly error: unknown;
37
- /** The value returned by the last successful run, or `undefined`. */
38
- readonly data: TData | undefined;
39
- }
40
-
41
- /** Handle returned by {@link useAction}: current state plus `run` / `reset`. */
42
- export interface ActionHandle<TInput, TData> extends ActionState<TData> {
43
- /**
44
- * Run the action. Resolves to the result on success, or `undefined` if it threw (the error is
45
- * captured in `error` instead of rejecting, so a fire-and-forget `onClick` can't leak an
46
- * unhandled rejection).
47
- */
48
- run: (input: TInput) => Promise<TData | undefined>;
49
- /** Reset back to idle (clears `pending` / `error` / `data`). */
50
- reset: () => void;
51
- }
52
-
53
- /** Refetches loader data per a {@link RevalidateTarget}, then re-renders once. */
54
- function applyRevalidate(target: RevalidateTarget | undefined): void {
55
- if (target === false) return;
56
- if (target === undefined || target === true) {
57
- invalidateLoaderData();
58
- } else {
59
- const hrefs = typeof target === 'string' ? [target] : target;
60
- for (const href of hrefs) invalidateLoaderData(href);
61
- }
62
- refresh();
63
- }
64
-
65
- /**
66
- * Runs a mutation with pending/error/result tracking, revalidating loader data on success. Example:
67
- *
68
- * ```ts
69
- * const save = useAction((title: string) => api.save(title), { revalidate: true });
70
- * <button disabled={save.pending} onClick={() => void save.run(title)}>Save</button>
71
- * ```
72
- */
73
- export function useAction<TInput = void, TData = unknown>(
74
- fn: (input: TInput) => TData | Promise<TData>,
75
- options: UseActionOptions<TData> = {},
76
- ): ActionHandle<TInput, TData> {
77
- const [state, setState] = useState<ActionState<TData>>({
78
- pending: false,
79
- error: undefined,
80
- data: undefined,
81
- });
82
-
83
- // Hold the latest fn/options so `run` keeps a stable identity across renders.
84
- const latest = useRef({ fn, options });
85
- latest.current = { fn, options };
86
- const runId = useRef(0);
87
- const mounted = useRef(true);
88
- useEffect(
89
- () => () => {
90
- mounted.current = false;
91
- },
92
- [],
93
- );
94
-
95
- const run = useCallback(async (input: TInput): Promise<TData | undefined> => {
96
- const id = ++runId.current;
97
- setState((s) => ({ ...s, pending: true, error: undefined }));
98
- try {
99
- const data = await latest.current.fn(input);
100
- // Ignore a stale run that a newer one (or unmount) has superseded.
101
- if (mounted.current && id === runId.current) {
102
- setState({ pending: false, error: undefined, data });
103
- }
104
- applyRevalidate(latest.current.options.revalidate);
105
- latest.current.options.onSuccess?.(data);
106
- return data;
107
- } catch (error) {
108
- if (mounted.current && id === runId.current) {
109
- setState({ pending: false, error, data: undefined });
110
- }
111
- latest.current.options.onError?.(error);
112
- return undefined;
113
- }
114
- }, []);
115
-
116
- const reset = useCallback(() => {
117
- runId.current += 1;
118
- setState({ pending: false, error: undefined, data: undefined });
119
- }, []);
120
-
121
- return { ...state, run, reset };
122
- }
1
+ /**
2
+ * Mutations (writes), the counterpart to loaders (reads). A loader fetches data on navigation;
3
+ * an action performs a write (save, delete, a server/WASM call) on demand, then revalidates the
4
+ * affected loader data so the UI reflects the change. `useAction` tracks pending/error/result state;
5
+ * `<Form>` is sugar over it for the form case.
6
+ */
7
+ import { useCallback, useEffect, useRef, useState } from 'react';
8
+
9
+ import { invalidateLoaderData } from './loader.js';
10
+ import { refresh } from '../navigation/navigation.js';
11
+ import type { Href } from '../types.js';
12
+
13
+ /**
14
+ * Which loader data to refetch after an action succeeds:
15
+ * - `true` (default), the current route.
16
+ * - an `Href` (or array), those specific routes.
17
+ * - `false`, nothing.
18
+ */
19
+ export type RevalidateTarget = boolean | Href | readonly Href[];
20
+
21
+ /** Options for {@link useAction}. */
22
+ export interface UseActionOptions<TData> {
23
+ /** Loader data to revalidate after success. Default `true` (the current route). */
24
+ readonly revalidate?: RevalidateTarget;
25
+ /** Called after a successful run, with the action's return value. */
26
+ readonly onSuccess?: (data: TData) => void;
27
+ /** Called when the action throws. */
28
+ readonly onError?: (error: unknown) => void;
29
+ }
30
+
31
+ /** Live state of an action. */
32
+ export interface ActionState<TData> {
33
+ /** True while a run is in flight. */
34
+ readonly pending: boolean;
35
+ /** The error from the last failed run, or `undefined`. */
36
+ readonly error: unknown;
37
+ /** The value returned by the last successful run, or `undefined`. */
38
+ readonly data: TData | undefined;
39
+ }
40
+
41
+ /** Handle returned by {@link useAction}: current state plus `run` / `reset`. */
42
+ export interface ActionHandle<TInput, TData> extends ActionState<TData> {
43
+ /**
44
+ * Run the action. Resolves to the result on success, or `undefined` if it threw (the error is
45
+ * captured in `error` instead of rejecting, so a fire-and-forget `onClick` can't leak an
46
+ * unhandled rejection).
47
+ */
48
+ run: (input: TInput) => Promise<TData | undefined>;
49
+ /** Reset back to idle (clears `pending` / `error` / `data`). */
50
+ reset: () => void;
51
+ }
52
+
53
+ /** Refetches loader data per a {@link RevalidateTarget}, then re-renders once. */
54
+ function applyRevalidate(target: RevalidateTarget | undefined): void {
55
+ if (target === false) return;
56
+ if (target === undefined || target === true) {
57
+ invalidateLoaderData();
58
+ } else {
59
+ const hrefs = typeof target === 'string' ? [target] : target;
60
+ for (const href of hrefs) invalidateLoaderData(href);
61
+ }
62
+ refresh();
63
+ }
64
+
65
+ /**
66
+ * Runs a mutation with pending/error/result tracking, revalidating loader data on success. Example:
67
+ *
68
+ * ```ts
69
+ * const save = useAction((title: string) => api.save(title), { revalidate: true });
70
+ * <button disabled={save.pending} onClick={() => void save.run(title)}>Save</button>
71
+ * ```
72
+ */
73
+ export function useAction<TInput = void, TData = unknown>(
74
+ fn: (input: TInput) => TData | Promise<TData>,
75
+ options: UseActionOptions<TData> = {},
76
+ ): ActionHandle<TInput, TData> {
77
+ const [state, setState] = useState<ActionState<TData>>({
78
+ pending: false,
79
+ error: undefined,
80
+ data: undefined,
81
+ });
82
+
83
+ // Hold the latest fn/options so `run` keeps a stable identity across renders.
84
+ const latest = useRef({ fn, options });
85
+ latest.current = { fn, options };
86
+ const runId = useRef(0);
87
+ const mounted = useRef(true);
88
+ useEffect(
89
+ () => () => {
90
+ mounted.current = false;
91
+ },
92
+ [],
93
+ );
94
+
95
+ const run = useCallback(async (input: TInput): Promise<TData | undefined> => {
96
+ const id = ++runId.current;
97
+ setState((s) => ({ ...s, pending: true, error: undefined }));
98
+ try {
99
+ const data = await latest.current.fn(input);
100
+ // Ignore a stale run that a newer one (or unmount) has superseded.
101
+ if (mounted.current && id === runId.current) {
102
+ setState({ pending: false, error: undefined, data });
103
+ }
104
+ applyRevalidate(latest.current.options.revalidate);
105
+ latest.current.options.onSuccess?.(data);
106
+ return data;
107
+ } catch (error) {
108
+ if (mounted.current && id === runId.current) {
109
+ setState({ pending: false, error, data: undefined });
110
+ }
111
+ latest.current.options.onError?.(error);
112
+ return undefined;
113
+ }
114
+ }, []);
115
+
116
+ const reset = useCallback(() => {
117
+ runId.current += 1;
118
+ setState({ pending: false, error: undefined, data: undefined });
119
+ }, []);
120
+
121
+ return { ...state, run, reset };
122
+ }
@@ -1,43 +1,43 @@
1
- import { Component, Suspense, type ComponentType, type ReactNode } from 'react';
2
-
3
- import type { RouteErrorProps } from '../types.js';
4
-
5
- interface ErrorBoundaryProps {
6
- readonly fallback: ComponentType<RouteErrorProps>;
7
- readonly children: ReactNode;
8
- }
9
- interface ErrorBoundaryState {
10
- readonly error: Error | null;
11
- }
12
-
13
- /**
14
- * Catches render errors in its subtree and shows the route's `error.tsx` (with a `reset` to retry).
15
- * Error boundaries must be class components, React has no hook equivalent.
16
- */
17
- export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
18
- state: ErrorBoundaryState = { error: null };
19
-
20
- static getDerivedStateFromError(error: Error): ErrorBoundaryState {
21
- return { error };
22
- }
23
-
24
- reset = (): void => {
25
- this.setState({ error: null });
26
- };
27
-
28
- render(): ReactNode {
29
- const { error } = this.state;
30
- if (error) {
31
- const Fallback = this.props.fallback;
32
- return (
33
- <Suspense fallback={null}>
34
- <Fallback
35
- error={error}
36
- reset={this.reset}
37
- />
38
- </Suspense>
39
- );
40
- }
41
- return this.props.children;
42
- }
43
- }
1
+ import { Component, Suspense, type ComponentType, type ReactNode } from 'react';
2
+
3
+ import type { RouteErrorProps } from '../types.js';
4
+
5
+ interface ErrorBoundaryProps {
6
+ readonly fallback: ComponentType<RouteErrorProps>;
7
+ readonly children: ReactNode;
8
+ }
9
+ interface ErrorBoundaryState {
10
+ readonly error: Error | null;
11
+ }
12
+
13
+ /**
14
+ * Catches render errors in its subtree and shows the route's `error.tsx` (with a `reset` to retry).
15
+ * Error boundaries must be class components, React has no hook equivalent.
16
+ */
17
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
18
+ state: ErrorBoundaryState = { error: null };
19
+
20
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
21
+ return { error };
22
+ }
23
+
24
+ reset = (): void => {
25
+ this.setState({ error: null });
26
+ };
27
+
28
+ render(): ReactNode {
29
+ const { error } = this.state;
30
+ if (error) {
31
+ const Fallback = this.props.fallback;
32
+ return (
33
+ <Suspense fallback={null}>
34
+ <Fallback
35
+ error={error}
36
+ reset={this.reset}
37
+ />
38
+ </Suspense>
39
+ );
40
+ }
41
+ return this.props.children;
42
+ }
43
+ }
@@ -2,7 +2,14 @@
2
2
  * Router hooks for user route components: read the params / pathname / search params, navigate
3
3
  * imperatively, and grab a router handle.
4
4
  */
5
- import { useContext, useEffect, useMemo, useReducer, useSyncExternalStore } from 'react';
5
+ import {
6
+ startTransition,
7
+ useContext,
8
+ useEffect,
9
+ useMemo,
10
+ useReducer,
11
+ useSyncExternalStore,
12
+ } from 'react';
6
13
 
7
14
  import type { RouteParams } from './match.js';
8
15
  import {
@@ -76,14 +83,22 @@ export function useRouter(): RouterInstance {
76
83
  }
77
84
 
78
85
  /**
79
- * Subscribes to location changes and reads the live `window.location` on render. Re-renders on any
80
- * pathname, search, or hash change. The re-render is orchestrated by `navigate`/`notify` (wrapped in
81
- * `startTransition` for smooth nav, or `document.startViewTransition` when enabled), so the listener
82
- * itself is a plain force-update.
86
+ * Subscribes to location changes and reads the live `window.location` on render. The re-render runs
87
+ * inside `startTransition` so React keeps the current page visible while the next route's chunk and
88
+ * loader resolve (committing the new tree only once it's ready), instead of committing a suspended
89
+ * tree and flashing an empty page on every link click.
83
90
  */
84
91
  function useLocationSubscription(): void {
85
92
  const [, forceUpdate] = useReducer((n: number): number => n + 1, 0);
86
- useEffect(() => subscribeLocation(forceUpdate), []);
93
+ useEffect(
94
+ () =>
95
+ subscribeLocation(() => {
96
+ startTransition(() => {
97
+ forceUpdate();
98
+ });
99
+ }),
100
+ [],
101
+ );
87
102
  }
88
103
 
89
104
  /** Subscribes to and returns the current `location.pathname`. */