Issue
Context: I am using react-hook-forms to create a list of text fields (controlled by the controller).I'm finding that I'm writing quite a bit of similar code and thought to create a generic text field.
My problem is two fold:
- How do I pass through the properties of my interface that populate the
name
of the textfield? - How do I pass through the generic object type successfully though props for use in the component leveraging react-hook-forms?
The code I have is as follows:
interface GenericTextfieldProps<T extends FieldValues> {
formLabel: string;
name: keyof T;
control: Control<T>;
};
function GenericTextfield<T extends FieldValues>({
formLabel,
name,
control,
}: GenericTextfieldProps<T>) {
return (
<Stack direction="column" spacing={0.5}>
<Controller
render={({ field }) => (
<TextField
{...field}
id="textfield"
label=""
size="small"
fullWidth
sx={{ ml: 3 }}
disabled
variant="outlined"
/>
)}
name={name}
control={control}
/>
</Stack>
);
}
However, when I translated my code to make it more generic (i.e parsing through a generic type), name={name}
started throwing the following string error:
Type 'keyof T' is not assignable to type 'Path<T>'.
For Q1:
First I tried name: string in props. This did not work as react-hook-forms needs the actual key to allow the controller to do it's magic.
I determined from this post 9 months ago that I could use keyof {my typescript object}
in props. This was very successful.
However, when I translated my code to make it more generic (i.e parsing through a generic type), name={name}
started throwing the following string error:
Type 'keyof T' is not assignable to type 'Path<T>'.
Looking more into react-hook-forms, it seems like there is some sort of relative Field path which needs the keys and values from the actual typescript object. I also think this stackoverflow post is related but unfortunately there are no answers there that I can reference.
I think it has something to do with how the form finds the path with the keys and values (gleaned from the controller.d.ts file here:
export declare type UseControllerProps<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = {
name: TName;
rules?: Omit<RegisterOptions<TFieldValues, TName>, 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'>;
shouldUnregister?: boolean;
defaultValue?: FieldPathValue<TFieldValues, TName>;
control?: Control<TFieldValues>;
};
Ideally, if anyone has done this before I would be looking for an elegant solution using react-hook-forms in a more generic manner. Note: I'm not wedded to using keyOf
any other solutions/ideas are welcome as well.
Solution
You were on the right track with UseControllerProps
.
By extending your props interface with UseControllerProps<T>
where T
has been extended with FieldValues
, you now have access to set the Controller props that you would like to consume.
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import { Controller, FieldValues, UseControllerProps } from 'react-hook-form';
interface GenericTextfieldProps<T extends FieldValues>
extends UseControllerProps<T> {
formLabel: string;
disabled?: boolean;
}
function GenericTextfield<T extends FieldValues>({
name,
control,
formLabel,
disabled = false,
}: GenericTextfieldProps<T>) {
return (
<Stack direction="column" spacing={0.5}>
<Controller
render={({ field }) => (
<TextField
{...field}
id="textfield"
label={formLabel}
size="small"
fullWidth
sx={{ ml: 3 }}
disabled={disabled}
variant="outlined"
/>
)}
name={name}
control={control}
/>
</Stack>
);
}
Answered By - Imogen T
Answer Checked By - - David Goodson (ReactFix Volunteer)