toiljs 0.0.15 → 0.0.19

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 (273) 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 +116 -5
  14. package/LICENSE +187 -187
  15. package/README.md +524 -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/backend/index.d.ts +1 -0
  21. package/build/backend/index.js +20 -1
  22. package/build/cli/.tsbuildinfo +1 -1
  23. package/build/cli/index.js +1320 -696
  24. package/build/client/.tsbuildinfo +1 -1
  25. package/build/client/dev/devtools.d.ts +6 -0
  26. package/build/client/dev/devtools.js +479 -0
  27. package/build/client/dev/error-overlay.d.ts +9 -0
  28. package/build/client/dev/error-overlay.js +19 -4
  29. package/build/client/errors.d.ts +1 -0
  30. package/build/client/errors.js +3 -0
  31. package/build/client/index.d.ts +2 -0
  32. package/build/client/index.js +2 -0
  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 +23 -0
  38. package/build/client/routing/loader.js +53 -7
  39. package/build/client/routing/mount.js +4 -3
  40. package/build/client/rpc.d.ts +1 -0
  41. package/build/client/rpc.js +37 -0
  42. package/build/compiler/.tsbuildinfo +1 -1
  43. package/build/compiler/config.d.ts +16 -0
  44. package/build/compiler/config.js +9 -0
  45. package/build/compiler/docs.js +78 -21
  46. package/build/compiler/generate.js +5 -4
  47. package/build/compiler/index.d.ts +3 -2
  48. package/build/compiler/index.js +2 -2
  49. package/build/compiler/plugin.js +228 -0
  50. package/build/compiler/prerender.d.ts +1 -0
  51. package/build/compiler/prerender.js +1 -1
  52. package/build/compiler/seo.d.ts +1 -1
  53. package/build/compiler/seo.js +20 -5
  54. package/build/compiler/ssg.js +39 -2
  55. package/build/compiler/vite.js +25 -0
  56. package/build/io/.tsbuildinfo +1 -1
  57. package/build/io/codec.d.ts +54 -0
  58. package/build/io/codec.js +143 -0
  59. package/build/io/index.d.ts +1 -2
  60. package/build/io/index.js +1 -2
  61. package/build/logger/.tsbuildinfo +1 -1
  62. package/build/shared/.tsbuildinfo +1 -1
  63. package/eslint.config.js +48 -48
  64. package/examples/basic/client/404.tsx +11 -11
  65. package/examples/basic/client/components/.gitkeep +1 -1
  66. package/examples/basic/client/global-error.tsx +13 -13
  67. package/examples/basic/client/layout.tsx +25 -25
  68. package/examples/basic/client/public/images/.gitkeep +1 -1
  69. package/examples/basic/client/public/images/logo.svg +36 -36
  70. package/examples/basic/client/public/robots.txt +2 -2
  71. package/examples/basic/client/routes/docs/[...slug].tsx +12 -12
  72. package/examples/basic/client/routes/features/error/error.tsx +16 -16
  73. package/examples/basic/client/routes/features/index.tsx +1 -1
  74. package/examples/basic/client/routes/features/template/b.tsx +14 -14
  75. package/examples/basic/client/routes/files/[[...slug]].tsx +21 -21
  76. package/examples/basic/client/routes/gallery/layout.tsx +13 -13
  77. package/examples/basic/client/routes/io.tsx +23 -24
  78. package/examples/basic/client/routes/loader-demo/loading.tsx +13 -13
  79. package/examples/basic/client/routes/rest.tsx +74 -0
  80. package/examples/basic/client/routes/rpc.tsx +43 -0
  81. package/examples/basic/client/routes/search.tsx +61 -61
  82. package/examples/basic/client/toil.tsx +5 -5
  83. package/package.json +167 -148
  84. package/presets/eslint.js +88 -88
  85. package/presets/no-uint8array-tostring.js +200 -200
  86. package/presets/prettier-plugin.js +51 -0
  87. package/presets/prettier.json +19 -18
  88. package/presets/tsconfig.json +37 -37
  89. package/server/runtime/README.md +97 -0
  90. package/server/runtime/abort/abort.ts +27 -0
  91. package/server/runtime/env/Server.ts +61 -0
  92. package/server/runtime/envelope.ts +191 -0
  93. package/server/runtime/exports/index.ts +52 -0
  94. package/server/runtime/handlers/ToilHandler.ts +34 -0
  95. package/server/runtime/index.ts +26 -0
  96. package/server/runtime/lang/Potential.ts +5 -0
  97. package/server/runtime/memory.ts +81 -0
  98. package/server/runtime/request.ts +55 -0
  99. package/server/runtime/response.ts +86 -0
  100. package/server/runtime/rest/Rest.ts +39 -0
  101. package/server/runtime/rest/RestHandler.ts +20 -0
  102. package/server/runtime/rest/RouteContext.ts +82 -0
  103. package/server/runtime/rest/match.ts +48 -0
  104. package/server/runtime/tsconfig.json +7 -0
  105. package/src/backend/index.ts +202 -160
  106. package/src/cli/create.ts +15 -5
  107. package/src/cli/diagnostics.ts +81 -0
  108. package/src/cli/doctor.ts +384 -7
  109. package/src/cli/index.ts +11 -2
  110. package/src/cli/proc.ts +50 -50
  111. package/src/cli/updates.ts +69 -69
  112. package/src/cli/validate.ts +31 -31
  113. package/src/client/channel/channel.ts +146 -146
  114. package/src/client/components/Form.tsx +65 -65
  115. package/src/client/components/Script.tsx +113 -113
  116. package/src/client/components/Slot.tsx +21 -21
  117. package/src/client/dev/devtools.tsx +1018 -0
  118. package/src/client/dev/error-overlay.tsx +30 -4
  119. package/src/client/errors.ts +11 -0
  120. package/src/client/head/head.ts +167 -167
  121. package/src/client/head/metadata.ts +112 -112
  122. package/src/client/index.ts +91 -89
  123. package/src/client/navigation/NavLink.tsx +86 -86
  124. package/src/client/navigation/navigation.ts +235 -235
  125. package/src/client/navigation/prefetch.ts +169 -130
  126. package/src/client/navigation/scroll.ts +53 -53
  127. package/src/client/routing/Router.tsx +8 -2
  128. package/src/client/routing/action.ts +122 -122
  129. package/src/client/routing/error-boundary.tsx +43 -43
  130. package/src/client/routing/hooks.ts +21 -6
  131. package/src/client/routing/loader.ts +325 -235
  132. package/src/client/routing/match.ts +47 -47
  133. package/src/client/routing/mount.tsx +54 -52
  134. package/src/client/routing/params-context.ts +10 -10
  135. package/src/client/routing/slot-context.ts +7 -7
  136. package/src/client/rpc.ts +64 -0
  137. package/src/client/search/search.ts +189 -189
  138. package/src/client/search/use-page-search.ts +73 -73
  139. package/src/client/types.ts +73 -73
  140. package/src/compiler/config.ts +221 -182
  141. package/src/compiler/docs.ts +285 -228
  142. package/src/compiler/generate.ts +395 -394
  143. package/src/compiler/index.ts +66 -57
  144. package/src/compiler/pages.ts +70 -70
  145. package/src/compiler/plugin.ts +258 -2
  146. package/src/compiler/prerender.ts +156 -156
  147. package/src/compiler/seo.ts +417 -390
  148. package/src/compiler/ssg.ts +171 -126
  149. package/src/compiler/vite.ts +34 -0
  150. package/src/io/FastMap.ts +151 -127
  151. package/src/io/FastSet.ts +15 -1
  152. package/src/io/codec.ts +217 -0
  153. package/src/io/index.ts +10 -11
  154. package/src/io/lengths.ts +14 -14
  155. package/src/io/types.ts +19 -18
  156. package/src/logger/index.ts +22 -22
  157. package/src/shared/index.ts +10 -10
  158. package/std/client/index.d.ts +15 -15
  159. package/std/client/package.json +3 -3
  160. package/test/assembly/example.spec.ts +17 -7
  161. package/test/channel.test.ts +21 -21
  162. package/test/doctor.test.ts +65 -0
  163. package/test/dom/Link.test.tsx +47 -47
  164. package/test/dom/NavLink.test.tsx +37 -37
  165. package/test/dom/error-overlay.test.tsx +44 -44
  166. package/test/dom/loader.test.tsx +121 -121
  167. package/test/dom/navigation.test.ts +59 -59
  168. package/test/dom/revalidate.test.tsx +38 -38
  169. package/test/dom/route-head.test.tsx +78 -78
  170. package/test/dom/router-loading.test.tsx +44 -44
  171. package/test/dom/scroll.test.ts +56 -56
  172. package/test/dom/use-metadata.test.tsx +58 -58
  173. package/test/errors.test.ts +21 -0
  174. package/test/io.test.ts +117 -93
  175. package/test/navlink.test.ts +28 -28
  176. package/test/placeholder.test.ts +9 -9
  177. package/test/prettier-plugin.test.ts +46 -0
  178. package/test/routes.test.ts +76 -76
  179. package/test/rpc.test.ts +50 -0
  180. package/test/seo.test.ts +175 -164
  181. package/test/slot-layouts.test.ts +69 -69
  182. package/test/ssg.test.ts +36 -36
  183. package/test/update.test.ts +44 -44
  184. package/test/validate.test.ts +42 -42
  185. package/tests/data-parity/generated-parity.ts +99 -0
  186. package/tests/data-parity/parity.ts +80 -0
  187. package/tests/data-parity/spec.ts +46 -0
  188. package/toil-routes.d.ts +7 -0
  189. package/tsconfig.backend.json +13 -13
  190. package/tsconfig.base.json +35 -35
  191. package/tsconfig.cli.json +13 -13
  192. package/tsconfig.client.json +14 -14
  193. package/tsconfig.compiler.json +13 -13
  194. package/tsconfig.io.json +12 -12
  195. package/tsconfig.json +22 -22
  196. package/tsconfig.logger.json +12 -12
  197. package/tsconfig.server.json +10 -10
  198. package/tsconfig.shared.json +12 -12
  199. package/vitest.config.ts +26 -26
  200. package/.idea/codeStyles/Project.xml +0 -54
  201. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  202. package/.idea/inspectionProfiles/Project_Default.xml +0 -6
  203. package/.idea/modules.xml +0 -8
  204. package/.idea/prettier.xml +0 -7
  205. package/.idea/toiljs.iml +0 -8
  206. package/.idea/vcs.xml +0 -6
  207. package/.toil/entry.tsx +0 -9
  208. package/.toil/index.html +0 -12
  209. package/.toil/routes.ts +0 -9
  210. package/build/cli/configure.d.ts +0 -16
  211. package/build/cli/configure.js +0 -272
  212. package/build/cli/create.d.ts +0 -16
  213. package/build/cli/create.js +0 -420
  214. package/build/cli/diagnostics.d.ts +0 -55
  215. package/build/cli/diagnostics.js +0 -333
  216. package/build/cli/doctor.d.ts +0 -6
  217. package/build/cli/doctor.js +0 -249
  218. package/build/cli/features.d.ts +0 -25
  219. package/build/cli/features.js +0 -107
  220. package/build/cli/index.d.ts +0 -2
  221. package/build/cli/proc.d.ts +0 -6
  222. package/build/cli/proc.js +0 -31
  223. package/build/cli/ui.d.ts +0 -9
  224. package/build/cli/ui.js +0 -75
  225. package/build/cli/update.d.ts +0 -7
  226. package/build/cli/update.js +0 -117
  227. package/build/cli/updates.d.ts +0 -10
  228. package/build/cli/updates.js +0 -45
  229. package/build/cli/validate.d.ts +0 -4
  230. package/build/cli/validate.js +0 -19
  231. package/build/client/Link.d.ts +0 -8
  232. package/build/client/Link.js +0 -44
  233. package/build/client/NavLink.d.ts +0 -14
  234. package/build/client/NavLink.js +0 -37
  235. package/build/client/Router.d.ts +0 -7
  236. package/build/client/Router.js +0 -55
  237. package/build/client/channel.d.ts +0 -23
  238. package/build/client/channel.js +0 -94
  239. package/build/client/error-boundary.d.ts +0 -16
  240. package/build/client/error-boundary.js +0 -19
  241. package/build/client/head.d.ts +0 -26
  242. package/build/client/head.js +0 -87
  243. package/build/client/hooks.d.ts +0 -17
  244. package/build/client/hooks.js +0 -48
  245. package/build/client/lazy.d.ts +0 -16
  246. package/build/client/lazy.js +0 -53
  247. package/build/client/match.d.ts +0 -2
  248. package/build/client/match.js +0 -32
  249. package/build/client/mount.d.ts +0 -2
  250. package/build/client/mount.js +0 -13
  251. package/build/client/navigation.d.ts +0 -13
  252. package/build/client/navigation.js +0 -97
  253. package/build/client/params-context.d.ts +0 -2
  254. package/build/client/params-context.js +0 -2
  255. package/build/client/prefetch.d.ts +0 -11
  256. package/build/client/prefetch.js +0 -100
  257. package/build/client/runtime.d.ts +0 -31
  258. package/build/client/runtime.js +0 -112
  259. package/build/client/scroll.d.ts +0 -8
  260. package/build/client/scroll.js +0 -36
  261. package/build/io/BinaryReader.d.ts +0 -44
  262. package/build/io/BinaryReader.js +0 -244
  263. package/build/io/BinaryWriter.d.ts +0 -44
  264. package/build/io/BinaryWriter.js +0 -297
  265. package/build/server/release.wasm +0 -0
  266. package/build/server/release.wat +0 -9
  267. package/src/io/BinaryReader.ts +0 -340
  268. package/src/io/BinaryWriter.ts +0 -385
  269. package/src/server/index.ts +0 -10
  270. package/src/server/main.ts +0 -13
  271. package/src/server/tsconfig.json +0 -4
  272. package/toil-env.d.ts +0 -16
  273. package/toilconfig.json +0 -30
@@ -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`. */