Search code examples
javascriptreactjsantdreact-hook-formzod

How to use Ant Design <Select> with react-hook-forms and value transformation?


I have a zod schema that transforms a string (enum) value into an object. I want to use it with react-hook-forms and the zodResolver from @hookform/resolvers. I'd like to use my transformed value with an Ant Design <Select>.

Runnable code sandbox example: https://codesandbox.io/s/shy-water-cz9ryj?file=/src/App.js

The code currently looks like this:

import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { Select } from "antd";

const Foos = {
  a: { key: "a", label: "A" },
  b: { key: "b", label: "B" },
  c: { key: "c", label: "C" }
};

const Schema = z.object({
  foo: z.enum(["a", "b", "c"]).transform((key) => Foos[key])
});

const defaultValues = { foo: "a" };

export default function App() {
  const {
    handleSubmit,
    control,
    formState: { errors }
  } = useForm({
    defaultValues,
    resolver: zodResolver(Schema)
  });

  return (
    <div>
      <form onSubmit={handleSubmit((d) => console.log(d))}>
        <Controller
          control={control}
          name="foo"
          render={({ field }) => (
            <Select {...field} value={field.value.key}>
              {Object.values(Foos).map((foo) => (
                <Select.Option value={foo.key} key={foo.key}>
                  {foo.label}
                </Select.Option>
              ))}
            </Select>
          )}
        />
    </div>
  );
}

This works in principle, but the initial value is not correctly displayed. The <Select> appears emtpy until you select a value manually.

As shown in the codesandbox, this works as expected with native HTML <select> and <option>. That probably means that I am doing something wrong in regards to the Ant Design components.

Also, this works as expected with Ant Design and plain string values. It only broke when introducing the transformation to the schema.

Can someone give me a hint what I'm doing wrong here?


Solution

  • You need to use defaultValue and field.defaultValue instead of value and field.value.key:

    export default function App() {
      const {
        handleSubmit,
        control,
        formState: { errors }
      } = useForm({
        defaultValues,
        resolver: zodResolver(Schema)
      });
      return (
        <div className="App">
          <h2>Native HTML</h2>
          <br />
          <h2>Ant Design</h2>
          <form onSubmit={handleSubmit((d) => console.log(d))}>
            <Controller
              control={control}
              name="foo"
              render={({ field }) => (
                <Select {...field} defaultValue={field.defaultValue}>
                  {Object.values(Foos).map((foo) => (
                    <Select.Option value={foo.key} key={foo.key}>
                      {foo.label}
                    </Select.Option>
                  ))}
                </Select>
              )}
            />
            <br />
            Error: {JSON.stringify(errors)}
            <br />
            <input type="submit" />
          </form>
        </div>
      );
    }
    

    Here is the modified codeSandbox example: https://codesandbox.io/s/crazy-maria-mxhmno?file=/src/App.js