@firtoz/router-toolkit 8.0.1 → 9.0.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 CHANGED
@@ -106,9 +106,15 @@ export default function Dashboard() {
106
106
 
107
107
  ### 3. Forms with Actions
108
108
 
109
+ `useDynamicSubmitter` returns a **stable** `{ submit, submitJson, Form, fetcherKey }` and does not expose `state` / `data`. Use `useDynamicSubmitterFetcher(submitter)` when you need reactive loading or action results in JSX (or `submitter.fetcherKey` with `useFetcher` for advanced cases).
110
+
109
111
  ```tsx
110
112
  // app/routes/create-user.tsx
111
- import { useDynamicSubmitter, type RoutePath } from '@firtoz/router-toolkit';
113
+ import {
114
+ useDynamicSubmitter,
115
+ useDynamicSubmitterFetcher,
116
+ type RoutePath,
117
+ } from '@firtoz/router-toolkit';
112
118
 
113
119
  export const route: RoutePath<"/create-user"> = "/create-user";
114
120
 
@@ -119,15 +125,17 @@ export async function action({ request }) {
119
125
  }
120
126
 
121
127
  export default function CreateUser() {
122
- const submitter = useDynamicSubmitter<typeof import("./create-user")>("/create-user");
123
-
128
+ const path = "/create-user" as const;
129
+ const submitter = useDynamicSubmitter<typeof import("./create-user")>(path);
130
+ const fetcher = useDynamicSubmitterFetcher(submitter);
131
+
124
132
  return (
125
133
  <submitter.Form method="post">
126
134
  <input name="name" placeholder="User name" required />
127
135
  <button type="submit">
128
- {submitter.state === "submitting" ? "Creating..." : "Create"}
136
+ {fetcher.state === "submitting" ? "Creating..." : "Create"}
129
137
  </button>
130
- {submitter.data?.success && <p>✅ User created!</p>}
138
+ {fetcher.data?.success && <p>✅ User created!</p>}
131
139
  </submitter.Form>
132
140
  );
133
141
  }
@@ -136,8 +144,8 @@ export default function CreateUser() {
136
144
  **Key Points:**
137
145
  - Export `route: RoutePath<"your-path">` in every route file
138
146
  - Use `useDynamicFetcher<typeof import("./route-file")>` for type-safe data fetching
139
- - Use `useDynamicSubmitter<typeof import("./route-file")>` for type-safe form submission
140
- - Full TypeScript inference for `fetcher.data` and `submitter.data`
147
+ - Use `useDynamicSubmitter<typeof import("./route-file")>` for type-safe `submit` / `submitJson` / `Form`; use `useDynamicSubmitterFetcher(submitter)` for reactive `state` / `data` on the same submission (optional `keySuffix` when two submitters share one URL)
148
+ - Await `submit` / `submitJson` for programmatic flows; full TypeScript inference on the resolved payload
141
149
 
142
150
  > **💡 Tip**: Start with `useDynamicFetcher` for data loading, then add `useDynamicSubmitter` for forms. The `useFetcherStateChanged` hook is great for notifications and side effects.
143
151
 
@@ -196,11 +204,15 @@ export default function UsersPage() {
196
204
 
197
205
  ### `useDynamicSubmitter`
198
206
 
199
- Type-safe form submission with Zod validation and enhanced submit functionality. Works seamlessly with route modules for full type inference.
207
+ Type-safe `submit`, `submitJson`, and `Form` with a **stable** return object (`fetcherKey` included). Use `await submitJson(...)` (or `await submit(...)`) for action payloads; use `useDynamicSubmitterFetcher(submitter)` when you need `fetcher.state` / `fetcher.data` in the UI.
200
208
 
201
209
  ```tsx
202
210
  // app/routes/contact.tsx
203
- import { useDynamicSubmitter, type RoutePath } from '@firtoz/router-toolkit';
211
+ import {
212
+ useDynamicSubmitter,
213
+ useDynamicSubmitterFetcher,
214
+ type RoutePath,
215
+ } from '@firtoz/router-toolkit';
204
216
  import { z } from 'zod';
205
217
  import type { Route } from './+types/contact';
206
218
 
@@ -234,9 +246,11 @@ export async function action({ request }: Route.ActionArgs) {
234
246
  };
235
247
  }
