'How to create a reusable react-hook-form component with TypeScript generics?
Person
component is reused in two different forms:
Person.tsx
import { UseFormReturn } from "react-hook-form";
import { FieldPaths } from "./types";
type Props<TFormValues> = {
methods: UseFormReturn<TFormValues>;
fieldPaths: FieldPaths<TFormValues>;
};
const Person = <TFormValues,>({
methods,
fieldPaths
}: Props<TFormValues>) => {
const { register } = methods;
return (
<div className="person">
<div className="name-field">
<input {...register(fieldPaths.firstName)} placeholder="First name" />
<input {...register(fieldPaths.lastName)} placeholder="Last name" />
</div>
<div className="radio-group">
<label>
<input type="radio" value="free" {...register(fieldPaths.status)} />{" "}
Free
</label>
<label>
<input type="radio" value="busy" {...register(fieldPaths.status)} />{" "}
Busy
</label>
</div>
</div>
);
};
export { Person };
SimpleForm.tsx
import { useForm } from "react-hook-form";
import { Person } from "./Person";
import { TPerson } from "./types";
import { emptyPerson } from "./utils";
type SimpleFormValues = {
person: TPerson;
};
const simpleFormFieldPaths = {
firstName: "person.name.first",
lastName: "person.name.last",
status: "person.status"
} as const;
const SimpleForm = () => {
const methods = useForm<SimpleFormValues>({
defaultValues: {
person: emptyPerson
}
});
const { handleSubmit } = methods;
const onSubmit = ({ person }: SimpleFormValues) => {
console.log(JSON.stringify(person, null, 2));
};
return (
<div>
<h2>Simple form</h2>
<form method="post" onSubmit={handleSubmit(onSubmit)}>
<Person methods={methods} fieldPaths={simpleFormFieldPaths} />
<button type="submit">Submit</button>
</form>
</div>
);
};
export { SimpleForm };
ComplexForm.tsx
import { useFieldArray, useForm } from "react-hook-form";
import { Person } from "./Person";
import { TPerson } from "./types";
import { emptyPerson } from "./utils";
type ComplexFormValues = {
artists: TPerson[];
};
const ComplexForm = () => {
const methods = useForm<ComplexFormValues>({
defaultValues: {
artists: [emptyPerson, emptyPerson, emptyPerson]
}
});
const { control, handleSubmit } = methods;
const { fields } = useFieldArray({
control,
name: "artists"
});
const onSubmit = ({ artists }: ComplexFormValues) => {
console.log(JSON.stringify(artists, null, 2));
};
return (
<div>
<h2>Complex form</h2>
<form method="post" onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => {
const complexFormFieldPaths = {
firstName: `artists.${index}.name.first`,
lastName: `artists.${index}.name.last`,
status: `artists.${index}.status`
} as const;
return (
<Person
methods={methods}
fieldPaths={complexFormFieldPaths}
key={field.id}
/>
);
})}
<button type="submit">Submit</button>
</form>
</div>
);
};
export { ComplexForm };
types.ts
import { FieldPath } from "react-hook-form";
export type FieldPaths<TFormValues> = {
firstName: FieldPath<TFormValues>;
lastName: FieldPath<TFormValues>;
status: FieldPath<TFormValues>;
};
export type TPerson = {
name: {
first: string;
last: string;
};
status: "" | "free" | "busy";
};
utils.ts
import { TPerson } from "./types";
export const emptyPerson: TPerson = {
name: {
first: "",
last: ""
},
status: ""
};
It works beautifully, but if I try to setValue
in Person
, TypeScript throws an error:
const { setValue } = methods;
setValue(fieldPaths.firstName, "David");
Argument of type 'string' is not assignable to parameter of type 'UnpackNestedValue<PathValue<TFormValues, Path>>'
How could I fix this?
I believe what I want is to hint TypeScript that PathValue(TFormValues, typeof fieldPaths.firstName)
is always going to be a string
, and then it should work, but I'm not sure how to achieve that.
Note: PathValue
is defined here.
Solution 1:[1]
Looks like this used to be fixed in late react-hook-form@6; along had came v7 which reintroduced the problem and seems to have been carried into v8 beta as well.
Regarding your question
"hint TypeScript that
PathValue(TFormValues, typeof fieldPaths.firstName)
is always going to be astring
"
This will likely not work, because of what setValue
has been defined to accept.
Ideally we'd push the issue to the authors and hope for a speedy fix, but in the absence of that, there are two solutions here:
Either alias
UnpackNestedValue
and cast to itimport { UnpackNestedValue, PathValue, Path } from "react-hook-form"; type ValueArgument<f> = UnpackNestedValue<PathValue<f, Path<f>>>; setValue(fieldPaths.firstName, "David" as ValueArgument<TFormValues>);
Extend the types all the way up to accept primitives
import { UseFormReturn, FieldValues, FieldPath, UnpackNestedValue, FieldPathValue, SetValueConfig, } from "react-hook-form"; import { FieldPaths } from "./types"; type UseMyFormSetValue<TFieldValues extends FieldValues> = < TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> >( name: TFieldName, value: UnpackNestedValue<FieldPathValue<TFieldValues, TFieldName>> | string, options?: SetValueConfig ) => void; interface UseMyFormReturn<TFormValues> extends UseFormReturn<TFormValues> { setValue: UseMyFormSetValue<TFormValues>; } type Props<TFormValues> = { methods: UseMyFormReturn<TFormValues>; fieldPaths: FieldPaths<TFormValues>; }; const Person = <TFormValues>({ methods, fieldPaths }: Props<TFormValues>) => { const { register, setValue } = methods; const changeName = () => { setValue(fieldPaths.firstName, "David"); };
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | diedu |