formstand 0.20.0 → 0.22.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { getAtPath, slotAtPath, setAtPath, isPlainObject, hasOwnSlot, valuesEqual, useFormSelectorShallow, isFieldDirty, useFormSelector, parsePath } from './chunk-N7WPXYCU.js';
2
- export { getAtPath, parsePath, setAtPath, useFormSelector, useFormSelectorShallow } from './chunk-N7WPXYCU.js';
3
- import { shouldValidateOn } from './chunk-MQ6262S3.js';
4
- export { shouldValidateOn } from './chunk-MQ6262S3.js';
1
+ import { isDevToolsBuild, getAtPath, isDevBuild, slotAtPath, setAtPath, isPlainObject, hasOwnSlot, valuesEqual, useFormSelectorShallow, isFieldDirty, useFormSelector, parsePath } from './chunk-BMXIZEI4.js';
2
+ export { getAtPath, parsePath, setAtPath, useFormSelector, useFormSelectorShallow } from './chunk-BMXIZEI4.js';
3
+ import { flagAt, entryAt, shouldValidateOn } from './chunk-V3PDMTPP.js';
4
+ export { shouldValidateOn } from './chunk-V3PDMTPP.js';
5
5
  import { createStore } from 'zustand/vanilla';
6
6
  import { devtools } from 'zustand/middleware';
7
7
  import { z } from 'zod';
@@ -114,10 +114,17 @@ var expandIssue = (issue, prefix) => {
114
114
  }
115
115
  return [{ path: issuePathToFormPath(at), message: issue.message }];
116
116
  };
