react-hook-form 7.84.0 → 7.85.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.
@@ -170,6 +170,82 @@ const useIsomorphicLayoutEffect = isWeb
170
170
  ? React.useLayoutEffect
171
171
  : React.useEffect;
172
172
 
173
+ var isPrimitive = (value) => isNullOrUndefined(value) || !isObjectType(value);
174
+
175
+ const isEmptyObjectWithCustomPrototype = (object, keys) => keys.length === 0 && !Array.isArray(object) && !isPlainObject(object);
176
+ function deepEqual(object1, object2, visited = new WeakMap()) {
177
+ if (object1 === object2) {
178
+ return true;
179
+ }
180
+ if (isPrimitive(object1) || isPrimitive(object2)) {
181
+ return Object.is(object1, object2);
182
+ }
183
+ if (isDateObject(object1) && isDateObject(object2)) {
184
+ return Object.is(object1.getTime(), object2.getTime());
185
+ }
186
+ const keys1 = Object.keys(object1);
187
+ const keys2 = Object.keys(object2);
188
+ if (keys1.length !== keys2.length) {
189
+ return false;
190
+ }
191
+ if (isEmptyObjectWithCustomPrototype(object1, keys1) ||
192
+ isEmptyObjectWithCustomPrototype(object2, keys2)) {
193
+ return Object.is(object1, object2);
194
+ }
195
+ if (!keys1.length && Array.isArray(object1) !== Array.isArray(object2)) {
196
+ return false;
197
+ }
198
+ const visitedPairs = visited.get(object1);
199
+ if (visitedPairs && visitedPairs.has(object2)) {
200
+ return true;
201
+ }
202
+ if (visitedPairs) {
203
+ visitedPairs.add(object2);
204
+ }
205
+ else {
206
+ const ws = new WeakSet();
207
+ ws.add(object2);
208
+ visited.set(object1, ws);
209
+ }
210
+ for (const key of keys1) {
211
+ const val1 = object1[key];
212
+ if (!(key in object2)) {
213
+ return false;
214
+ }
215
+ if (key !== 'ref') {
216
+ const val2 = object2[key];
217
+ if ((isDateObject(val1) && isDateObject(val2)) ||
218
+ ((isObject(val1) || Array.isArray(val1)) &&
219
+ (isObject(val2) || Array.isArray(val2)))
220
+ ? !deepEqual(val1, val2, visited)
221
+ : !Object.is(val1, val2)) {
222
+ return false;
223
+ }
224
+ }
225
+ }
226
+ return true;
227
+ }
228
+
229
+ function useResyncOnReconnect() {
230
+ const _connected = React.useRef(false);
231
+ const _prevValue = React.useRef(undefined);
232
+ const resyncIfNeeded = React.useCallback((enabled, getCurrentValue, setValue) => {
233
+ if (enabled && _connected.current) {
234
+ const currentValue = getCurrentValue();
235
+ if (!deepEqual(_prevValue.current, currentValue)) {
236
+ setValue(currentValue);
237
+ }
238
+ }
239
+ _connected.current = true;
240
+ }, []);
241
+ const snapshot = React.useCallback((enabled, getCurrentValue) => {
242
+ if (enabled) {
243
+ _prevValue.current = cloneObject(getCurrentValue());
244
+ }
245
+ }, []);
246
+ return { resyncIfNeeded, snapshot };
247
+ }
248
+
173
249
  /**
174
250
  * Subscribes to each form state and isolates re-renders at the custom hook level. It has its own scope for form state subscriptions, so it will not affect other useFormState or useForm instances. Using this hook can reduce the re-render impact on large and complex form applications.
175
251
  *
@@ -217,19 +293,31 @@ function useFormState(props) {
217
293
  isValid: false,
218
294
  errors: false,
219
295
  });
220
- useIsomorphicLayoutEffect(() => control._subscribe({
221
- name,
222
- formState: _localProxyFormState.current,
223
- exact,
224
- callback: (formState) => {
225
- !disabled &&
226
- updateFormState({
227
- ...control._formState,
228
- ...formState,
229
- defaultValues: control._defaultValues,
230
- });
231
- },
232
- }), [name, disabled, exact]);
296
+ const { resyncIfNeeded, snapshot } = useResyncOnReconnect();
297
+ useIsomorphicLayoutEffect(() => {
298
+ const getCurrentFormState = () => ({
299
+ ...control._formState,
300
+ defaultValues: control._defaultValues,
301
+ });
302
+ resyncIfNeeded(!disabled, getCurrentFormState, updateFormState);
303
+ const unsubscribe = control._subscribe({
304
+ name,
305
+ formState: _localProxyFormState.current,
306
+ exact,
307
+ callback: (formState) => {
308
+ !disabled &&
309
+ updateFormState({
310
+ ...control._formState,
311
+ ...formState,
312
+ defaultValues: control._defaultValues,
313
+ });
314
+ },
315
+ });
316
+ return () => {
317
+ unsubscribe();
318
+ snapshot(!disabled, getCurrentFormState);
319
+ };
320
+ }, [name, disabled, exact, resyncIfNeeded, snapshot]);
233
321
  React.useEffect(() => {
234
322
  _localProxyFormState.current.isValid && control._setValid(true);
235
323
  }, [control]);
@@ -251,62 +339,6 @@ var generateWatchOutput = (names, _names, formValues, isGlobal, defaultValue) =>
251
339
  return formValues;
252
340
  };
253
341
 
254
- var isPrimitive = (value) => isNullOrUndefined(value) || !isObjectType(value);
255
-
256
- const isEmptyObjectWithCustomPrototype = (object, keys) => keys.length === 0 && !Array.isArray(object) && !isPlainObject(object);
257
- function deepEqual(object1, object2, visited = new WeakMap()) {
258
- if (object1 === object2) {
259
- return true;
260
- }
261
- if (isPrimitive(object1) || isPrimitive(object2)) {
262
- return Object.is(object1, object2);
263
- }
264
- if (isDateObject(object1) && isDateObject(object2)) {
265
- return Object.is(object1.getTime(), object2.getTime());
266
- }
267
- const keys1 = Object.keys(object1);
268
- const keys2 = Object.keys(object2);
269
- if (keys1.length !== keys2.length) {
270
- return false;
271
- }
272
- if (isEmptyObjectWithCustomPrototype(object1, keys1) ||
273
- isEmptyObjectWithCustomPrototype(object2, keys2)) {
274
- return Object.is(object1, object2);
275
- }
276
- if (!keys1.length && Array.isArray(object1) !== Array.isArray(object2)) {
277
- return false;
278
- }
279
- const visitedPairs = visited.get(object1);
280
- if (visitedPairs && visitedPairs.has(object2)) {
281
- return true;
282
- }
283
- if (visitedPairs) {
284
- visitedPairs.add(object2);
285
- }
286
- else {
287
- const ws = new WeakSet();
288
- ws.add(object2);
289
- visited.set(object1, ws);
290
- }
291
- for (const key of keys1) {
292
- const val1 = object1[key];
293
- if (!(key in object2)) {
294
- return false;
295
- }
296
- if (key !== 'ref') {
297
- const val2 = object2[key];
298
- if ((isDateObject(val1) && isDateObject(val2)) ||
299
- ((isObject(val1) || Array.isArray(val1)) &&
300
- (isObject(val2) || Array.isArray(val2)))
301
- ? !deepEqual(val1, val2, visited)
302
- : !Object.is(val1, val2)) {
303
- return false;
304
- }
305
- }
306
- }
307
- return true;
308
- }
309
-
310
342
  /**
311
343
  * Custom hook to subscribe to field changes and isolate re-rendering at the component level.
312
344
  *
@@ -355,24 +387,39 @@ function useWatch(props) {
355
387
  }
356
388
  }
357
389
  }, [control._formValues, control._names, disabled, name]);
390
+ const { resyncIfNeeded, snapshot } = useResyncOnReconnect();
391
+ const _refreshValue = React.useRef(refreshValue);
392
+ _refreshValue.current = refreshValue;
393
+ const _getCurrentOutput = React.useRef(getCurrentOutput);
394
+ _getCurrentOutput.current = getCurrentOutput;
358
395
  useIsomorphicLayoutEffect(() => {
359
396
  if (_prevControl.current !== control ||
360
397
  !deepEqual(_prevName.current, name)) {
361
398
  _prevControl.current = control;
362
399
  _prevName.current = name;
363
- refreshValue();
400
+ _refreshValue.current();
364
401
  }
365
- return control._subscribe({
402
+ else {
403
+ resyncIfNeeded(!disabled, () => _getCurrentOutput.current(), (currentValue) => {
404
+ updateValue(currentValue);
405
+ _computeFormValues.current = currentValue;
406
+ });
407
+ }
408
+ const unsubscribe = control._subscribe({
366
409
  name,
367
410
  formState: {
368
411
  values: true,
369
412
  },
370
413
  exact,
371
414
  callback: (formState) => {
372
- refreshValue(formState.values);
415
+ _refreshValue.current(formState.values);
373
416
  },
374
417
  });
375
- }, [control, exact, name, refreshValue]);
418
+ return () => {
419
+ unsubscribe();
420
+ snapshot(!disabled, () => _getCurrentOutput.current());
421
+ };
422
+ }, [control, exact, name, disabled, resyncIfNeeded, snapshot]);
376
423
  React.useEffect(() => control._removeUnmounted());
377
424
  // If name or control changed for this render, synchronously reflect the
378
425
  // latest value so callers (like useController) see the correct value
@@ -830,7 +877,9 @@ var validateField = async (field, disabledFieldNames, formValues, validateAllFie
830
877
  let exceedMin;
831
878
  const maxOutput = getValueAndMessage(max);
832
879
  const minOutput = getValueAndMessage(min);
833
- if (!isNullOrUndefined(inputValue) && !isNaN(inputValue)) {
880
+ if (!isNullOrUndefined(inputValue) &&
881
+ !isDateObject(inputValue) &&
882
+ !isNaN(inputValue)) {
834
883
  const valueNumber = ref.valueAsNumber ||
835
884
  (inputValue ? +inputValue : inputValue);
836
885
  if (!isNullOrUndefined(maxOutput.value)) {
@@ -1238,9 +1287,11 @@ function useFieldArray(props) {
1238
1287
  };
1239
1288
  React.useEffect(() => {
1240
1289
  if (disabled) {
1290
+ control._state.actionArrayLengths.delete(name);
1241
1291
  return;
1242
1292
  }
1243
1293
  control._state.action = false;
1294
+ control._state.actionArrayLengths.delete(name);
1244
1295
  isWatched(name, control._names) &&
1245
1296
  control._subjects.state.next({
1246
1297
  ...control._formState,
@@ -1320,6 +1371,7 @@ function useFieldArray(props) {
1320
1371
  !get(control._formValues, name) && control._setFieldArray(name);
1321
1372
  }
1322
1373
  return () => {
1374
+ control._state.actionArrayLengths.delete(name);
1323
1375
  if (disabled) {
1324
1376
  return;
1325
1377
  }
@@ -1964,7 +2016,12 @@ var skipValidation = (isBlurEvent, isTouched, isSubmitted, reValidateMode, mode)
1964
2016
  return true;
1965
2017
  };
1966
2018
 
1967
- var unsetEmptyArray = (ref, name) => !compact(get(ref, name)).length && unset(ref, name);
2019
+ var unsetEmptyArray = (ref, name) => {
2020
+ const array = get(ref, name);
2021
+ !compact(array).length &&
2022
+ !(array === null || array === void 0 ? void 0 : array.root) &&
2023
+ unset(ref, name);
2024
+ };
1968
2025
 
1969
2026
  const defaultOptions = {
1970
2027
  mode: VALIDATION_MODE.onSubmit,
@@ -2013,6 +2070,7 @@ function createFormControl(props = {}) {
2013
2070
  : cloneObject(_defaultValues);
2014
2071
  let _state = {
2015
2072
  action: false,
2073
+ actionArrayLengths: new Map(),
2016
2074
  mount: false,
2017
2075
  watch: false,
2018
2076
  keepIsValid: false,
@@ -2108,13 +2166,22 @@ function createFormControl(props = {}) {
2108
2166
  const _setFieldArray = (name, values = [], method, args, shouldSetValues = true, shouldUpdateFieldsAndState = true) => {
2109
2167
  if (args && method && !_options.disabled) {
2110
2168
  _state.action = true;
2169
+ if (!_state.actionArrayLengths.has(name)) {
2170
+ const preActionFields = get(_fields, name);
2171
+ _state.actionArrayLengths.set(name, Array.isArray(preActionFields) ? preActionFields.length : 0);
2172
+ }
2111
2173
  if (shouldUpdateFieldsAndState && Array.isArray(get(_fields, name))) {
2112
2174
  const fieldValues = method(get(_fields, name), args.argA, args.argB);
2113
2175
  shouldSetValues && set(_fields, name, fieldValues);
2114
2176
  }
2115
2177
  if (shouldUpdateFieldsAndState &&
2116
2178
  Array.isArray(get(_formState.errors, name))) {
2117
- const errors = method(get(_formState.errors, name), args.argA, args.argB);
2179
+ const fieldArrayErrors = get(_formState.errors, name);
2180
+ const rootError = fieldArrayErrors.root;
2181
+ const errors = method(fieldArrayErrors, args.argA, args.argB) || fieldArrayErrors;
2182
+ if (rootError) {
2183
+ errors.root = rootError;
2184
+ }
2118
2185
  shouldSetValues && set(_formState.errors, name, errors);
2119
2186
  unsetEmptyArray(_formState.errors, name);
2120
2187
  }
@@ -2170,10 +2237,40 @@ function createFormControl(props = {}) {
2170
2237
  }
2171
2238
  return false;
2172
2239
  };
2240
+ const isStaleArrayIndex = (name) => {
2241
+ if (!_state.actionArrayLengths.size) {
2242
+ return false;
2243
+ }
2244
+ const segments = isKey(name) ? [name] : stringToPath(name);
2245
+ let node = _formValues;
2246
+ let path = '';
2247
+ let ownerDepth = -1;
2248
+ let ownerPreActionLength = 0;
2249
+ for (let i = 0; i < segments.length; i++) {
2250
+ if (isNullOrUndefined(node)) {
2251
+ return false;
2252
+ }
2253
+ const key = segments[i];
2254
+ path = path ? `${path}.${key}` : key;
2255
+ if (Array.isArray(node) && +key >= node.length) {
2256
+ return ownerDepth === -1
2257
+ ? false
2258
+ : i === ownerDepth
2259
+ ? +key < ownerPreActionLength
2260
+ : true;
2261
+ }
2262
+ if (_state.actionArrayLengths.has(path)) {
2263
+ ownerDepth = i + 1;
2264
+ ownerPreActionLength = _state.actionArrayLengths.get(path);
2265
+ }
2266
+ node = node[key];
2267
+ }
2268
+ return false;
2269
+ };
2173
2270
  const updateValidAndValue = (name, shouldSkipSetValueAs, value, ref) => {
2174
2271
  const field = get(_fields, name);
2175
2272
  if (field) {
2176
- if (hasExplicitNullIntermediate(name)) {
2273
+ if (hasExplicitNullIntermediate(name) || isStaleArrayIndex(name)) {
2177
2274
  return;
2178
2275
  }
2179
2276
  const wasUnsetInFormValues = isUndefined(get(_formValues, name));
@@ -2421,14 +2518,12 @@ function createFormControl(props = {}) {
2421
2518
  const _getWatch = (names, defaultValue, isGlobal) => generateWatchOutput(names, _names, {
2422
2519
  ...(_state.mount
2423
2520
  ? _formValues
2424
- : isUndefined(defaultValue)
2521
+ : isUndefined(defaultValue) || isString(names)
2425
2522
  ? _defaultValues
2426
- : isString(names)
2427
- ? { [names]: defaultValue }
2428
- : defaultValue),
2523
+ : defaultValue),
2429
2524
  }, isGlobal, defaultValue);
2430
2525
  const _getFieldArray = (name) => compact(get(_state.mount ? _formValues : _defaultValues, name, _options.shouldUnregister ? get(_defaultValues, name, []) : []));
2431
- const setFieldValue = (name, value, options = {}, skipClone = false, skipRender = false) => {
2526
+ const setFieldValue = (name, value, options = {}, skipClone = false, skipRender = false, skipValueRender = false) => {
2432
2527
  const field = get(_fields, name);
2433
2528
  let fieldValue = value;
2434
2529
  if (field) {
@@ -2466,7 +2561,7 @@ function createFormControl(props = {}) {
2466
2561
  }
2467
2562
  else {
2468
2563
  fieldReference.ref.value = fieldValue;
2469
- if (!fieldReference.ref.type && !skipRender) {
2564
+ if (!fieldReference.ref.type && !skipRender && !skipValueRender) {
2470
2565
  _subjects.state.next({
2471
2566
  name,
2472
2567
  values: skipClone ? _formValues : cloneObject(_formValues),
@@ -2482,7 +2577,7 @@ function createFormControl(props = {}) {
2482
2577
  delayError: options.delayError,
2483
2578
  });
2484
2579
  };
2485
- const setFieldValues = (name, value, options, skipClone = false, skipRender = false) => {
2580
+ const setFieldValues = (name, value, options, skipClone = false, skipRender = false, skipValueRender = false) => {
2486
2581
  if (_names.array.has(name)) {
2487
2582
  _subjects.array.next({
2488
2583
  name,
@@ -2500,8 +2595,8 @@ function createFormControl(props = {}) {
2500
2595
  isObject(fieldValue) ||
2501
2596
  (field && !field._f)) &&
2502
2597
  !isDateObject(fieldValue)
2503
- ? setFieldValues(fieldName, fieldValue, options, skipClone, skipRender)
2504
- : setFieldValue(fieldName, fieldValue, options, skipClone, skipRender);
2598
+ ? setFieldValues(fieldName, fieldValue, options, skipClone, skipRender, skipValueRender)
2599
+ : setFieldValue(fieldName, fieldValue, options, skipClone, skipRender, skipValueRender);
2505
2600
  }
2506
2601
  };
2507
2602
  const _setValue = (name, value, options, skipClone, skipStateEmit = false) => {
@@ -2536,11 +2631,12 @@ function createFormControl(props = {}) {
2536
2631
  else {
2537
2632
  const isEmpty = (Array.isArray(cloneValue) && !cloneValue.length) ||
2538
2633
  isEmptyObject(cloneValue);
2634
+ const skipValueRender = !isValueUnchanged && !skipStateEmit;
2539
2635
  if (!field || field._f || isNullOrUndefined(cloneValue) || isEmpty) {
2540
- setFieldValue(name, cloneValue, options, skipClone, skipStateEmit);
2636
+ setFieldValue(name, cloneValue, options, skipClone, skipStateEmit, skipValueRender);
2541
2637
  }
2542
2638
  else {
2543
- setFieldValues(name, cloneValue, options, skipClone, skipStateEmit);
2639
+ setFieldValues(name, cloneValue, options, skipClone, skipStateEmit, skipValueRender);
2544
2640
  }
2545
2641
  }
2546
2642
  if (!isValueUnchanged && !skipStateEmit) {
@@ -3202,6 +3298,7 @@ function createFormControl(props = {}) {
3202
3298
  _state.watch = !!_options.shouldUnregister;
3203
3299
  _state.keepIsValid = !!keepStateOptions.keepIsValid;
3204
3300
  _state.action = false;
3301
+ _state.actionArrayLengths.clear();
3205
3302
  if (!keepStateOptions.keepErrors) {
3206
3303
  _formState.errors = {};
3207
3304
  }
@@ -3274,6 +3371,7 @@ function createFormControl(props = {}) {
3274
3371
  ...formState,
3275
3372
  };
3276
3373
  };
3374
+ _subjects.state.subscribe({ next: _setFormState });
3277
3375
  const _resetDefaultValues = () => isFunction(_options.defaultValues) &&
3278
3376
  _options.defaultValues().then((values) => {
3279
3377
  reset(values, _options.resetOptions);
@@ -3443,8 +3541,14 @@ function useForm(props = {}) {
3443
3541
  }
3444
3542
  const control = _formControl.current.control;
3445
3543
  control._options = props;
3544
+ const { resyncIfNeeded, snapshot } = useResyncOnReconnect();
3446
3545
  useIsomorphicLayoutEffect(() => {
3447
- const sub = control._subscribe({
3546
+ const getCurrentFormState = () => ({
3547
+ ...control._formState,
3548
+ defaultValues: control._defaultValues,
3549
+ });
3550
+ resyncIfNeeded(true, getCurrentFormState, updateFormState);
3551
+ const unsubscribe = control._subscribe({
3448
3552
  formState: control._proxyFormState,
3449
3553
  callback: () => updateFormState({
3450
3554
  ...control._formState,
@@ -3457,8 +3561,11 @@ function useForm(props = {}) {
3457
3561
  isReady: true,
3458
3562
  }));
3459
3563
  control._formState.isReady = true;
3460
- return sub;
3461
- }, [control]);
3564
+ return () => {
3565
+ unsubscribe();
3566
+ snapshot(true, getCurrentFormState);
3567
+ };
3568
+ }, [control, resyncIfNeeded, snapshot]);
3462
3569
  React.useEffect(() => control._disableForm(props.disabled), [control, props.disabled]);
3463
3570
  React.useEffect(() => {
3464
3571
  if (props.mode) {