Search code examples
typescripttypescript-typingstypescript-genericsreact-hook-formnext.js14

How to Create a Generic Form Component in Next.js 14 with TypeScript and react-hook-form?


Question:

I'm working on a Next.js 13 application with TypeScript, and I'm trying to create a generic form component using react-hook-form. The goal is to have a single form component that can handle forms of different types, such as reports, clients, and users. Each type of form has its own set of fields, but the form operations (save, reset, update, delete) remain the same across all types.

Here's a simplified version of what I'm trying to achieve:

import { FC } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';

// Define different types of form data
type ReportFormData = {
  reportField: string;
};

type ClientFormData = {
  clientField: string;
};

type UserFormData = {
  userField: string;
};

// Define props for the generic form component
type FormProps<T> = {
  initialData: T;
  onSubmit: SubmitHandler<T>;
};

// Create the generic form component
const GenericForm: FC<FormProps> = ({ initialData, onSubmit }) => {
  const { register, handleSubmit, reset } = useForm({
    defaultValues: initialData,
  });

  const onSubmitHandler: SubmitHandler<T> = (data) => {
    onSubmit(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmitHandler)}>
      {/* How can I conditionally render fields based on the type of data? */}
      {/* How to properly handle type checking in this scenario? */}
      
      {/* Placeholder for save operation */}
      <button type="submit">Save</button>
      
      {/* Placeholder for reset operation */}
      <button type="button" onClick={() => reset(initialData)}>
        Reset
      </button>
      
      {/* Placeholder for update operation */}
      {/* Placeholder for delete operation */}
    </form>
  );
};

Problem:

I'm facing an issue with TypeScript type checking within the GenericForm component. Since react-hook-form requires precise typing for form fields, I can't simply use a union type for my form data. However, I need the GenericForm component to accept different types of initial data based on the form being rendered.

I'm unsure how to properly handle type checking and conditional rendering within the GenericForm component to accommodate different types of form data while maintaining type safety.

Expected Output:

I expect to have a single GenericForm component that can handle forms of different types, rendering the appropriate fields based on the type of initial data provided. Additionally, I need to ensure that TypeScript type checking is properly handled within the component.


Solution

  • You're on the right path. I would extend on what you've done and make GenericForm take a generic argument.

    Define the component like so.

    const GenericForm = <T,>({ initialData, onSubmit }:FormProps<T>) => {

    Then use it like this <GenericForm<ReportFormData> initialData={{reportField:""}} onSubmit={()=>{}}/>

    That should make GenericForm have type safe initialData and onSubmit

    Here is an example from TS playground https://tsplay.dev/WG05vw

    import React from 'react';
    import { useForm, SubmitHandler, FieldValues, DefaultValues } from 'react-hook-form';
    
    // Define different types of form data
    type ReportFormData = {
      reportField: string | null;
    };
    
    type ClientFormData = {
      clientField: string | null;
    };
    
    type UserFormData = {
      userField: string | null;
    };
    
    // Define props for the generic form component 
    type FormProps<T extends FieldValues> = {
      initialData: DefaultValues<T>;
      onSubmit: SubmitHandler<T>;
    };
    
    // Create the generic form component
    const GenericForm =<T extends FieldValues,> ({ initialData, onSubmit }:FormProps<T>) => {
      const { register, handleSubmit, reset } = useForm<T>({
        defaultValues: initialData,
      });
    
      const onSubmitHandler: SubmitHandler<T> = (data) => {
        onSubmit(data);
      };
    
      return (
        <form onSubmit={handleSubmit(onSubmitHandler)}>
          {/* How can I conditionally render fields based on the type of data? */}
          {/* How to properly handle type checking in this scenario? */}
          
          {/* Placeholder for save operation */}
          <button type="submit">Save</button>
          
          {/* Placeholder for reset operation */}
          <button type="button" onClick={() => reset(initialData)}>
            Reset
          </button>
          
          {/* Placeholder for update operation */}
          {/* Placeholder for delete operation */}
        </form>
      );
    };
    
    
    const ReportFormComponent = () =>{
    
      return <GenericForm<ReportFormData> initialData={{reportField:null}} onSubmit={(data)=>console.log(data)}/>
    }