Skip to content

Commit ba2df7b

Browse files
authored
🐞 fix(useForm): make values and defaultValues work correctly with createFormControl and useMemo (#12675)
* Revert "🐞 fix #12665 regression on values over take default values" This reverts commit ef80ff6. * Revert "🥹 close #12665 issue with values not populate form" This reverts commit d9e7e4d. * test: add test case for #12665 * fix(vscode): fix running individual tests and debugging * test: verify setting values doesn't unregister fields * fix(createFormControl): don't unregister fields on reset * fix(useForm): set default values if given along with a form control Fixes #12665 .
1 parent d9e7e4d commit ba2df7b

File tree

8 files changed

+155
-22
lines changed

8 files changed

+155
-22
lines changed

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"disableOptimisticBPs": true,
1010
"cwd": "${workspaceFolder}",
1111
"runtimeExecutable": "pnpm",
12-
"args": ["test", "--", "--runInBand", "--watchAll=false"]
12+
"args": ["test", "--runInBand", "--watchAll=false"]
1313
}
1414
]
1515
}

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"source.fixAll.eslint": "explicit"
44
},
55
"typescript.tsdk": "node_modules/typescript/lib",
6-
"jest.jestCommandLine": "pnpm test --",
6+
"jest.jestCommandLine": "pnpm test",
77
"jest.autoRun": {
88
"watch": false,
99
"onSave": "test-file",

src/__tests__/useController.test.tsx

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React, { useEffect, useMemo, useState } from 'react';
22
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
33

44
import { Controller } from '../controller';
5-
import { Control, FieldPath, FieldValues } from '../types';
5+
import { Control, FieldPath, FieldValues, UseFormReturn } from '../types';
66
import { useController } from '../useController';
77
import { useForm } from '../useForm';
88
import { FormProvider, useFormContext } from '../useFormContext';
@@ -29,6 +29,72 @@ describe('useController', () => {
2929
render(<Component />);
3030
});
3131

32+
it('component using the hook can be memoized', async () => {
33+
function App() {
34+
const form = useForm({
35+
values: { login: 'john' },
36+
});
37+
38+
return useMemo(() => <LoginField form={form} />, [form]);
39+
}
40+
41+
function LoginField({ form }: { form: UseFormReturn<{ login: string }> }) {
42+
const ctrl = useController({
43+
name: 'login',
44+
control: form.control,
45+
});
46+
47+
return <input {...ctrl.field} />;
48+
}
49+
50+
render(<App />);
51+
52+
const input = screen.getAllByRole<HTMLInputElement>('textbox')[0];
53+
expect(input.value).toBe('john');
54+
55+
fireEvent.input(input, { target: { value: 'abc' } });
56+
expect(input.value).toBe('abc');
57+
});
58+
59+
it("setting values doesn't cause fields to be unregistered", async () => {
60+
function App() {
61+
const [values, setValues] = useState<{ login: string } | undefined>();
62+
63+
const form = useForm({
64+
values,
65+
});
66+
67+
useEffect(() => {
68+
setTimeout(() => {
69+
setValues({ login: 'john' });
70+
}, 100);
71+
}, []);
72+
73+
return useMemo(
74+
() => values?.login && <LoginField form={form} />,
75+
[values, form],
76+
);
77+
}
78+
79+
function LoginField({ form }: { form: UseFormReturn<{ login: string }> }) {
80+
const ctrl = useController({
81+
name: 'login',
82+
control: form.control,
83+
defaultValue: 'john',
84+
});
85+
86+
return <input value={ctrl.field.value} onChange={ctrl.field.onChange} />;
87+
}
88+
89+
render(<App />);
90+
91+
const input = await screen.findByRole<HTMLInputElement>('textbox');
92+
expect(input.value).toBe('john');
93+
94+
fireEvent.input(input, { target: { value: 'jane' } });
95+
expect(input.value).toBe('jane');
96+
});
97+
3298
it('should only subscribe to formState at each useController level', async () => {
3399
const renderCounter = [0, 0];
34100
type FormValues = {

src/__tests__/useForm.test.tsx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
FieldValues,
1717
FormState,
1818
RegisterOptions,
19+
SubmitHandler,
1920
UseFormGetFieldState,
2021
UseFormRegister,
2122
UseFormReturn,
@@ -24,7 +25,7 @@ import {
2425
import isFunction from '../utils/isFunction';
2526
import noop from '../utils/noop';
2627
import sleep from '../utils/sleep';
27-
import { Controller, useFieldArray, useForm } from '../';
28+
import { Controller, createFormControl, useFieldArray, useForm } from '../';
2829

2930
jest.useFakeTimers();
3031

@@ -2498,4 +2499,57 @@ describe('useForm', () => {
24982499
).toBeInTheDocument();
24992500
});
25002501
});
2502+
2503+
describe('when given formControl', () => {
2504+
it('accepts default values', async () => {
2505+
type FormValues = {
2506+
firstName: string;
2507+
};
2508+
2509+
const { register, handleSubmit, formControl } =
2510+
createFormControl<FormValues>();
2511+
2512+
function FormComponent({
2513+
onSubmit,
2514+
defaultValues,
2515+
}: {
2516+
defaultValues: FormValues;
2517+
onSubmit: SubmitHandler<FormValues>;
2518+
}) {
2519+
useForm({
2520+
formControl,
2521+
defaultValues,
2522+
});
2523+
return (
2524+
<form onSubmit={handleSubmit(onSubmit)}>
2525+
<input {...register('firstName')} placeholder="First Name" />
2526+
<input type="submit" />
2527+
</form>
2528+
);
2529+
}
2530+
2531+
function App() {
2532+
const [state, setState] = React.useState('');
2533+
return (
2534+
<div>
2535+
<FormComponent
2536+
defaultValues={{ firstName: 'Emilia' }}
2537+
onSubmit={(data) => {
2538+
setState(JSON.stringify(data));
2539+
}}
2540+
/>
2541+
<pre>{state}</pre>
2542+
</div>
2543+
);
2544+
}
2545+
2546+
render(<App />);
2547+
2548+
const input = screen.getAllByRole<HTMLInputElement>('textbox')[0];
2549+
expect(input.value).toBe('Emilia');
2550+
2551+
fireEvent.input(input, { target: { value: 'abc' } });
2552+
expect(input.value).toBe('abc');
2553+
});
2554+
});
25012555
});

