sansalgo
sansalgo

Reputation: 103

How to make name type-safe in a reusable input component using useFormContext from React Hook Form?

I'm using React Hook Form with useFormContext() in my Next.js project, and I’m trying to build a reusable input component. The problem I’m facing is that the name prop, which is used for registering the input field, is just a string.

I want TypeScript to suggest only valid field names based on my form schema instead of allowing any random string.

import { ExclamationCircleIcon } from "@heroicons/react/20/solid";
import React from "react";
import { useFormContext } from "react-hook-form";

interface InputProps {
  label: string;
  name: string; // ❌ I want this to be type-safe and suggest only valid form fields
  type?: string;
  placeholder?: string;
}

export function InputWithValidationError({ label, name, type = "text", placeholder }: InputProps) {
  const { register, formState: { errors } } = useFormContext(); // Accessing form context

  const error = errors[name]?.message as string | undefined;

  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <input
        id={name}
        type={type}
        placeholder={placeholder}
        {...register(name)} // ❌ This allows any string, but I want it to be type-safe
      />
      {error && <p>{error}</p>}
    </div>
  );
}

Schema is defined like this:

const methods = useForm<{ email: string; password: string }>();

I use my component like this:

<InputWithValidationError label="Email" name="email" />

✅ TypeScript should suggest only "email" and "password"

❌ It should NOT allow random strings like "username" or "test"

My Question: How can I make the name prop type-safe so that TypeScript only allows valid form field names based on the form schema from useFormContext()?

Would really appreciate any help! 😊

Upvotes: 1

Views: 76

Answers (2)

Caleth
Caleth

Reputation: 63142

You need to make the input component generic over the form fields, and then constrain name to be any path to a field.

import { ExclamationCircleIcon } from "@heroicons/react/20/solid";
import React from "react";
import { useFormContext, FieldPath, FieldValues } from "react-hook-form";

interface InputProps<TFieldValues extends FieldValues> {
  label: string;
  name: FieldPath<TFieldValues>;
  type?: string;
  placeholder?: string;
}

export function InputWithValidationError<TFieldValues extends FieldValues>({ label, name, type = "text", placeholder }: InputProps<TFieldValues>) {
  const { register, formState: { errors } } = useFormContext<TFieldValues>(); // Accessing form context

  const error = errors[name]?.message as string | undefined;

  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <input
        id={name}
        type={type}
        placeholder={placeholder}
        {...register(name)} // This is constrained to valid names
      />
      {error && <p>{error}</p>}
    </div>
  );
}

You would then use it like:

type FormValues = { email: string; password: string };

const methods = useForm<FormValues>();

<InputWithValidationError<FormValues> label="Email" name="email" />
<InputWithValidationError<FormValues> label="Email" name="address" /> // ❌ Type '"address"' is not assignable to type '"email" | "password"'

Upvotes: 1

Pinal Tilva
Pinal Tilva

Reputation: 848

You can follow the below approach:

// types.tsx

export interface FormFields {
  email: string;
  name: string;
}

export interface InputProps {
  label: string;
  name: keyof FormFields;  // The name must be one of the keys of FormFields (i.e., 'email' or 'name')
  type?: string;
  placeholder?: string;
}

// your-component.tsx

import { InputProps } from "./types"

export function InputWithValidationError({ label, name, type = "text", placeholder }: InputProps) {
  const { register, formState: { errors } } = useFormContext(); // Accessing form context

  const error = errors[name]?.message as string | undefined;

  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <input
        id={name}
        type={type}
        placeholder={placeholder}
        {...register(name)} // ❌ This allows any string, but I want it to be type-safe
      />
      {error && <p>{error}</p>}
    </div>
  );
}
// your-form-component.tsx

import { FormFields } from "./types"

const methods = useForm<FormFields>();

Upvotes: 0

Related Questions