117
- var flattenIssues = (issues) => issues.flatMap((issue) => expandIssue(issue, [])).reduce((acc, { path, message }) => {
118
- const existing = (Object.hasOwn(acc, path) ? acc[path] : void 0) ?? [];
119
- return existing.includes(message) ? acc : { ...acc, [path]: [...existing, message] };
120
- }, {});
117
+ var flattenIssues = (issues) => Object.fromEntries(
118
+ issues.flatMap((issue) => expandIssue(issue, [])).reduce((acc, { path, message }) => {
119
+ const existing = acc.get(path);
120
+ if (existing === void 0) {
121
+ acc.set(path, [message]);
122
+ } else if (!existing.includes(message)) {
123
+ existing.push(message);
124
+ }
125
+ return acc;
126
+ }, /* @__PURE__ */ new Map())
127
+ );
121
128
  var validateSync = (schema, values) => {
122
129
  const result = schema.safeParse(values);
123
130
  return result.success ? { kind: "valid", data: result.data } : { kind: "invalid", errors: flattenIssues(result.error.issues) };
@@ -182,6 +189,34 @@ var hasChecks = (s) => {
182
189
  const checks = s.def.checks;
183
190
  return checks !== void 0 && checks.length > 0;
184
191
  };
192
+ var fieldSchemaForIntrospection = (schema, path) => {
193
+ const childOf = (s, segment) => {
194
+ const inner = unwrapForWalk(s);
195
+ if (inner !== null) return childOf(inner, segment);
196
+ if (s instanceof z.ZodObject && typeof segment === "string") {
197
+ const shape = s.shape;
198
+ return Object.hasOwn(shape, segment) ? shape[segment] ?? null : null;
199
+ }
200
+ if (s instanceof z.ZodArray && typeof segment === "number") {
201
+ return s.element;
202
+ }
203
+ if (s instanceof z.ZodTuple && typeof segment === "number") {
204
+ return s.def.items[segment] ?? null;
205
+ }
206
+ if (s instanceof z.ZodDiscriminatedUnion || s instanceof z.ZodUnion) {
207
+ const options = s.def.options;
208
+ return options.reduce(
209
+ (found, option) => found ?? childOf(option, segment),
210
+ null
211
+ );
212
+ }
213
+ return null;
214
+ };
215
+ return parsePath(path).reduce(
216
+ (current, segment) => current === null ? null : childOf(current, segment),
217
+ schema
218
+ );
219
+ };
185
220
  var fieldSchemaAtPath = (schema, path) => parsePath(path).reduce((current, segment) => {
186
221
  if (current === null || hasChecks(current)) return null;
187
222
  if (current instanceof z.ZodObject && typeof segment === "string") {
@@ -200,6 +235,12 @@ var emptyErrors = {};
200
235
  var emptyBools = {};
201
236
  var identityMapper = (n) => n;
202
237
  var omitKey = (map, key) => Object.fromEntries(Object.entries(map).filter(([k]) => k !== key));
238
+ var omitKeys = (map, keys) => {
239
+ if (keys.size === 0) return map;
240
+ const entries = Object.entries(map);
241
+ const kept = entries.filter(([k]) => !keys.has(k));
242
+ return kept.length === entries.length ? map : Object.fromEntries(kept);
243
+ };
203
244
  var inScopeOfAny = (key, paths) => paths.some((p) => isPathOrChild(key, p));
204
245
  var omitScope = (map, paths) => {
205
246
  const entries = Object.entries(map);
@@ -238,13 +279,13 @@ var stringArraysEqual = (a, b) => a === b || a.length === b.length && a.every((v
238
279
  var reuseErrorRefs = (next, prev) => {
239
280
  if (next === prev) return prev;
240
281
  const entries = Object.entries(next).map(([k, v]) => {
241
- const old = prev[k];
282
+ const old = entryAt(prev, k);
242
283
  return [
243
284
  k,
244
285
  old !== void 0 && stringArraysEqual(old, v) ? old : v
245
286
  ];
246
287
  });
247
- const sameShape = entries.length === Object.keys(prev).length && entries.every(([k, v]) => prev[k] === v);
288
+ const sameShape = entries.length === Object.keys(prev).length && entries.every(([k, v]) => entryAt(prev, k) === v);
248
289
  return sameShape ? prev : Object.fromEntries(entries);
249
290
  };
250
291
  var scopeSettledResult = (result, path, viaSubschema) => result.kind !== "invalid" ? emptyErrors : viaSubschema ? prefixErrorKeys(result.errors, path) : path === "" ? result.errors : pickScope(result.errors, [path]);
@@ -301,9 +342,10 @@ var createForm = (schema, options) => {
301
342
  // opting in during development must NOT stream every keystroke to
302
343
  // an end user who happens to have the extension installed, where
303
344
  // a module-singleton form's unbounded time-travel history is a
304
- // real memory + serialization cost. Guarded for environments
305
- // without process (a bundler defines NODE_ENV in real dev).
306
- enabled: typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production"
345
+ // real memory + serialization cost. Fail-closed gate: a runtime
346
+ // without a readable NODE_ENV (CDN/ESM, Deno, a worker) reads as
347
+ // production here, unlike the console-warning gate.
348
+ enabled: isDevToolsBuild()
307
349
  })
308
350
  );
309
351
  const commitFullPass = (state, next) => errorChannels(state, {
@@ -419,13 +461,19 @@ var createForm = (schema, options) => {
419
461
  const slot = slotAtPath(values, path);
420
462
  return slot.exists ? { kind: "parse", schema: sub, value: slot.value, viaSubschema: true } : { kind: "skip" };
421
463
  };
422
- const scopedFieldErrorsAsync = async (path, values) => {
423
- const scope = fieldScopeFor(path, values);
424
- return scope.kind === "skip" ? emptyErrors : scopeSettledResult(
425
- await validateAsync(scope.schema, scope.value),
426
- path,
427
- scope.viaSubschema
428
- );
464
+ const parseScopeAsync = async (path, scope) => scope.kind === "skip" ? emptyErrors : scopeSettledResult(
465
+ await validateAsync(scope.schema, scope.value),
466
+ path,
467
+ scope.viaSubschema
468
+ );
469
+ const scopeStale = (scope, path, valuesAtStart, live) => {
470
+ if (live === valuesAtStart) return false;
471
+ switch (scope.kind) {
472
+ case "skip":
473
+ return slotAtPath(live, path).exists;
474
+ case "parse":
475
+ return scope.viaSubschema ? !Object.is(getAtPath(live, path), scope.value) : true;
476
+ }
429
477
  };
430
478
  const fieldResult = (scoped) => {
431
479
  const messages = Object.values(scoped).flat();
@@ -473,10 +521,10 @@ var createForm = (schema, options) => {
473
521
  const value = getAtPath(state.values, path);
474
522
  return {
475
523
  value,
476
- error: state.errors[path],
477
- touched: state.touched[path] ?? false,
524
+ error: entryAt(state.errors, path),
525
+ touched: flagAt(state.touched, path),
478
526
  dirty: isFieldDirty(value, getAtPath(state.initialValues, path)),
479
- isValidating: state.isValidating[path] ?? false
527
+ isValidating: flagAt(state.isValidating, path)
480
528
  };
481
529
  };
482
530
  const sequences = /* @__PURE__ */ new Map();
@@ -515,44 +563,66 @@ var createForm = (schema, options) => {
515
563
  }
516
564
  const passes = new Map(paths.map((p) => [p, claimPass(p)]));
517
565
  const valuesAtStart = store.getState().values;
566
+ const scopes = paths.map((path) => ({
567
+ path,
568
+ scope: fieldScopeFor(path, valuesAtStart)
569
+ }));
570
+ const allViaSubschema = scopes.every(
571
+ ({ scope }) => scope.kind === "skip" || scope.kind === "parse" && scope.viaSubschema
572
+ );
518
573
  store.setState((state) => ({
519
574
  ...state,
520
- isValidating: paths.reduce(
521
- (acc, p) => ({ ...acc, [p]: true }),
522
- { ...state.isValidating }
523
- )
575
+ isValidating: {
576
+ ...state.isValidating,
577
+ ...Object.fromEntries(paths.map((p) => [p, true]))
578
+ }
524
579
  }));
525
- const result = await validateAsync(schema, valuesAtStart);
526
- const fullErrors = result.kind === "invalid" ? result.errors : emptyErrors;
527
- const requestedErrors = pickScope(fullErrors, paths);
580
+ const parsed = allViaSubschema ? {
581
+ kind: "slots",
582
+ byPath: new Map(
583
+ await Promise.all(
584
+ scopes.map(
585
+ async ({ path, scope }) => [path, await parseScopeAsync(path, scope)]
586
+ )
587
+ )
588
+ )
589
+ } : {
590
+ kind: "full",
591
+ errors: await validateAsync(schema, valuesAtStart).then(
592
+ (result) => result.kind === "invalid" ? result.errors : emptyErrors
593
+ )
594
+ };
595
+ const errorsFor = (path) => {
596
+ switch (parsed.kind) {
597
+ case "slots":
598
+ return parsed.byPath.get(path) ?? emptyErrors;
599
+ case "full":
600
+ return pickScope(parsed.errors, [path]);
601
+ }
602
+ };
603
+ const requestedErrors = Object.fromEntries(
604
+ paths.flatMap((path) => Object.entries(errorsFor(path)))
605
+ );
528
606
  const requested = settledFields(requestedErrors);
529
607
  const live = store.getState();
530
- const stillCurrent = paths.filter(
531
- (p) => sequences.get(p) === passes.get(p)
608
+ const owned = scopes.filter(
609
+ ({ path }) => sequences.get(path) === passes.get(path)
532
610
  );
533
- if (live.values !== valuesAtStart) {
534
- if (stillCurrent.length > 0) {
535
- store.setState((state) => ({
536
- ...state,
537
- isValidating: stillCurrent.reduce(
538
- (acc, p) => omitKey(acc, p),
539
- state.isValidating
540
- )
541
- }));
542
- }
543
- return requested;
544
- }
611
+ const fresh = owned.filter(
612
+ ({ path, scope }) => !scopeStale(scope, path, valuesAtStart, live.values)
613
+ ).map(({ path }) => path);
614
+ const ownedKeys = new Set(owned.map(({ path }) => path));
615
+ if (owned.length === 0) return requested;
545
616
  store.setState((state) => ({
546
617
  ...state,
547
- ...commitScopedErrors(
618
+ ...fresh.length === 0 ? {} : commitScopedErrors(
548
619
  state,
549
- pickScope(fullErrors, stillCurrent),
550
- stillCurrent
620
+ Object.fromEntries(
621
+ fresh.flatMap((path) => Object.entries(errorsFor(path)))
622
+ ),
623
+ fresh
551
624
  ),
552
- isValidating: stillCurrent.reduce(
553
- (acc, p) => omitKey(acc, p),
554
- state.isValidating
555
- )
625
+ isValidating: omitKeys(state.isValidating, ownedKeys)
556
626
  }));
557
627
  return requested;
558
628
  };
@@ -566,14 +636,15 @@ var createForm = (schema, options) => {
566
636
  }
567
637
  const pass = claimPass(path);
568
638
  const valuesAtStart = store.getState().values;
639
+ const scope = fieldScopeFor(path, valuesAtStart);
569
640
  store.setState((state) => ({
570
641
  ...state,
571
642
  isValidating: { ...state.isValidating, [path]: true }
572
643
  }));
573
- const scoped = await scopedFieldErrorsAsync(path, valuesAtStart);
644
+ const scoped = await parseScopeAsync(path, scope);
574
645
  const live = store.getState();
575
646
  const stillOwns = sequences.get(path) === pass;
576
- if (!stillOwns || live.values !== valuesAtStart) {
647
+ if (!stillOwns || scopeStale(scope, path, valuesAtStart, live.values)) {
577
648
  if (stillOwns) {
578
649
  store.setState((state) => ({
579
650
  ...state,
@@ -808,17 +879,24 @@ var createForm = (schema, options) => {
808
879
  ...state,
809
880
  ...errorChannels(state, { serverErrors: errors })
810
881
  })),
811
- addErrors: (errors) => store.setState((state) => ({
812
- ...state,
813
- ...errorChannels(state, {
814
- serverErrors: Object.entries(errors).reduce(
815
- // Computed keys throughout, so a server field named
816
- // "__proto__" lands as an own property like everywhere else.
817
- (acc, [key, list]) => list === void 0 || list.length === 0 ? omitKey(acc, key) : { ...acc, [key]: list },
818
- state.serverErrors
819
- )
820
- })
821
- })),
882
+ addErrors: (errors) => store.setState((state) => {
883
+ const entries = Object.entries(errors);
884
+ const removed = new Set(
885
+ entries.filter(([, list]) => list === void 0 || list.length === 0).map(([key]) => key)
886
+ );
887
+ const added = Object.fromEntries(
888
+ entries.filter(([, list]) => list !== void 0 && list.length > 0)
889
+ );
890
+ return {
891
+ ...state,
892
+ ...errorChannels(state, {
893
+ serverErrors: {
894
+ ...omitKeys(state.serverErrors, removed),
895
+ ...added
896
+ }
897
+ })
898
+ };
899
+ }),
822
900
  clearErrors: (path) => store.setState((state) => {
823
901
  const scrub = (map) => path === void 0 ? emptyErrors : omitScope(map, [path]);
824
902
  return {
@@ -830,27 +908,26 @@ var createForm = (schema, options) => {
830
908
  };
831
909
  }),
832
910
  updateState: (updater) => {
833
- const valuesBefore = store.getState().values;
834
- store.setState((state) => {
835
- const patch = updater(state);
836
- if (Object.keys(patch).length === 0) return state;
837
- if (patch.errors !== void 0 && patch.errors !== state.errors) {
838
- console.warn(
839
- "[formstand] `errors` is derived from schemaErrors/serverErrors \u2014 patch those channels instead; the direct `errors` patch is ignored."
840
- );
841
- }
842
- const next = { ...state, ...patch };
843
- return {
844
- ...next,
845
- ...errorChannels(state, {
846
- schemaErrors: next.schemaErrors,
847
- serverErrors: next.serverErrors
848
- })
849
- };
850
- });
851
- if (store.getState().values !== valuesBefore) {
911
+ const state = store.getState();
912
+ const patch = updater(state);
913
+ if (Object.keys(patch).length === 0) return;
914
+ if (patch.errors !== void 0 && patch.errors !== state.errors) {
915
+ console.warn(
916
+ "[formstand] `errors` is derived from schemaErrors/serverErrors \u2014 patch those channels instead; the direct `errors` patch is ignored."
917
+ );
918
+ }
919
+ const next = { ...state, ...patch };
920
+ if (next.values !== state.values) {
852
921
  clearArrayOpsUnder(store, "");
853
922
  }
923
+ const serverErrors = patch.serverErrors !== void 0 ? patch.serverErrors : next.values !== state.values ? releaseChangedSlices(state.serverErrors, state.values, next.values) : state.serverErrors;
924
+ store.setState(() => ({
925
+ ...next,
926
+ ...errorChannels(state, {
927
+ schemaErrors: next.schemaErrors,
928
+ serverErrors
929
+ })
930
+ }));
854
931
  },
855
932
  adoptValues: (values) => {
856
933
  sequences.clear();
@@ -959,7 +1036,7 @@ var createForm = (schema, options) => {
959
1036
  handleSubmit: (onValid, onInvalid, opts) => async (event) => {
960
1037
  event?.preventDefault();
961
1038
  const result = await submit(onValid, onInvalid, opts);
962
- if (result.kind === "error" && opts?.onError === void 0 && process.env.NODE_ENV !== "production") {
1039
+ if (result.kind === "error" && opts?.onError === void 0 && isDevBuild()) {
963
1040
  console.warn(
964
1041
  "[formstand] submit handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to handleSubmit (or setError inside your handler):",
965
1042
  result.error
@@ -1010,6 +1087,35 @@ var createForm = (schema, options) => {
1010
1087
  });
1011
1088
  };
1012
1089
 
1090
+ // src/core/debounce.ts
1091
+ var createDebouncer = (ms) => {
1092
+ const timer = {
1093
+ current: null
1094
+ };
1095
+ const cancel = () => {
1096
+ if (timer.current !== null) {
1097
+ clearTimeout(timer.current);
1098
+ timer.current = null;
1099
+ }
1100
+ };
1101
+ const schedule = (run) => {
1102
+ cancel();
1103
+ if (ms === 0) {
1104
+ run();
1105
+ return;
1106
+ }
1107
+ timer.current = setTimeout(() => {
1108
+ timer.current = null;
1109
+ run();
1110
+ }, ms);
1111
+ };
1112
+ return Object.freeze({
1113
+ schedule,
1114
+ cancel,
1115
+ pending: () => timer.current !== null
1116
+ });
1117
+ };
1118
+
1013
1119
  // src/core/persist.ts
1014
1120
  var kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
1015
1121
  var conflictsWith = (draft, reference) => {
@@ -1041,7 +1147,7 @@ var persistForm = (form, options) => {
1041
1147
  const storage = options.storage ?? defaultStorage();
1042
1148
  const debounceMs = options.debounceMs ?? 300;
1043
1149
  const apply = options.apply ?? "adopt";
1044
- const applyDraft = (values) => apply === "restore" ? form.setValues(values) : form.adoptValues(values);
1150
+ const applyDraft = (values) => apply === "edit" || apply === "restore" ? form.setValues(values) : form.adoptValues(values);
1045
1151
  const referenceShape = () => {
1046
1152
  try {
1047
1153
  return JSON.parse(JSON.stringify(form.getState().initialValues));
@@ -1063,6 +1169,7 @@ var persistForm = (form, options) => {
1063
1169
  if (versioned && wrapper.__v !== options.version) return false;
1064
1170
  if (versioned && wrapper.values === void 0) return false;
1065
1171
  const values = versioned ? wrapper.values : parsed;
1172
+ if (values === null || values === void 0) return false;
1066
1173
  const reference = referenceShape();
1067
1174
  if (reference !== void 0 && conflictsWith(values, reference)) {
1068
1175
  return false;
@@ -1084,28 +1191,13 @@ var persistForm = (form, options) => {
1084
1191
  } catch {
1085
1192
  }
1086
1193
  };
1087
- const timerRef = {
1088
- current: null
1089
- };
1090
- const cancelPending = () => {
1091
- if (timerRef.current !== null) {
1092
- clearTimeout(timerRef.current);
1093
- timerRef.current = null;
1094
- }
1095
- };
1194
+ const writes = createDebouncer(debounceMs);
1195
+ const cancelPending = () => writes.cancel();
1096
1196
  if (apply !== "manual") {
1097
1197
  load();
1098
1198
  }
1099
1199
  const unsubscribe = form.watchValues((values) => {
1100
- cancelPending();
1101
- if (debounceMs === 0) {
1102
- writeDraft(values);
1103
- } else {
1104
- timerRef.current = setTimeout(() => {
1105
- timerRef.current = null;
1106
- writeDraft(values);
1107
- }, debounceMs);
1108
- }
1200
+ writes.schedule(() => writeDraft(values));
1109
1201
  });
1110
1202
  return Object.freeze({
1111
1203
  load,
@@ -1287,7 +1379,7 @@ var useMaskedInput = (field, options) => {
1287
1379
  field.setValue(parsed.value);
1288
1380
  return;
1289
1381
  case "empty": {
1290
- const empty = field.emptyValue;
1382
+ const empty = field.clearable || typeof field.initialValue !== "string" ? field.emptyValue : "";
1291
1383
  setEdit({ raw: text, pushed: empty });
1292
1384
  field.setValue(empty);
1293
1385
  return;
@@ -1327,9 +1419,14 @@ var useFormSteps = (form, steps) => {
1327
1419
  },
1328
1420
  [form]
1329
1421
  );
1422
+ const currentStep = useRef(step);
1423
+ useEffect(() => {
1424
+ currentStep.current = step;
1425
+ });
1330
1426
  const next = useCallback(async () => {
1331
1427
  const ok = await validateStep(step);
1332
1428
  if (!ok) return false;
1429
+ if (currentStep.current !== step) return false;
1333
1430
  const target = Math.min(step + 1, latest.current.length - 1);
1334
1431
  setStep(target);
1335
1432
  setVisited((prev) => /* @__PURE__ */ new Set([...prev, target]));
@@ -1351,6 +1448,7 @@ var useFormSteps = (form, steps) => {
1351
1448
  return await validateStep(from) ? walk(from + 1) : from;
1352
1449
  };
1353
1450
  const landed = await walk(step);
1451
+ if (currentStep.current !== step) return currentStep.current;
1354
1452
  setStep(landed);
1355
1453
  setVisited(
1356
1454
  (prev) => /* @__PURE__ */ new Set([
@@ -1396,7 +1494,7 @@ var useFormSteps = (form, steps) => {
1396
1494
  );
1397
1495
  };
1398
1496
  var warnUnobserved = (result, options, site) => {
1399
- if (result.kind === "error" && options?.onError === void 0 && process.env.NODE_ENV !== "production") {
1497
+ if (result.kind === "error" && options?.onError === void 0 && isDevBuild()) {
1400
1498
  console.warn(
1401
1499
  `[formstand] action handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to ${site} (or setError inside your handler):`,
1402
1500
  result.error
@@ -1465,10 +1563,12 @@ function useField(form, pathArg, options) {
1465
1563
  path: p,
1466
1564
  value,
1467
1565
  initialValue: initialValue2,
1468
- error: state.errors[p],
1469
- touched: state.touched[p] ?? false,
1566
+ // Own-key reads: a field named "constructor" or "toString" would
1567
+ // otherwise read Object.prototype's function as its error/flag.
1568
+ error: entryAt(state.errors, p),
1569
+ touched: flagAt(state.touched, p),
1470
1570
  dirty: isFieldDirty(value, initialValue2),
1471
- isValidating: state.isValidating[p] ?? false
1571
+ isValidating: flagAt(state.isValidating, p)
1472
1572
  };
1473
1573
  })
1474
1574
  );
@@ -1477,7 +1577,7 @@ function useField(form, pathArg, options) {
1477
1577
  const initialValue = slice.initialValue;
1478
1578
  const { emptyValue, clearable } = useMemo(() => {
1479
1579
  if (formSchema !== void 0) {
1480
- const sub = fieldSchemaAtPath(formSchema, path);
1580
+ const sub = fieldSchemaForIntrospection(formSchema, path);
1481
1581
  if (sub !== null) {
1482
1582
  return {
1483
1583
  emptyValue: emptyValueForSchema(sub),
@@ -1488,28 +1588,26 @@ function useField(form, pathArg, options) {
1488
1588
  return initialValue === null ? { emptyValue: null, clearable: true } : { emptyValue: void 0, clearable: false };
1489
1589
  }, [formSchema, path, initialValue]);
1490
1590
  const debounceMs = options?.debounceMs;
1491
- const debounceTimerRef = useRef(null);
1591
+ const debouncerRef = useRef(null);
1492
1592
  useEffect(() => {
1493
1593
  return () => {
1494
- if (debounceTimerRef.current !== null) {
1495
- clearTimeout(debounceTimerRef.current);
1496
- debounceTimerRef.current = null;
1497
- }
1594
+ debouncerRef.current?.cancel();
1595
+ debouncerRef.current = null;
1498
1596
  };
1499
1597
  }, [form, path, debounceMs]);
1500
1598
  const triggerValidate = useCallback(() => {
1501
1599
  if (debounceMs !== void 0 && debounceMs > 0) {
1502
- if (debounceTimerRef.current !== null) {
1503
- clearTimeout(debounceTimerRef.current);
1504
- }
1505
- const scheduled = getAtPath(form.store.getState().values, path);
1506
- debounceTimerRef.current = setTimeout(() => {
1507
- debounceTimerRef.current = null;
1600
+ const debouncer = debouncerRef.current ?? createDebouncer(debounceMs);
1601
+ debouncerRef.current = debouncer;
1602
+ const scheduledValues = form.store.getState().values;
1603
+ const scheduled = getAtPath(scheduledValues, path);
1604
+ debouncer.schedule(() => {
1508
1605
  const state = form.store.getState();
1509
- if (state.values === state.initialValues) return;
1606
+ const rebased = state.values !== scheduledValues && state.values === state.initialValues;
1607
+ if (rebased) return;
1510
1608
  if (!Object.is(getAtPath(state.values, path), scheduled)) return;
1511
1609
  void form.validateFieldAsync(path);
1512
- }, debounceMs);
1610
+ });
1513
1611
  return;
1514
1612
  }
1515
1613
  form.validateField(path);
@@ -1525,7 +1623,7 @@ function useField(form, pathArg, options) {
1525
1623
  state.mode,
1526
1624
  state.reValidateMode,
1527
1625
  state.submitCount > 0,
1528
- state.touched[path] ?? false
1626
+ flagAt(state.touched, path)
1529
1627
  )) {
1530
1628
  triggerValidate();
1531
1629
  }
@@ -1606,7 +1704,11 @@ var reconcileIds = (prev, nextItems) => {
1606
1704
  if (prev.items.length === nextItems.length && prev.items.every((item, i) => Object.is(item, nextItems[i]))) {
1607
1705
  return { ...prev, items: nextItems };
1608
1706
  }
1707
+ const keptAtIndex = nextItems.map(
1708
+ (item, i) => i < prev.items.length && Object.is(item, prev.items[i])
1709
+ );
1609
1710
  const buckets = prev.items.reduce((acc, item, i) => {
1711
+ if (keptAtIndex[i] === true) return acc;
1610
1712
  const bucket = acc.get(item);
1611
1713
  if (bucket === void 0) {
1612
1714
  acc.set(item, [i]);
@@ -1615,7 +1717,8 @@ var reconcileIds = (prev, nextItems) => {
1615
1717
  }
1616
1718
  return acc;
1617
1719
  }, /* @__PURE__ */ new Map());
1618
- const matchedIds = nextItems.map((item) => {
1720
+ const matchedIds = nextItems.map((item, i) => {
1721
+ if (keptAtIndex[i] === true) return prev.ids[i] ?? null;
1619
1722
  const prevIndex = buckets.get(item)?.shift();
1620
1723
  return prevIndex === void 0 ? null : prev.ids[prevIndex] ?? null;
1621
1724
  });
@@ -1709,7 +1812,9 @@ function useFieldArray(form, pathArg) {
1709
1812
  const value = getAtPath(state.values, path);
1710
1813
  return {
1711
1814
  items: Array.isArray(value) ? value : EMPTY_ITEMS,
1712
- error: state.errors[path]
1815
+ // Own-key read, like useField: an array named "constructor" must
1816
+ // not read Object as its error list.
1817
+ error: entryAt(state.errors, path)
1713
1818
  };
1714
1819
  })
1715
1820
  );
@@ -1723,7 +1828,7 @@ function useFieldArray(form, pathArg) {
1723
1828
  state.mode,
1724
1829
  state.reValidateMode,
1725
1830
  state.submitCount > 0,
1726
- state.touched[path] ?? false
1831
+ flagAt(state.touched, path)
1727
1832
  )) {
1728
1833
  form.validateField?.(path);
1729
1834
  }