src/__tests__/useForm/watch.test.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,17 @@ describe('watch', () => {
510510

511511
expect(await screen.findByText('1234')).toBeVisible();
512512

513-
expect(watchedData).toEqual([{}, {}, { test: '1234' }]);
513+
expect(watchedData).toEqual([
514+
{},
515+
{
516+
test: '1234',
517+
data: '1234',
518+
},
519+
{
520+
test: '1234',
521+
data: '1234',
522+
},
523+
]);
514524
});
515525

516526
it('should not be able to overwrite global watch state', () => {

src/__tests__/useFormState.test.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect } from 'react';
1+
import React from 'react';
22
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
33

44
import { Controller } from '../controller';
@@ -810,13 +810,7 @@ describe('useFormState', () => {
810810
}
811811

812812
function App() {
813-
const [values, setValues] = React.useState({ firstName: '' });
814-
815-
useEffect(() => {
816-
setValues({ firstName: 'test' });
817-
}, [setValues]);
818-
819-
return <Form values={values} />;
813+
return <Form values={{ firstName: 'test' }} />;
820814
}
821815

822816
render(<App />);

src/logic/createFormControl.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ export function createFormControl<
121121
errors: _options.errors || {},
122122
disabled: _options.disabled || false,
123123
};
124-
let _fields: FieldRefs = {};
124+
const _fields: FieldRefs = {};
125125
let _defaultValues =
126126
isObject(_options.defaultValues) || isObject(_options.values)
127-
? cloneObject(_options.defaultValues || _options.values) || {}
127+
? cloneObject(_options.values || _options.defaultValues) || {}
128128
: {};
129129
let _formValues = _options.shouldUnregister
130130
? ({} as TFieldValues)
@@ -1340,14 +1340,15 @@ export function createFormControl<
13401340
}
13411341
}
13421342

1343-
_fields = {};
1343+
for (const fieldName of _names.mount) {
1344+
setValue(
1345+
fieldName as FieldPath<TFieldValues>,
1346+
get(values, fieldName),
1347+
);
1348+
}
13441349
}
13451350

1346-
_formValues = _options.shouldUnregister
1347-
? keepStateOptions.keepDefaultValues
1348-
? (cloneObject(_defaultValues) as TFieldValues)
1349-
: ({} as TFieldValues)
1350-
: (cloneObject(values) as TFieldValues);
1351+
_formValues = cloneObject(values) as TFieldValues;
13511352

13521353
_subjects.array.next({
13531354
values: { ...values },

src/useForm.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ export function useForm<
7171
...(props.formControl ? props.formControl : createFormControl(props)),
7272
formState,
7373
};
74+
75+
if (
76+
props.formControl &&
77+
props.defaultValues &&
78+
!isFunction(props.defaultValues)
79+
) {
80+
props.formControl.reset(props.defaultValues, props.resetOptions);
81+
}
7482
}
7583

7684
const control = _formControl.current.control;

0 commit comments

Comments
 (0)