React Hook Form (RHF) is a form management library commonly used in the React ecosystem.
Mainly responsible for:
- Register and manage form fields.
- Collect user input.
- Field validation and error management.
- Form submission.
- Manage Dirty, Touched, Submitting and other states.
- Minimize unnecessary React re-rendering.
React Hook Form mainly manages the client form status, while TanStack Query mainly manages the server status.
Typical workflow:
Django API
↓
TanStack Query
↓
Server Data
↓
React Hook Form
│
├── Field Registration
├── Form Values
├── Validation
├── Error Management
└── Form State
↓
handleSubmit()
↓
TanStack Query Mutation
↓
Axios
↓
Django API
1. Basic usage
Installation:
pnpm add react-hook-form
Basic example:
import { useForm } from "react-hook-form";
type FormData = {
username: string;
email: string;
};
function ProfileForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
defaultValues: {
username: "",
email: "",
},
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("username", {
required: "Username is required",
})}
/>
{errors.username && <p>{errors.username.message}</p>}
<input
{...register("email", {
required: "Email is required",
})}
/>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
);
}
Compared to the traditional React form, RHF does not require a separate useState to be created for each input field.
2. useForm: form controller
useForm() is Hook, the core of RHF.
const form = useForm<FormData>({
defaultValues: {
username: "",
email: "",
},
mode: "onSubmit",
});
It creates and returns a set of form management methods:
const {
register,
control,
handleSubmit,
watch,
getValues,
setValue,
reset,
trigger,
setError,
clearErrors,
formState,
} = useForm<FormData>();
useForm Common configuration
| parameters | function |
|---|---|
defaultValues | Initialize form values |
values | Reactively update form values based on external data |
mode | Trigger mode for first verification |
reValidateMode | Revalidation mode for error fields after submission |
resolver | Access external verification library |
criteriaMode | Control how many validation errors are collected per field |
shouldFocusError | Whether to focus on the error field when validation fails |
shouldUnregister | Whether to cancel the registration when the field is unloaded |
disabled | Disable entire form |
Validation Mode
| Mode | Verification timing |
|---|---|
onSubmit | When submitting, the default |
onBlur | When a field loses focus |
onChange | When input changes |
onTouched | After the first Blur, subsequent Change |
all | When Blur and Change |
For example:
useForm<FormData>({
mode: "onBlur",
reValidateMode: "onChange",
});
It means that the first verification is mainly triggered at Blur, and if there are error fields after submission, it will be re-verified at Change.
onChange mode may perform frequent validations, so performance needs to be taken into account for complex forms.
3. register: Registration form field
register() registers native input elements to RHF.
<input {...register("username")} />
Roughly equivalent to:
<input
name="username"
onChange={...}
onBlur={...}
ref={...}
/>
register() mainly returns:
name: field name.onChange: Field change processing function.onBlur: Lost focus processing function.ref: Input element reference.
Differences from traditional controlled components
Traditional React:
const [username, setUsername] = useState("");
<input
value={username}
onChange={(event) => setUsername(event.target.value)}
/>;
RHF:
<input {...register("username")} />
RHF usually adopts an uncontrolled management method for native input, and does not need to update the field value through the component's own setState every time it is input.
This helps reduce unnecessary component re-rendering.
Nested objects
register("address.city");
register("address.country");
Corresponds to:
{
address: {
city: "Tokyo",
country: "Japan"
}
}
Array field
register("users.0.name");
Corresponds to:
{
users: [{ name: "Alice" }];
}
Convert input type
The value of HTML Input is usually a string.
You can use:
<input
type="number"
{...register("age", {
valueAsNumber: true,
})}
/>
Or:
<input
{...register("price", {
setValueAs: (value) => Number(value),
})}
/>
Avoid manually converting all numeric fields after the form is submitted.
4. handleSubmit: Form submission
handleSubmit() is responsible for verification before submission and calling the corresponding callback.
Browser Submit
↓
handleSubmit
↓
Validation
│
├── Success → onValid(data)
│
└── Failure → onInvalid(errors)
Basic usage
const onSubmit = (data: FormData) => {
console.log(data);
};
<form onSubmit={handleSubmit(onSubmit)}>
handleSubmit(onSubmit) Returns a function that can be used as a form submission handler.
Success and failure callbacks
import type {
SubmitHandler,
SubmitErrorHandler,
} from "react-hook-form";
const onValid: SubmitHandler<FormData> = (data) => {
console.log(data);
};
const onInvalid: SubmitErrorHandler<FormData> = (errors) => {
console.log(errors);
};
<form onSubmit={handleSubmit(onValid, onInvalid)}>
Asynchronous submission
const onSubmit = async (data: FormData) => {
try {
await updateProfile(data);
} catch (error) {
console.error(error);
}
};
For asynchronous submission, handleSubmit will wait for Promise returned by the callback.
RHF will not automatically swallow exceptions thrown by the submission handler, so it should be handled by itself.
5. Validation: Field validation
The simplest verification can be written directly in register().
<input
{...register("username", {
required: "Username is required",
minLength: {
value: 3,
message: "At least 3 characters",
},
maxLength: {
value: 20,
message: "At most 20 characters",
},
})}
/>
Common validation rules
| rules | function |
|---|---|
required | Required |
minLength | minimum length |
maxLength | maximum length |
min | minimum value |
max | maximum value |
pattern | regular expression |
validate | Custom validation function |
For example:
register("age", {
valueAsNumber: true,
validate: (value) => value >= 18 || "Must be at least 18",
});
Custom asynchronous validation
register("username", {
validate: async (value) => {
const available = await checkUsername(value);
return available || "Username already exists";
},
});
This type of verification may require anti-shaking or adjusting the verification triggering time to avoid calling the server for every input.
At the same time, front-end verification cannot replace Django Serializer’s server-side verification.
6. Resolver: External verification library
Resolver is an adapter between RHF and an external Schema Validation Library.
Common validation libraries include:
- Zod
- Yup
- Valibot
- Joi
Install Zod:
pnpm add zod @hookform/resolvers
Example:
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
const schema = z.object({
username: z.string().min(3).max(20),
email: z.email(),
});
type FormData = z.infer<typeof schema>;
const form = useForm<FormData>({
resolver: zodResolver(schema),
});
resolver is responsible for converting external validation results into field errors that RHF can understand.
Do I have to use Zod?
Not required.
Simple forms can be used directly:
register("username", {
required: true,
minLength: 3,
});
If you need cross-field validation, complex object validation, or reuse Schema, consider Zod.
If the project has already generated the TypeScript type through OpenAPI + Orval, you can choose whether to add client runtime verification according to actual needs, without having to mechanically repeat the definition of all API Schema.
7. formState: form status
formState is a very important set of states in RHF.
const {
formState: {
errors,
isDirty,
dirtyFields,
touchedFields,
isValid,
isValidating,
isSubmitting,
isSubmitted,
isSubmitSuccessful,
submitCount,
},
} = useForm<FormData>();
| Status | meaning |
|---|---|
errors | Field validation error |
isDirty | Whether the current overall form value is different from the default value |
dirtyFields | Dirty status of each field |
touchedFields | Fields touched by the user |
isValid | Is the form verification result valid? |
isValidating | Is verification in progress? |
isSubmitting | Whether the commit callback is being executed |
isSubmitted | Has it been submitted? |
isSubmitSuccessful | Whether the most recent submission was completed successfully |
submitCount | Number of submissions |
isDirty and dirtyFields
Assumptions:
defaultValues: {
username: "Alice",
email: "alice@example.com",
}
The user changes the username to Bob:
isDirty = true
dirtyFields = {
username: true
}
If the user changes the username back to Alice:
isDirty = false
Because the current value of the entire form is again consistent with defaultValues.
But dirtyFields has field-level state semantics and should not be considered to be always fully synchronized with isDirty.
It is recommended to provide complete and stable defaultValues for all fields, otherwise the Dirty judgment may not meet expectations.
touchedFields
Blur after the field gets focus:
Focus → Blur
↓
touchedFields.username = true
Field values are not required to be modified.
isSubmitting
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
Note: isSubmitting tracks the submission process of handleSubmit.
If onSubmit just calls:
mutation.mutate(data);
Then returns immediately, isSubmitting usually does not wait continuously for Mutation to complete.
Should use:
const onSubmit = async (data: FormData) => {
await mutation.mutateAsync(data);
};
Or directly use TanStack Query’s mutation.isPending management server to submit the status.
8. watch: Subscription form value
watch() is used to obtain form values, and UI is updated as the fields change.
For example:
const country = watch("country");
return (
<>
<select {...register("country")}>
<option value="JP">Japan</option>
<option value="US">United States</option>
</select>
{country === "JP" && <input {...register("prefecture")} />}
</>
);
When country changes, the component can display different fields based on the new value.
Common calling methods
watch(); // Listen to the entire form
watch("username"); // Monitor single field
watch(["city", "country"]); // Monitor multiple fields
watch() may cause the component using it to re-render when the associated value changes.
For complex forms, it is recommended to avoid indiscriminately monitoring all fields at the top level.
9. useWatch: Partial subscription
useWatch() is similar to watch(), but limits the effect of re-rendering to the component using it.
import { useWatch } from "react-hook-form";
function CountryDetails({ control }: { control: Control<FormData> }) {
const country = useWatch({
control,
name: "country",
});
return <p>Selected country: {country}</p>;
}
watch and useWatch
| Features | watch | useWatch |
|---|---|---|
| Main functions | Get and subscribe to form values | Get and subscribe to form values |
| Usage | The method returned by useForm | Independent Hook |
| Update scope | May affect form root component | Easier to isolate in subcomponents |
| Applicable scenarios | simple form | Complex forms, partial subscriptions |
For example, in a form containing dozens of fields, if only a certain subcomponent needs to listen to country, it is usually more recommended to use useWatch() within the subcomponent.
10. getValues: Read the current value
getValues() can directly read the current field value, but does not establish a subscription.
const username = getValues("username");
Commonly used in event handling functions:
const handleSomething = () => {
const username = getValues("username");
console.log(username);
};
watch and getValues
| method | Whether to subscribe | Purpose |
|---|---|---|
| watch | Yes | Update UI when value changes |
| useWatch | Yes | Local component update |
| getValues | No | Temporarily read the current value |
If you only read the value once when the button is clicked, usually use getValues().
11. setValue: Actively modify fields
setValue() is used to modify form values through code.
setValue("username", "Bob");
Status updates can also be actively triggered:
setValue("username", "Bob", {
shouldDirty: true,
shouldValidate: true,
shouldTouch: true,
});
| parameters | function |
|---|---|
shouldDirty | Update Dirty status |
shouldValidate | Perform verification |
shouldTouch | Update Touched status |
Example
After the user selects a city, the country is automatically filled in:
setValue("country", "Japan", {
shouldDirty: true,
});
Note: setValue() is not the same as setState() for React.
RHF will decide which components need to be updated based on subscriptions, errors and state changes, usually without causing the entire form to be re-rendered every time.
12. reset: Reset form
reset() is used to reset the value and related status of the form.
reset();
Normally restores to current defaultValues.
Also possible:
reset({
username: "Alice",
email: "alice@example.com",
});
In the default configuration, the new value provided becomes the new default value baseline.
Therefore:
Current Values = New Values
Default Values = New Values
isDirty = false
Get user information asynchronously
const { data: user } = useGetUser();
const form = useForm<FormData>({
defaultValues: {
username: "",
email: "",
},
});
useEffect(() => {
if (user) {
form.reset({
username: user.username,
email: user.email,
});
}
}, [user, form.reset]);
Protect data being edited by users
If the server re-obtains the data but the user has modified some fields, consider:
reset(newData, {
keepDirtyValues: true,
});
This way the Dirty field retains the user's current input and the other fields are updated based on the new data.
This is especially important for the background Refetch combined with TanStack Query.
Otherwise, if reset(data) is executed unconditionally every time the Query data is updated, the user's unsaved input may be overwritten.
13. values: Responsive external data
The newer version RHF supports synchronizing external data into the form using values.
For example:
const form = useForm<FormData>({
defaultValues: {
username: "",
email: "",
},
values: user
? {
username: user.username,
email: user.email,
}
: undefined,
});
Unlike defaultValues, values responds to external data changes.
Note: external data updates may also affect the value currently edited by the user.
Therefore, when editing existing data on the server, you can choose according to your needs:
- Initialize only once: use
reset()or the appropriatevaluesscheme. - Continuous synchronization of server data: use
values. - Preserve user-modified fields: combined with
keepDirtyValues.
14. Controller: Controlled component adapter
RHF can directly use register() for native input.
But some third-party UI components use controlled mode, for example:
- Radix UI Select。
- React Select。
- Date picker.
- Custom Switch.
- Custom combination input component.
Controller is usually used at this time.
import { Controller } from "react-hook-form";
<Controller
name="city"
control={control}
render={({ field, fieldState }) => (
<>
<Select
value={field.value}
onValueChange={field.onChange}
onBlur={field.onBlur}
/>
{fieldState.error && <p>{fieldState.error.message}</p>}
</>
)}
/>;
The role of Controller
Controller relates the value changes of the controlled component to RHF.
Usually provided:
field.name;
field.value;
field.onChange;
field.onBlur;
field.ref;
Among them:
value: Current value.onChange: Write new value back to RHF.onBlur: Notifies the RHF field that it has lost focus.ref: associates underlying input elements and supports functions such as focusing.
For non-native components, whether ref can be passed in directly depends on whether the component exposes the corresponding input element reference.
15. useController
useController() provides similar capabilities to Controller, but in the form of Hook.
const { field, fieldState, formState } = useController({
name: "username",
control,
});
Use:
<Input
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
Suitable for encapsulating common form components:
FormInput
FormSelect
FormSwitch
FormDatePicker
Controller and useController
| Contrast | Controller | useController |
|---|---|---|
| form | React Component | React Hook |
| Main purpose | Directly adapt to third-party components | Encapsulate reusable form components |
| Provide field | Yes | Yes |
| Provide fieldState | Yes | Yes |
The two use similar control mechanisms at the bottom. Usually, you can choose one of them without having to register the same field repeatedly at the same time.
16. useFieldArray: Dynamic field array
useFieldArray() is used to manage an array of fields that can be dynamically added, deleted, or sorted.
For example, travel planning:
{
destinations: [{ city: "Tokyo" }, { city: "Kyoto" }];
}
Example
type FormData = {
destinations: {
city: string;
}[];
};
const { register, control } = useForm<FormData>({
defaultValues: {
destinations: [{ city: "" }],
},
});
const { fields, append, remove, update, move } = useFieldArray({
control,
name: "destinations",
});
Rendering:
{
fields.map((field, index) => (
<input key={field.id} {...register(`destinations.${index}.city`)} />
));
}
New:
append({ city: "" });
Delete:
remove(1);
Move:
move(0, 2);
Common methods
| method | function |
|---|---|
append | Add at the end |
prepend | Add at the beginning |
insert | Insert at specified position |
remove | Delete |
swap | Swap two elements |
move | Move elements |
update | Replace specified item |
replace | replace entire array |
Things to note
Must use:
key={field.id}
Instead of simply using:
key = { index };
Because the index may change after the array is inserted or deleted.
Additionally, update() may cause the corresponding fields to be remounted. If you only need to modify the value of a certain field, consider using setValue().
17. FormProvider and useFormContext
Complex forms are often split into subcomponents:
ProfileForm
├── BasicInfoSection
│ ├── UsernameField
│ └── EmailField
├── AddressSection
│ ├── CountryField
│ └── CityField
└── SubmitButton
If passed layer by layer:
register;
control;
errors;
setValue;
A large number of Props will be generated.
FormProvider can share RHF's form controller through React Context.
parent component
import { FormProvider, useForm } from "react-hook-form";
function ProfileForm() {
const methods = useForm<FormData>();
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<BasicInfoSection />
<AddressSection />
<SubmitButton />
</form>
</FormProvider>
);
}
subcomponent
function UsernameField() {
const {
register,
formState: { errors },
} = useFormContext<FormData>();
return (
<>
<input {...register("username")} />
{errors.username && <p>{errors.username.message}</p>}
</>
);
}
core role
useForm()
↓
FormProvider
↓
React Context
↓
useFormContext()
↓
Child Components
There is no need to pass register, control and other methods layer by layer.
Note: You should generally not nest multiple FormProvider unnecessarily in the same form.
18. useFormState: Partial form status subscription
useFormState() allows the component to subscribe to only the required form state.
For example:
import { useFormState } from "react-hook-form";
function SubmitButton() {
const { control } = useFormContext<FormData>();
const { isDirty, isSubmitting } = useFormState({
control,
});
return (
<button type="submit" disabled={!isDirty || isSubmitting}>
Save
</button>
);
}
In this way, SubmitButton can independently subscribe to the relevant status.
Value of useFormState
For large forms, avoid re-rendering the entire form root component with every field update.
Especially suitable for:
- Independent error message component.
- SubmitButton。
- Field grouping.
- Complex nested forms.
The form status of RHF uses a subscription-based optimization mechanism. It is recommended to only read the status that the component really needs.
19. trigger: Actively perform verification
trigger() Field validation can be performed manually.
Validate the entire form
const valid = await trigger();
if (valid) {
console.log("All fields are valid");
}
Validate a single field
await trigger("email");
Validate multiple fields
await trigger(["username", "email"]);
Common uses
- Multi-step forms.
- Verify before proceeding to the next step.
- After field A changes, re-validate field B.
- Proactively check input before specific button operations.
For example:
const handleNextStep = async () => {
const valid = await trigger(["username", "email"]);
if (valid) {
setStep(2);
}
};
trigger() returns a Boolean value indicating whether the verification passed.
20. setError: Manual setting error
setError() is used to write errors to RHF.
setError("username", {
type: "manual",
message: "Username already exists",
});
You can then pass:
errors.username?.message;
Read the error message.
Handle server-side validation errors
For example Django DRF returns:
{
"username": ["This username already exists."]
}
Server-side errors can be mapped to form fields:
setError("username", {
type: "server",
message: "This username already exists.",
});
Non-field level errors
For example, the server returns a common error:
setError("root.server", {
type: "server",
message: "Failed to save profile",
});
Shown in UI:
{
errors.root?.server && <p>{errors.root.server.message}</p>;
}
setError and isValid
setError() can add error status manually, but the final result of isValid is mainly calculated by the form validation rules.
Therefore, setError() should not be understood as a switch that permanently controls isValid.
Errors manually set on registered fields may also be cleared when the field is revalidated.
21. clearErrors: Clear error
clearErrors() is used to manually clear form errors.
Clear individual fields
clearErrors("username");
Clear multiple fields
clearErrors(["username", "email"]);
Clear all errors
clearErrors();
Clear server errors
clearErrors("root.server");
For example, when the user re-edits a field, previous server errors can be cleared:
<input
{...register("username", {
onChange: () => {
clearErrors("root.server");
},
})}
/>
The difference between clearErrors and trigger
| method | function |
|---|---|
clearErrors() | Clear currently stored errors |
trigger() | Rerun verification |
setError() | Manually add errors |
Important: clearErrors() does not modify the validation rules, nor does it automatically trigger validation or cause isValid to become true. If the value is still illegal, the error may reappear the next time it is verified.
22. React Hook Form + TanStack Query
For editing forms that already have data on the server, the common process is:
TanStack Query
↓
GET User Profile
↓
React Hook Form
↓
User Edits Fields
↓
handleSubmit
↓
useMutation
↓
PATCH User Profile
↓
Update Query Cache
↓
reset(savedData)
Example
type ProfileFormData = {
username: string;
email: string;
};
function ProfileForm({ initialData }: { initialData: ProfileFormData }) {
const queryClient = useQueryClient();
const {
register,
handleSubmit,
reset,
setError,
formState: { errors, isDirty },
} = useForm<ProfileFormData>({
defaultValues: initialData,
});
const mutation = useUserProfilePartialUpdate();
const onSubmit = async (data: ProfileFormData) => {
try {
const updatedProfile = await mutation.mutateAsync({
data,
});
queryClient.setQueryData(
getUserProfileRetrieveQueryKey(),
updatedProfile,
);
reset({
username: updatedProfile.username,
email: updatedProfile.email,
});
} catch (error) {
setError("root.server", {
type: "server",
message: "Unable to save profile",
});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("username")} />
<input {...register("email")} />
{errors.root?.server && <p>{errors.root.server.message}</p>}
<button type="submit" disabled={!isDirty || mutation.isPending}>
{mutation.isPending ? "Saving..." : "Save"}
</button>
</form>
);
}
The above example assumes that Mutation returns the complete latest Profile data.
If the backend only returns a 204 or simple success status, it should use invalidateQueries() to re-fetch the data instead of directly writing the Mutation return value to the Profile cache.
Why reset after saving?
Because after successful saving, the current data has become the new value recognized by the server.
For example:
original username = Alice
↓
User modified to Bob
↓
isDirty = true
↓
Server save Bob
↓
reset({ username: "Bob" })
↓
isDirty = false
In this way, when the user continues editing, the Dirty status will be judged relative to the data just saved.
23. File upload
For file fields you can use:
<input type="file" {...register("avatar")} />
RHF usually receives FileList.
For example:
type AvatarFormData = {
avatar: FileList;
};
const onSubmit = async (data: AvatarFormData) => {
const file = data.avatar?.[0];
if (!file) return;
const formData = new FormData();
formData.append("avatar", file);
await uploadAvatar(formData);
};
For the upload function generated by Orval, OpenAPI Schema and the actual generated parameter type should prevail to avoid repeatedly constructing FormData.
Additionally, browser file inputs cannot set file values arbitrarily like normal text inputs. Therefore the reset and default value handling of file fields need to be considered separately.
24. Performance optimization
One of the advantages of React Hook Form is the reduction of unnecessary re-renders.
Commonly used strategies:
1. Use native input register first
<input {...register("username")} />
It is not necessary to use Controller for every native Input.
2. Complex components use Controller
<Controller
name="city"
control={control}
render={({ field }) => (
<CustomSelect value={field.value} onValueChange={field.onChange} />
)}
/>
3. Avoid the full form watch
It is not recommended to use it indiscriminately at the top level of large forms:
watch();
Can be used instead in the required subcomponent:
useWatch({
control,
name: "country",
});
4. Partial subscription formState
Use:
useFormState({
control,
});
Avoid having all state read and rendered by top-level components.
5. Provide complete defaultValues
useForm<FormData>({
defaultValues: {
username: "",
email: "",
},
});
This helps to accurately determine the Dirty status and avoids the problem of the controlled component encountering the initial value of undefined.
6. Don’t reset frequently
If the server Refetch calls reset() every time, the data being edited by the user may be overwritten.
You can use keepDirtyValues or create a strategy that is initialized only once.
25. FAQ
Q1: Can React Hook Form replace useState?
Only useState, a part of form field state management, can be replaced.
Whether Modal is open, current Tab, page UI status, etc. are still suitable for using React State.
Q2: Do RHF and TanStack Query have overlapping functions?
There is a small amount of overlap, but the main responsibilities are different.
- RHF: Temporary form status that has not been submitted by the user.
- TanStack Query: Server data, cache and request status.
The two are often used in combination.
Q3: Why should I use reset when I already have defaultValues?
Because defaultValues is mainly used for initialization.
Server data is usually obtained asynchronously and needs to be synchronized to the form after the acquisition is completed. reset() is one of the commonly used methods.
Q4: Is Controller more advanced than register?
no.
Both target different component types.
Native Input is usually simpler to use register; complex controlled components are more suitable to use Controller.
Q5: The form validation has passed, why does the server still return 400?
Because front-end verification is only user experience optimization, there is no guarantee that the server will accept the data.
The server may have additional:
- Database unique constraints.
- Permission check.
- Business rules.
- Concurrent state changes.
- Field conversion and validation.
Therefore, server-side validation of Django Serializer must be retained.
Summary
The core content of React Hook Form can be divided into four levels:
| Classification | focus |
|---|---|
| basic form | useForm、register、handleSubmit、validation |
| form status | formState、watch、getValues、setValue、reset |
| Complex components | Controller、useController、useFieldArray、FormProvider |
| advanced capabilities | resolver、trigger、setError、clearErrors、useWatch、useFormState |
Core process:
useForm()
↓
register / Controller
↓
Form Values
↓
Validation
↓
handleSubmit()
↓
TanStack Query Mutation
↓
Axios
↓
Django API
↓
Cache Update / Invalidation
↓
reset()
For the React + TypeScript + Orval + TanStack Query project, the most important thing is to understand:
- The difference between
registerandController. - The relationship between
defaultValues,isDirtyanddirtyFields. - The difference between
watch,useWatchandgetValues. resetHow to initialize and redefine form bases.- How server-side validation errors are displayed via
setError. - How to manage asynchronous commits using
mutateAsync. - Mutation How to synchronize server cache and form status after success.