React hook form useController typescript type error

Viewed 4385

Im trying to split the code in this example: https://codesandbox.io/s/usecontroller-0o8px

but Im getting a type error when Im passing control={control} to input component.

(JSX attribute) control?: Control<FieldValues, object> | undefined
Type 'Control<formValues, object>' is not assignable to type 'Control<FieldValues, object>'.
  The types of '_options.resolver' are incompatible between these types.
    Type 'Resolver<formValues, object> | undefined' is not assignable to type 'Resolver<FieldValues, object> | undefined'.
      Type 'Resolver<formValues, object>' is not assignable to type 'Resolver<FieldValues, object>'.ts(2322)
controller.d.ts(22, 5): The expected type comes from property 'control' which is declared here on type 'IntrinsicAttributes & { label?: string | undefined; supportive?: string | undefined; } & UseControllerProps<FieldValues, string> & InputHTMLAttributes<...> & { ...; }'

Here is my input component

type Props = {
  label?: string
  supportive?: string
} & UseControllerProps &
  InputHTMLAttributes<HTMLInputElement>

const InputComp: FC<Props> = ({ label, supportive, ...props }) => {

  ...

  const {
    field,
    fieldState: { invalid, isTouched, isDirty },
    formState: { touchedFields, dirtyFields },
  } = useController(props)

  return <input {...field} />
}

when I pass formValues type to UseControllerProps<formValues> the error is gone.

I want to use the input component in different forms with different formValues How can I achieve that?

3 Answers

Here's a solution that worked for me. I used this & the link to the original code snippet for help.

I have a reusable Headless UI ListBox component that I wanted to pass dropdown values into along with my original form interface.

Reference to React Hook Form origination:

enum State {
  AL = "AL",
  ...
}

export interface UserRegistration {
  firstName: string
  lastName: string
  streetAddress: string
  streetAddress2: string
  city: string
  state: State
  zipCode: string
}

const registration = () => {

  const {
    control
  } = useForm<UserRegistration>()

  return (
    ...
    
    <Dropdown
      control={control}
      name="state"
      dropdownOptions={State}
    />
  )
}

Reusable Component:

import { Fragment } from 'react'
import { Listbox, Transition } from '@headlessui/react'
import { useController, UseControllerProps } from 'react-hook-form'    

interface DropdownProps {
  dropdownOptions: any
}

const Dropdown = (props: DropdownProps & UseControllerProps<UserRegistration>) => {
  const {
    field: { value, onChange },
  } = useController(props)

  return (
    <div>
      <Listbox value={value} onChange={onChange}>
        <div className="relative">
          <Listbox.Button>
            <span>{value}</span>
            <span>
              <SelectorIcon aria-hidden="true" />
            </span>
          </Listbox.Button>
          <Transition
            as={Fragment}
            leave="transition ease-in duration-100"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <Listbox.Options>
              {Object.keys(props.dropdownOptions).map((value, index) => (
                <Listbox.Option
                  key={index}
                  value={value}
                >
                  {({ selected }) => (
                    <>
                      <span>
                        {value}
                      </span>
                      {selected ? (
                        <span>
                          <CheckIcon aria-hidden="true" />
                        </span>
                      ) : null}
                    </>
                  )}
                </Listbox.Option>
              ))}
            </Listbox.Options>
          </Transition>
        </div>
      </Listbox>
    </div>
  )
}

I have a less elegant solution(Just wrap your custom components with a hook), it works:

custom component:

import { FC } from "react"
import { TextField } from "@mui/material"
import { TextFieldProps } from "@mui/material"
import { Controller, FieldValues, UseControllerProps } from 'react-hook-form'

export const useRHFTextField = <T extends FieldValues>() => {

  const RHFTextField: FC<TextFieldProps & UseControllerProps<T>> = props => {
    const {
      control,
      name,
      defaultValue,
      ...other
    } = props

    return (
      <Controller
        control={control}
        name={name}
        defaultValue={defaultValue}
        render={({ field }) => (
          <TextField
            {...field}
            {...other}
            variant="standard"
          />
        )}
      />
    )
  }

  return RHFTextField
}

useage:

import { useRHFTextField } from "../components/RHFTextField"
...

export const BaseInfo = () => {
  
  const RHFTextField = useRHFTextField<IBaseInfo>()

  const { control,... } = useForm<IBaseInfo>({
    ...
    defaultValues,
  })

  ...

  return (
    <Box component="form" ...>
      <RHFTextField control={control} label="username" name="username" defaultValue="" />
      <RHFTextField control={control} label="password" name="password" defaultValue="" />
    </Box>
  )
}
Related