Nilan Saha
Nilan Saha

Reputation: 249

How to use register from react-hook-forms with Headless UI Combobox

I have this piece of code where I am trying to wire up the Combobox Component from Headless UI with react-hook-form. Whenever I try to enter a different value and select a different option it gives me an error saying - Cannot read properties of undefined (reading 'name')

I have tried a lot of different variations but I fail to use the Combobox with register. Any help would be appreciated. I want to use register to make this work and don't want to use the other Controller method of doing ways from react-hook-form.

import React from "react";

import { useState } from "react";
import { Combobox } from "@headlessui/react";
import { useForm } from "react-hook-form";

const people = [
  { id: 1, name: "Durward Reynolds" },
  { id: 2, name: "Kenton Towne" },
  { id: 3, name: "Therese Wunsch" },
  { id: 4, name: "Benedict Kessler" },
  { id: 5, name: "Katelyn Rohan" },
];

function Example() {
  const [query, setQuery] = useState("");

  const filteredPeople =
    query === ""
      ? people
      : people.filter((person) => {
          return person.name.toLowerCase().includes(query.toLowerCase());
        });

  const {
    register,
    handleSubmit,
    setValue,
    formState: { errors },
  } = useForm();

  const submit = (data) => {
    console.log(JSON.stringify(data));
  };

  return (
    <form onSubmit={handleSubmit(submit)}>
      <Combobox
        as="div"
        name="assignee"
        defaultValue={people[0]}
        {...register("assignee")}
      >
        <Combobox.Input
          onChange={(event) => setQuery(event.target.value)}
          displayValue={(person) => person.name}
        />
        <Combobox.Options>
          {filteredPeople.map((person) => (
            <Combobox.Option key={person.id} value={person}>
              {person.name}
            </Combobox.Option>
          ))}
        </Combobox.Options>
      </Combobox>
      <button>Submit</button>
    </form>
  );
}

export default function check() {
  return (
    <div>
      <Example />
    </div>
  );
}

Upvotes: 8

Views: 4709

Answers (4)

guadki
guadki

Reputation: 1

The way I solved this was through using useController hook. (I assumed OP is skeptical about using <Controller> component, not the useController hook + I was not able to do this using register either)

First you need control from useForm:

 const {
    control,
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<DataSchema>()

Then you need field from useController hook:

// name is the name of your component used by react hook form
const { field } = useController({ control, name });

And now the trick is that you pass value and onChange from field to <Combobox> component and onBlur, name and/or ref to <Combobox.Input>:

<Combobox value={field.value} onChange={field.onChange} by="id">
  <Combobox.Input
    onBlur={field.onBlur}
    name={field.name}
    ref={field.ref}
    // you can still handle onChange however you want, 
    // as this input value is used to filter combobox options
    onChange={(e) => setFilter(e.target.value)}
  />
</Combobox>

You can find out what each of field properties does on react-hook-form site here under Tips

If your combobox component is a child of a component where you use useForm, and you're using typescript, you can define types for control and name as props like this:

type Props<TFormValues extends FieldValues> = {
  control: Control<TFormValues>;
  name: Path<TFormValues>;
  // other props
};

export default function MyCombobox<TFormValues extends FieldValues>({
  control,
  name,
  // other props
}: Props<TFormValues>) {
  // component
};

And if you need to pass field down to the child component:

type Props<TFormValues extends FieldValues> = {
  field: ControllerRenderProps<TFormValues, Path<TFormValues>>;
  // other props
};

export default function MyInput<TFormValues extends FieldValues>({
  field,
  // other props
}: Props<TFormValues>) {
  //component
};


Upvotes: 0

vvlnv
vvlnv

Reputation: 431

Unfortunately it is not possible to use Combobox without Controller:

The problem is that Combobox calls onChange with bare value but RHF's onChange handler returned by register expects an event instead.

Upvotes: 0

Ivan Kleshnin
Ivan Kleshnin

Reputation: 1844

It's likely not a good idea to attach react-hook-form handlers to the Combobox directly.

  1. Input > onChange will give you an event with a string target.value, not a Location / User / ... model from the API. Will you do a copy request to "unpack" it in handleSubmit? And, if so, – how are you going to handle an API error there?!
  1. The input might be associated with an API error at the Combobox level. You'll have to be extra careful to distinguish "successful" and "failed" strings at the form component level.

  2. The string might be non-parsable at the form component level. Especially with "multiple" mode, where you can render an aggregate info, like "3 items are selected" in the input. And THAT will be your value if you expand register to the Combobox.Input.

  3. Finally, in some other (non HeadlessUI) Combobox implementations, the value will preserve raw user input.

For example:

  1. user inputs: "United"
  2. select suggests: "United States", "United Kingdom", ...
  3. user selects some option
  4. Combobox takes a new value but the Combobox.Input value still holds "United"

You probably want to stick to a portable and future-proof approach.


The conclusion is the same: let Combobox parse and translate values for you. Provide onChange to Combobox, not Combobox.Input. But that is possible only with controlled RHF API variant.

Upvotes: 0

const { register } = useFormContext();

<Combobox.Input
    {...register(id)}
    id={id}
    onChange={(event) => setQuery(event.target.value)}
/>

Upvotes: -2

Related Questions