236
248
 
237
- // 4. Use the hook with typeof import for full type inference
249
+ // 4. Submitter + matching fetcher for reactive UI
238
250
  export default function ContactForm() {
239
- const submitter = useDynamicSubmitter<typeof import("./contact")>("/contact");
251
+ const path = "/contact" as const;
252
+ const submitter = useDynamicSubmitter<typeof import("./contact")>(path);
253
+ const fetcher = useDynamicSubmitterFetcher(submitter);
240
254
 
241
255
  return (
242
256
  <div>
@@ -263,18 +277,18 @@ export default function ContactForm() {
263
277
 
264
278
  <button
265
279
  type="submit"
266
- disabled={submitter.state === "submitting"}
280
+ disabled={fetcher.state === "submitting"}
267
281
  >
268
- {submitter.state === "submitting" ? "Submitting..." : "Submit"}
282
+ {fetcher.state === "submitting" ? "Submitting..." : "Submit"}
269
283
  </button>
270
284
  </submitter.Form>
271
285
 
272
- {submitter.data && (
286
+ {fetcher.data && (
273
287
  <div>
274
- {submitter.data.success ? (
275
- <p>✅ {submitter.data.message}</p>
288
+ {fetcher.data.success ? (
289
+ <p>✅ {fetcher.data.message}</p>
276
290
  ) : (
277
- <p>❌ {submitter.data.message}</p>
291
+ <p>❌ {fetcher.data.message}</p>
278
292
  )}
279
293
  </div>
280
294
  )}
@@ -283,6 +297,22 @@ export default function ContactForm() {
283
297
  }
284
298
  ```
285
299
 
300
+ #### Local `useState` vs `useDynamicSubmitterFetcher`
301
+
302
+ Both are valid; pick based on how you want loading and action feedback to show up.
303
+
304
+ **Local state around `await` (e.g. `saving` + `try` / `finally`)**
305
+
306
+ - **Pros:** Matches the promise-first API; no extra hook; the pending flag tracks exactly your async handler (including extra `await`s in the same function); easy to reason about when you reload data after save (e.g. `useDynamicFetcher.load()`).
307
+ - **Cons:** You must remember `finally` (or equivalent); one flag is awkward if several overlapping operations need distinct UX; you do not get declarative `fetcher.data` for the action unless you store the awaited value yourself.
308
+
309
+ **`useDynamicSubmitterFetcher(submitter)`**
310
+
311
+ - **Pros:** Declarative `fetcher.state` / `fetcher.data` / `fetcher.error` in JSX—good for `<submitter.Form>`, inline validation or handler errors, and staying aligned with React Router’s fetcher lifecycle on `submitter.fetcherKey`.
312
+ - **Cons:** Second `useFetcher` subscription; UI follows RR’s state machine, not only “my handler,” unless you isolate keys with `keySuffix` when two widgets share one URL.
313
+
314
+ **Rule of thumb:** Use **local pending state** when the flow is “run this async function, disable until it finishes.” Use **`useDynamicSubmitterFetcher`** when you want **reactive** fetcher fields in render without mirroring them into state.
315
+
286
316
  ### `ConcurrentSubmitterProvider` + `useConcurrentSubmitter`
287
317
 
288
318
  Run multiple submissions in parallel via the framework fetcher; each is tracked in `operations` with `submittedData` (for optimistic UI) and `data` when done. Wrap your app (or subtree) with `ConcurrentSubmitterProvider`, then use `useConcurrentSubmitter<TInfo>()` for typed `submitJson` / `submitFormData` with path and args per call.
@@ -345,7 +375,12 @@ Track changes in fetcher state and react to them. Perfect for triggering side ef
345
375
 
346
376
  ```tsx
347
377
  // app/routes/notification-form.tsx
348
- import { useDynamicSubmitter, useFetcherStateChanged, type RoutePath } from '@firtoz/router-toolkit';
378
+ import {
379
+ useDynamicSubmitter,
380
+ useDynamicSubmitterFetcher,
381
+ useFetcherStateChanged,
382
+ type RoutePath,
383
+ } from '@firtoz/router-toolkit';
349
384
  import { useState } from 'react';
350
385
  import { z } from 'zod';
351
386
  import type { Route } from './+types/notification-form';
@@ -373,17 +408,19 @@ export async function action({ request }: Route.ActionArgs) {
373
408
  }
374
409
 
375
410
  export default function NotificationForm() {
376
- const submitter = useDynamicSubmitter<typeof import("./notification-form")>("/notification-form");
411
+ const path = "/notification-form" as const;
412
+ const submitter = useDynamicSubmitter<typeof import("./notification-form")>(path);
413
+ const fetcher = useDynamicSubmitterFetcher(submitter);
377
414
  const [notifications, setNotifications] = useState<string[]>([]);
378
415
 
379
- // Track fetcher state changes for side effects
380
- useFetcherStateChanged(submitter, (lastState, newState) => {
416
+ // Track fetcher state changes for side effects (pass the parallel fetcher, not submitter)
417
+ useFetcherStateChanged(fetcher, (lastState, newState) => {
381
418
  console.log(`Fetcher state changed from ${lastState} to ${newState}`);
382
419
 
383
420
  // Show success notification when form submission completes
384
421
  if (newState === 'idle' && lastState === 'submitting') {
385
- if (submitter.data?.success) {
386
- setNotifications(prev => [...prev, `✅ ${submitter.data.message}`]);
422
+ if (fetcher.data?.success) {
423
+ setNotifications(prev => [...prev, `✅ ${fetcher.data.message}`]);
387
424
  } else {
388
425
  setNotifications(prev => [...prev, `❌ Submission failed`]);
389
426
  }
@@ -421,12 +458,12 @@ export default function NotificationForm() {
421
458
 
422
459
  <button
423
460
  type="submit"
424
- disabled={submitter.state === 'submitting'}
461
+ disabled={fetcher.state === 'submitting'}
425
462
  >
426
- {submitter.state === 'submitting' ? 'Sending...' : 'Send Notification'}
463
+ {fetcher.state === 'submitting' ? 'Sending...' : 'Send Notification'}
427
464
  </button>
428
465
 
429
- <p>Current state: <strong>{submitter.state}</strong></p>
466
+ <p>Current state: <strong>{fetcher.state}</strong></p>
430
467
  </submitter.Form>
431
468
 
432
469
  {/* Show notifications triggered by state changes */}
@@ -520,18 +557,23 @@ The `formAction` utility works seamlessly with `useDynamicSubmitter` when you ex
520
557
 
521
558
  ```tsx
522
559
  // app/routes/register.tsx (component)
523
- import { useDynamicSubmitter } from "@firtoz/router-toolkit";
560
+ import {
561
+ useDynamicSubmitter,
562
+ useDynamicSubmitterFetcher,
563
+ } from "@firtoz/router-toolkit";
524
564
 
525
565
  export default function Register() {
526
- const submitter = useDynamicSubmitter<typeof import("./register")>("/register");
566
+ const path = "/register" as const;
567
+ const submitter = useDynamicSubmitter<typeof import("./register")>(path);
568
+ const fetcher = useDynamicSubmitterFetcher(submitter);
527
569
 
528
570
  return (
529
571
  <submitter.Form method="post">
530
572
  <input name="email" type="email" required />
531
573
  <input name="password" type="password" required />
532
574
  <input name="confirmPassword" type="password" required />
533
- <button type="submit" disabled={submitter.state === "submitting"}>
534
- {submitter.state === "submitting" ? "Registering..." : "Register"}
575
+ <button type="submit" disabled={fetcher.state === "submitting"}>
576
+ {fetcher.state === "submitting" ? "Registering..." : "Register"}
535
577
  </button>
536
578
  </submitter.Form>
537
579
  );
@@ -540,26 +582,25 @@ export default function Register() {
540
582
 
541
583
  #### Error Handling
542
584
 
543
- The `formAction` utility returns structured errors that you can handle in your components:
585
+ The `formAction` utility returns structured errors. Inspect `fetcher.data` from `useDynamicSubmitterFetcher(submitter)`, or `await submitter.submitJson(...)` in an async handler:
544
586
 
545
587
  ```tsx
546
588
  export default function Register() {
547
- const submitter = useDynamicSubmitter<typeof import("./register")>("/register");
589
+ const path = "/register" as const;
590
+ const submitter = useDynamicSubmitter<typeof import("./register")>(path);
591
+ const fetcher = useDynamicSubmitterFetcher(submitter);
592
+
593
+ if (fetcher.data && !fetcher.data.success) {
594
+ const error = fetcher.data.error;
548
595
 
549
- if (submitter.data && !submitter.data.success) {
550
- const error = submitter.data.error;
551
-
552
596
  switch (error.type) {
553
597
  case "validation":
554
- // Handle Zod validation errors
555
598
  console.log("Validation errors:", error.error);
556
599
  break;
557
600
  case "handler":
558
- // Handle business logic errors
559
601
  console.log("Handler error:", error.error);
560
602
  break;
561
603
  case "unknown":
562
- // Handle unexpected errors
563
604
  console.log("Unknown error occurred");
564
605
  break;
565
606
  }
@@ -843,7 +884,11 @@ export default function LoaderTest() {
843
884
 
844
885
  ```tsx
845
886
  // app/routes/action-test.tsx
846
- import { useDynamicSubmitter, type RoutePath } from '@firtoz/router-toolkit';
887
+ import {
888
+ useDynamicSubmitter,
889
+ useDynamicSubmitterFetcher,
890
+ type RoutePath,
891
+ } from '@firtoz/router-toolkit';
847
892
  import { z } from 'zod';
848
893
  import type { Route } from './+types/action-test';
849
894
 
@@ -887,7 +932,9 @@ export async function action({ request }: Route.ActionArgs): Promise<ActionData>
887
932
  }
888
933
 
889
934
  export default function ActionTest() {
890
- const submitter = useDynamicSubmitter<typeof import("./action-test")>("/action-test");
935
+ const path = "/action-test" as const;
936
+ const submitter = useDynamicSubmitter<typeof import("./action-test")>(path);
937
+ const fetcher = useDynamicSubmitterFetcher(submitter);
891
938
 
892
939
  return (
893
940
  <div className="p-6">
@@ -923,27 +970,27 @@ export default function ActionTest() {
923
970
 
924
971
  <button
925
972
  type="submit"
926
- disabled={submitter.state === "submitting"}
973
+ disabled={fetcher.state === "submitting"}
927
974
  className="bg-green-500 text-white px-4 py-2 rounded disabled:opacity-50"
928
975
  >
929
- {submitter.state === "submitting" ? "Submitting..." : "Submit"}
976
+ {fetcher.state === "submitting" ? "Submitting..." : "Submit"}
930
977
  </button>
931
978
  </submitter.Form>
932
979
 
933
- {submitter.data && (
980
+ {fetcher.data && (
934
981
  <div className="mt-6">
935
982
  <h2 className="text-lg font-semibold mb-2">Action Result:</h2>
936
983
  <pre className="bg-gray-200 p-3 rounded text-sm text-gray-800">
937
- {JSON.stringify(submitter.data, null, 2)}
984
+ {JSON.stringify(fetcher.data, null, 2)}
938
985
  </pre>
939
986
 
940
- {submitter.data.success ? (
987
+ {fetcher.data.success ? (
941
988
  <div className="mt-4 p-3 bg-green-100 rounded">
942
- <p className="text-green-800">✅ {submitter.data.message}</p>
989
+ <p className="text-green-800">✅ {fetcher.data.message}</p>
943
990
  </div>
944
991
  ) : (
945
992
  <div className="mt-4 p-3 bg-red-100 rounded">
946
- <p className="text-red-800">❌ {submitter.data.message}</p>
993
+ <p className="text-red-800">❌ {fetcher.data.message}</p>
947
994
  </div>
948
995
  )}
949
996
  </div>
@@ -958,8 +1005,8 @@ export default function ActionTest() {
958
1005
  ```tsx
959
1006
  // app/routes/combined-test.tsx
960
1007
  import {
961
- useDynamicFetcher,
962
1008
  useDynamicSubmitter,
1009
+ useDynamicSubmitterFetcher,
963
1010
  type RoutePath,
964
1011
  } from '@firtoz/router-toolkit';
965
1012
  import { useLoaderData } from 'react-router';
@@ -1033,8 +1080,9 @@ export async function action({ request }: Route.ActionArgs): Promise<ActionData>
1033
1080
 
1034
1081
  export default function CombinedTest() {
1035
1082
  const loaderData = useLoaderData<LoaderData>();
1036
- const fetcher = useDynamicFetcher<typeof import("./combined-test")>("/combined-test");
1037
- const submitter = useDynamicSubmitter<typeof import("./combined-test")>("/combined-test");
1083
+ const path = "/combined-test" as const;
1084
+ const submitter = useDynamicSubmitter<typeof import("./combined-test")>(path);
1085
+ const actionFetcher = useDynamicSubmitterFetcher(submitter);
1038
1086
 
1039
1087
  return (
1040
1088
  <div className="p-6">
@@ -1087,10 +1135,10 @@ export default function CombinedTest() {
1087
1135
 
1088
1136
  <button
1089
1137
  type="submit"
1090
- disabled={submitter.state === "submitting"}
1138
+ disabled={actionFetcher.state === "submitting"}
1091
1139
  className="bg-purple-500 text-white px-4 py-2 rounded disabled:opacity-50"
1092
1140
  >
1093
- {submitter.state === "submitting" ? "Updating..." : "Update User"}
1141
+ {actionFetcher.state === "submitting" ? "Updating..." : "Update User"}
1094
1142
  </button>
1095
1143
  </submitter.Form>
1096
1144
  </div>
@@ -1100,21 +1148,21 @@ export default function CombinedTest() {
1100
1148
  <div className="mt-6">
1101
1149
  <h2 className="text-lg font-semibold mb-2">Action Status:</h2>
1102
1150
  <pre className="bg-gray-200 p-3 rounded text-sm text-gray-800">
1103
- {JSON.stringify({ state: submitter.state }, null, 2)}
1151
+ {JSON.stringify({ state: actionFetcher.state }, null, 2)}
1104
1152
  </pre>
1105
1153
  </div>
1106
1154
 
1107
- {submitter.data && (
1155
+ {actionFetcher.data && (
1108
1156
  <div className="mt-6">
1109
1157
  <h2 className="text-lg font-semibold mb-2">Action Result:</h2>
1110
1158
  <pre className="bg-gray-200 p-3 rounded text-sm text-gray-800">
1111
- {JSON.stringify(submitter.data, null, 2)}
1159
+ {JSON.stringify(actionFetcher.data, null, 2)}
1112
1160
  </pre>
1113
1161
 
1114
- {submitter.data.success ? (
1162
+ {actionFetcher.data.success ? (
1115
1163
  <div className="mt-4 p-3 bg-green-100 rounded">
1116
- <p className="text-green-800">✅ {submitter.data.message}</p>
1117
- {submitter.data.updatedUser && (
1164
+ <p className="text-green-800">✅ {actionFetcher.data.message}</p>
1165
+ {actionFetcher.data.updatedUser && (
1118
1166
  <p className="text-sm text-green-700 mt-1">
1119
1167
  Tip: Reload the page to see if data persists (it won't in this demo)
1120
1168
  </p>
@@ -1122,7 +1170,7 @@ export default function CombinedTest() {
1122
1170
  </div>
1123
1171
  ) : (
1124
1172
  <div className="mt-4 p-3 bg-red-100 rounded">
1125
- <p className="text-red-800">❌ {submitter.data.message}</p>
1173
+ <p className="text-red-800">❌ {actionFetcher.data.message}</p>
1126
1174
  </div>
1127
1175
  )}
1128
1176
  </div>
@@ -1134,12 +1182,12 @@ export default function CombinedTest() {
1134
1182
 
1135
1183
  ## MaybeError Utility
1136
1184
 
1137
- The router-toolkit includes the `@firtoz/maybe-error` package, which provides type-safe error handling utilities using discriminated unions. This is perfect for handling operations that may fail in your route loaders and actions.
1185
+ `@firtoz/maybe-error` is a **dependency** of router-toolkit (it is installed with this package), but it is **not** re-exported from `@firtoz/router-toolkit`. Import `success`, `fail`, `MaybeError`, `exhaustiveGuard`, and related symbols **from `@firtoz/maybe-error`** so your imports match runtime and dependency boundaries stay clear.
1138
1186
 
1139
1187
  ### Basic Usage
1140
1188
 
1141
1189
  ```tsx
1142
- import { success, fail, type MaybeError } from '@firtoz/router-toolkit';
1190
+ import { success, fail, type MaybeError } from '@firtoz/maybe-error';
1143
1191
 
1144
1192
  // Define a function that may fail
1145
1193
  function divide(a: number, b: number): MaybeError<number> {
@@ -1162,7 +1210,8 @@ if (result.success) {
1162
1210
 
1163
1211
  ```tsx
1164
1212
  // app/routes/user-profile.tsx
1165
- import { success, fail, type MaybeError, type RoutePath } from '@firtoz/router-toolkit';
1213
+ import { success, fail, type MaybeError } from '@firtoz/maybe-error';
1214
+ import { type RoutePath } from '@firtoz/router-toolkit';
1166
1215
  import type { Route } from './+types/user-profile';
1167
1216
 
1168
1217
  interface User {
@@ -1230,7 +1279,12 @@ export default function UserProfile() {
1230
1279
 
1231
1280
  ```tsx
1232
1281
  // app/routes/create-user.tsx
1233
- import { success, fail, type MaybeError, useDynamicSubmitter, type RoutePath } from '@firtoz/router-toolkit';
1282
+ import { success, fail, type MaybeError } from '@firtoz/maybe-error';
1283
+ import {
1284
+ useDynamicSubmitter,
1285
+ useDynamicSubmitterFetcher,
1286
+ type RoutePath,
1287
+ } from '@firtoz/router-toolkit';
1234
1288
  import { z } from 'zod';
1235
1289
  import type { Route } from './+types/create-user';
1236
1290
 
@@ -1280,7 +1334,9 @@ export async function action({ request }: Route.ActionArgs): Promise<MaybeError<
1280
1334
  }
1281
1335
 
1282
1336
  export default function CreateUser() {
1283
- const submitter = useDynamicSubmitter<typeof import("./create-user")>("/create-user");
1337
+ const path = "/create-user" as const;
1338
+ const submitter = useDynamicSubmitter<typeof import("./create-user")>(path);
1339
+ const fetcher = useDynamicSubmitterFetcher(submitter);
1284
1340
 
1285
1341
  return (
1286
1342
  <div>
@@ -1297,24 +1353,24 @@ export default function CreateUser() {
1297
1353
  <input id="email" name="email" type="email" required />
1298
1354
  </div>
1299
1355
 
1300
- <button type="submit" disabled={submitter.state === "submitting"}>
1301
- {submitter.state === "submitting" ? "Creating..." : "Create User"}
1356
+ <button type="submit" disabled={fetcher.state === "submitting"}>
1357
+ {fetcher.state === "submitting" ? "Creating..." : "Create User"}
1302
1358
  </button>
1303
1359
  </submitter.Form>
1304
1360
 
1305
- {submitter.data && (
1361
+ {fetcher.data && (
1306
1362
  <div>
1307
- {submitter.data.success ? (
1363
+ {fetcher.data.success ? (
1308
1364
  <div className="success">
1309
1365
  <h3>User Created!</h3>
1310
- <p>Name: {submitter.data.result.name}</p>
1311
- <p>Email: {submitter.data.result.email}</p>
1366
+ <p>Name: {fetcher.data.result.name}</p>
1367
+ <p>Email: {fetcher.data.result.email}</p>
1312
1368
  </div>
1313
1369
  ) : (
1314
1370
  <div className="errors">
1315
1371
  <h3>Validation Errors:</h3>
1316
1372
  <ul>
1317
- {submitter.data.error.map((error, index) => (
1373
+ {fetcher.data.error.map((error, index) => (
1318
1374
  <li key={index}>
1319
1375
  <strong>{error.field}:</strong> {error.message}
1320
1376
  </li>
@@ -0,0 +1,183 @@
1
+ import { useMemo, useRef, useCallback, useEffect } from 'react';
2
+ import { href, useFetcher } from 'react-router';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ // src/useDynamicSubmitter.tsx
6
+ var SubmitterSupersededError = class extends Error {
7
+ constructor(message = "This submission was superseded by a newer submit before it completed.") {
8
+ super(message);
9
+ this.name = "SubmitterSupersededError";
10
+ }
11
+ };
12
+ var SubmitterUnmountedError = class extends Error {
13
+ constructor(message = "The submitter was unmounted before this submission completed.") {
14
+ super(message);
15
+ this.name = "SubmitterUnmountedError";
16
+ }
17
+ };
18
+ function dynamicSubmitterFetcherKey(resolvedHref, keySuffix) {
19
+ const base = `submitter-${resolvedHref}`;
20
+ if (keySuffix === void 0 || keySuffix === "") {
21
+ return base;
22
+ }
23
+ return `${base}::${encodeURIComponent(keySuffix)}`;
24
+ }
25
+ function isSubmitterOptions(x) {
26
+ if (x === null || typeof x !== "object") return false;
27
+ const keys = Object.keys(x);
28
+ if (keys.length === 0) return false;
29
+ return keys.every((k) => k === "keySuffix");
30
+ }
31
+ function parseUseDynamicSubmitterRestArgs(args) {
32
+ if (args.length === 0) {
33
+ return { hrefArgs: [], options: {} };
34
+ }
35
+ const last = args[args.length - 1];
36
+ if (args.length >= 2 && isSubmitterOptions(last)) {
37
+ return { hrefArgs: [...args.slice(0, -1)], options: last };
38
+ }
39
+ if (args.length === 1 && isSubmitterOptions(args[0])) {
40
+ return { hrefArgs: [], options: args[0] };
41
+ }
42
+ return { hrefArgs: [...args], options: {} };
43
+ }
44
+ var submitterKeyBuckets = /* @__PURE__ */ new Map();
45
+ function getSubmitterKeyBucket(key) {
46
+ let b = submitterKeyBuckets.get(key);
47
+ if (!b) {
48
+ b = { submitGen: 0, pending: null };
49
+ submitterKeyBuckets.set(key, b);
50
+ }
51
+ return b;
52
+ }
53
+ var nextSubmitterOwnerId = 1;
54
+ function allocateSubmitterOwnerId() {
55
+ return nextSubmitterOwnerId++;
56
+ }
57
+ function useDynamicSubmitter(path, ...rest) {
58
+ const { hrefArgs, options } = parseUseDynamicSubmitterRestArgs(rest);
59
+ const keySuffix = options.keySuffix;
60
+ const url = useMemo(() => {
61
+ return href(path, ...hrefArgs);
62
+ }, [path, keySuffix, ...hrefArgs]);
63
+ const fetcherKey = useMemo(
64
+ () => dynamicSubmitterFetcherKey(url, keySuffix),
65
+ [url, keySuffix]
66
+ );
67
+ const fetcher = useFetcher({
68
+ key: fetcherKey
69
+ });
70
+ const fetcherRef = useRef(fetcher);
71
+ fetcherRef.current = fetcher;
72
+ const ownerIdRef = useRef(allocateSubmitterOwnerId());
73
+ const prevStateRef = useRef(fetcher.state);
74
+ const beginSubmit = useCallback(
75
+ (runSubmit) => {
76
+ return new Promise((resolve, reject) => {
77
+ const bucket = getSubmitterKeyBucket(fetcherKey);
78
+ const prevPending = bucket.pending;
79
+ if (prevPending) {
80
+ prevPending.reject(new SubmitterSupersededError());
81
+ }
82
+ bucket.submitGen += 1;
83
+ const gen = bucket.submitGen;
84
+ bucket.pending = {
85
+ gen,
86
+ ownerId: ownerIdRef.current,
87
+ reject,
88
+ finishIdle: (data, error) => {
89
+ if (data !== void 0) {
90
+ resolve(data);
91
+ } else {
92
+ reject(error ?? new Error("Submission failed"));
93
+ }
94
+ }
95
+ };
96
+ runSubmit();
97
+ });
98
+ },
99
+ [fetcherKey]
100
+ );
101
+ useEffect(() => {
102
+ return () => {
103
+ const bucket = getSubmitterKeyBucket(fetcherKey);
104
+ const pending = bucket.pending;
105
+ if (pending && pending.ownerId === ownerIdRef.current) {
106
+ bucket.pending = null;
107
+ pending.reject(new SubmitterUnmountedError());
108
+ }
109
+ };
110
+ }, [fetcherKey]);
111
+ const fetcherError = fetcher.error;
112
+ useEffect(() => {
113
+ const prev = prevStateRef.current;
114
+ prevStateRef.current = fetcher.state;
115
+ const wasWorking = prev === "submitting" || prev === "loading";
116
+ if (!wasWorking || fetcher.state !== "idle") {
117
+ return;
118
+ }
119
+ const bucket = getSubmitterKeyBucket(fetcherKey);
120
+ const p = bucket.pending;
121
+ if (!p || p.gen !== bucket.submitGen) {
122
+ return;
123
+ }
124
+ bucket.pending = null;
125
+ p.finishIdle(fetcher.data, fetcherError);
126
+ }, [fetcherKey, fetcher.state, fetcher.data, fetcherError]);
127
+ const submit = useCallback(
128
+ (target, options2) => {
129
+ return beginSubmit(() => {
130
+ const f = fetcherRef.current;
131
+ void f.submit(target, {
132
+ ...options2,
133
+ method: options2?.method ?? "POST",
134
+ action: url,
135
+ encType: "multipart/form-data"
136
+ });
137
+ });
138
+ },
139
+ [beginSubmit, url]
140
+ );
141
+ const submitJson = useCallback(
142
+ (data, options2 = {}) => {
143
+ return beginSubmit(() => {
144
+ const f = fetcherRef.current;
145
+ void f.submit(
146
+ data,
147
+ {
148
+ ...options2,
149
+ method: options2.method ?? "POST",
150
+ action: url,
151
+ encType: "application/json"
152
+ }
153
+ );
154
+ });
155
+ },
156
+ [beginSubmit, url]
157
+ );
158
+ const fetcherFormRef = useRef(fetcher.Form);
159
+ fetcherFormRef.current = fetcher.Form;
160
+ const Form = useCallback(
161
+ ({ method = "POST", ...props }) => {
162
+ const OriginalForm = fetcherFormRef.current;
163
+ return /* @__PURE__ */ jsx(OriginalForm, { action: url, method, ...props });
164
+ },
165
+ [url]
166
+ );
167
+ return useMemo(
168
+ () => ({
169
+ submit,
170
+ submitJson,
171
+ Form,
172
+ fetcherKey
173
+ }),
174
+ [submit, submitJson, Form, fetcherKey]
175
+ );
176
+ }
177
+ function useDynamicSubmitterFetcher(submitter) {
178
+ return useFetcher({ key: submitter.fetcherKey });
179
+ }
180
+
181
+ export { SubmitterSupersededError, SubmitterUnmountedError, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher };
182
+ //# sourceMappingURL=chunk-2WSA75KM.js.map
183
+ //# sourceMappingURL=chunk-2WSA75KM.js.map