-
Notifications
You must be signed in to change notification settings - Fork 11
Description
I want to create type-safe, reusable components for complex sub-forms.
Consider the following example:
type FormValues = {
name: string
address: Address
}
const MyForm = () => {
const form = useForm()
const handleSubmit = (data: FormValues) => console.log(data)
return (
<form onSubmit={form.handleSubmit(handleSubmit)}>
<input {...form.register("name")} />
<AddressInput form={form} name="address" />
</form>
)
}
type Address = {
street: string
city: string
country: CountryCode
}
type AddressInputProps = {
form: UseFormReturn
name: string
}
const AddressInput = (props: AddressInputProps) => (
<div>
<input {...form.register(`${props.name}.street`)} />
<input {...form.register(`${props.name}.city`)} />
<CountrySelect control={props.form.control} name={`${props.name}.country`} />
</div>
)
Now, I want to use AddressInput
in different forms. To be able to use both register
and control
, it makes most sense to just pass the entire form
object to the input. I can get type safety for the AddressInput.name
prop by using FieldPathByValue
:
type AddressInputProps<
TFieldValues extends FieldValues,
TPath extends FieldPathByValue<TFieldValues, Address>,
> = {
name: TPath
form: UseFormReturn<TFieldValues>
}
// "name" is now type checked and has autocomplete
<AddressInput form={form} name="address" />
However, since Typescript does not support narrowing generic string literals inside other literals, the following now throws an error:
// Inside AddressInput
<input {...props.form.register(`${props.name}.street`)} />
// Argument of type '`${TPath}.street`' is not assignable to parameter of type 'Path<TFieldValues>'.
// Type '`${string}.street`' is not assignable to type 'Path<TFieldValues>'. [2345]
Even though props.name
is of type TPath
and the type `${TPath}.street`
should satisfy FieldPath<TFieldValues>
, it gets broadened to `${string}.street`
. which does not.
It's also arguably unergonomic having to prefix every form field name with ${props.name}
inside AddressInput
, and the input theoretically has access to the entire outer form.
Possible Solutions
I can see multiple ways to solve this problem. Essentially, there would have to be a way to create a "sub-form" that is scoped to a specific field.
This could either be a function on UseFormReturn
:
const { form: addressForm, name: addressName } = form.registerSubForm("address")
<AddressInput form={addressForm} name={addressName} />
// Or the shorthand:
<AddressInput {...form.registerSubForm("address")} />
Or a new useSubForm
hook: that can be called from within AddressInput
:
const AddressInput = (props: AddressInputProps) => {
// The arguments here can be constrainted so that they only accept
// FieldPathByValue<TFieldValues, Address>
const form = useSubForm(props.form, props.name)
// All fields within form are now scoped to the "address" field, including
// watch, control, etc. - basically every path is prefixed with "address.".
return (
<div>
<input {...form.register("street")} />
<input {...form.register("city")} />
<CountrySelect control={form.control} name="country" />
</div>
)
}
Alternatives
All alternatives I've considered make tradeoffs in type safety or performance:
- It's possible to just pass a control to the
AddressInput
, but every update would rerender the entire component, which is fine in this case - but not when there are multiple nested fields even within a sub-component. It's also not possible to useregister
in this case. - It's possible to actually use sub-forms, but then all state (validation, dirty, touched, handlers) needs to be passed up, which is not ideal and not always possible.
- The approach in the initial example is a possibility, but does not offer type safety and is unergonomic because every field needs to be prefixed.
I would love to use react-hook-form for a large project I'm working on, but we have lots of complex components with nested fields that are used in multiple forms.
Having an API like this would go a long way when it comes to composing forms from smaller, isolated parts, and even allowing libraries containing complex input components to be published.
If this is a feature you would consider, I'd be happy to take care of the implementation.