Using React Hook Form with auto-generated API types

Leo Sjöberg • July 12, 2024

Following my post on generating a type-safe API client from your Laravel API, I wanted to share how I then use those auto-generated request types not just for making requests, but also for ensuring my UI forms are valid.

For working with forms in React, I like to use React Hook Form (RHF), a library that makes it easy to represent the data structure of your forms, and handle form submissions on the frontend in a consistent way.

When you use RHF with TypeScript, you want to specify the type of your fields:

1export function App() {
2 const { register, handleSubmit } = useForm<{
3 name: string;
4 email: string;
5 }>({
6 name: '',
7 email: '',
8 });
9 const [data, setData] = useState("");
10 
11 return (
12 <form onSubmit={handleSubmit((data) => setData(JSON.stringify(data)))}>
13 <input {...register("name")} placeholder="Name" />
14 <input {...register("email")} placeholder="E-mail address" />
15 <button type="submit">Save</button>
16 </form>
17 );
18}

Most of the time, the forms you build will map 1:1 against your API, particularly ones powering CRUD operations. In those cases, having to manually declare the type of your form data can be a bit tedious.

With auto-generated types, you can use those same interface types that are used for requests in your forms!

A type generated from your OpenAPI spec might look something like this:

1/**
2 *
3 * @export
4 * @interface CreateApiTokenData
5 */
6export interface CreateApiTokenData {
7 /**
8 *
9 * @type {string}
10 * @memberof CreateApiTokenData
11 */
12 name: string;
13}

You can then use this in your form:

1const CreateTokenModal = ({ onClose }: { onClose: () => void }) => {
2 const { client } = useClient()
3 const { register, control } = useForm<CreateApiTokenData>();
4 const [newToken, setNewToken] = useState<string | null>(null)
5 
6 const createToken = async (data: CreateApiTokenData) => {
7 try {
8 const response = await client.apiTokensPost({
9 createApiTokenData: data
10 })
11 setNewToken(response.token)
12 } catch (error) {
13 console.error(error)
14 }
15 }
16 return (
17 <Form control={control} onSubmit={({ data }) => createToken(data)}>
18 <h3 className="text-lg font-semibold">Create new token</h3>
19 <label>
20 Name
21 <TextField.Root
22 {...register('name', { required: true })}
23 />
24 </label>
25 <Button type="submit">Create</Button>
26 </Form>
27 )
28}

Here, we have a form to create an API token, which uses the CreateApiTokenData type in the useForm call, meaning we can then pass the form data straight into our API client, without having to map two nearly identical types.

While it’s nothing groundbreaking, this little convenience makes for a nice bit of developer experience.