Search code examples
reactjsformikheadless-ui

Updating Formik State whilst using Headless UI "Listbox" (which manages its own state, internally)


I'm struggling to get a Headless UI Listbox implementation working successfully with Formik. After reading all the docs and scouring StackOverflow, I'm yet to find an answer.

I can get the Listbox to function perfectly well on its own, and Formik is working with other, less complex (but still custom) components. That said, I can't get them working in unison. Whenever I change the selection in the Select component, I can successfully update the Headless UI state (in as much as it updates to the correct value) and the Formik state, but for some reason the active and selected properties (within Headless UI) aren't working correctly (always false).

I assume what I need to do is to make use of the onChange handler so that it updates both the Headless UI state (to keep the active and selected properties updated) and the Formik value, but this doesn't seem to work as expected.

Does anyone have any suggestions? Please see a minimal representation of the code below:

export function TestForm() {
  return (
    <Formik
      initialValues={{ testing: {} }}
      onSubmit={ alert("Submitted"); }
    >
    <Form>
      <Select
        name="testing"
        items={[
          { id: 1, name: "Test 1" },
          { id: 2, name: "Test 2" },
          { id: 3, name: "Test 3" },
        ]}
      />
    </Form>
  )
}

export function Select(props) {
  // Get field properties
  const [field, meta, helpers] = useField(props);

  // Set initial value for `Select`
  const [selectedItem, setSelectedItem] = useState(null);

  // On change, update `Headless UI` and `Formik` values
  const handleChange = (newValue) => {
    setSelectedItem(newValue); // Update the Headless UI state
    helpers.setValue(newValue); // This seems to "break" the Headless UI state
  };

  // Return `Select` structure
  return (
    <Listbox
      value={selectedItem}
      name={props.name}
      onChange={handleChange}
      // onBlur={field.onBlur} ...Commented out for now as trying to figure out issue with `onChange`
    >
      <Listbox.Label>Test Label:</Listbox.Label>
      <Listbox.Button>
        {selectedItem ? selectedItem.name : "-- Select --"}
      </Listbox.Button>
      <Listbox.Options>
        {props.items.map((item) => {
          return (
            <Listbox.Option
              className={({ active }) => (active ? "active" : "")}
              key={item.id}
              value={item}
            >
              {({ selected }) => (
                <>
                  {item.name}
                  {selected ? <CheckIcon /> : null}
                </>
              )}
            </Listbox.Option>
          );
        })}
      </Listbox.Options>
    </Listbox>
  );
}

Solution

  • I managed to make it work like this:

    import { useField } from "formik";
    
    interface SelectProps {
      name: string;
      label: string;
      options: string[];
    }
    
    export const Select: React.FC<SelectProps> = ({
      name,
      label,
      options,
    }) => {
      const [field] = useField({ name });
    
      return (
        <Listbox
          value={field.value}
          onChange={(value: string) => {
            field.onChange({ target: { value, name } });
          }}
        >
          ...
        </Listbox>
      );
    };