Skip to content

mhaqnegahdar/reusable-rhf-components

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Reusable React Hook Form Components

A comprehensive, type-safe form component library built with React Hook Form, Zod validation, and shadcn/ui components. Designed for Next.js applications with clean, reusable architecture.

πŸ“š Table of Contents

πŸš€ Features

  • Type-Safe: Full TypeScript support with Zod schema validation
  • Single Input Component: One RHFInput component handles all input types
  • Clean API: Minimal, readable form definitions
  • Server-Side Ready: Built for Next.js Server Actions and API routes
  • Flexible Architecture: Easy to extend and customize
  • Accessible: Built on shadcn/ui components with accessibility in mind
  • Automatic Validation: Real-time validation with proper error handling
  • Debug Mode: Built-in debug panel for development

πŸ“¦ Installation

Prerequisites

Make sure you have the following dependencies installed:

bun install react-hook-form @hookform/resolvers zod lucide-react

Copy Components to Your Project

  1. Copy the form components to your project structure:
your-project/
β”œβ”€ components/
β”‚  β”œβ”€ ui/
β”‚  β”‚  β”œβ”€ button.tsx
β”‚  β”‚  β”œβ”€ form.tsx
β”‚  β”‚  β”œβ”€ input.tsx
β”‚  β”‚  β”œβ”€ textarea.tsx
β”‚  β”‚  β”œβ”€ select.tsx
β”‚  β”‚  β”œβ”€ checkbox.tsx
β”‚  β”‚  └─ alert.tsx
β”‚  └─ form/
β”‚     β”œβ”€ RHFormContainer.tsx
β”‚     β”œβ”€ RHFInput.tsx
β”‚     └─ types.ts
  1. Ensure you have shadcn/ui components installed:
bunx --bun shadcn@latest init

bunx --bun shadcn@latest add button form input textarea select checkbox alert

🎯 Quick Start

1. Define Your Form Schema

import { z } from "zod";

const userSchema = z.object({
  username: z
    .string()
    .min(2, "Username must be at least 2 characters")
    .max(50, "Username must be less than 50 characters"),
  email: z
    .string()
    .email("Please enter a valid email address"),
  age: z
    .number()
    .min(18, "Age must be at least 18")
    .max(120, "Age must be less than 120"),
  role: z.enum(['admin', 'user', 'moderator'], {
    errorMap: () => ({ message: 'Please select a role' }),
  }),
  bio: z
    .string()
    .max(500, "Bio must be less than 500 characters")
    .optional(),
  terms: z.boolean().refine((val) => val === true, {
    message: "You must accept the terms and conditions",
  }),
});

type UserFormData = z.infer<typeof userSchema>;

2. Create Your Form Component

'use client'

import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { useState } from 'react'
import { RHFormContainer } from '@/components/form/RHFormContainer'
import { RHFInput } from '@/components/form/RHFInput'
import { userSchema, type UserFormData } from './schema'

export function UserProfileForm() {
  const [submitStatus, setSubmitStatus] = useState<{
    type: 'success' | 'error' | null
    message: string
  }>({ type: null, message: '' })

  const form = useForm<UserFormData>({
    resolver: zodResolver(userSchema),
    defaultValues: {
      username: '',
      email: '',
      age: 18,
      bio: '',
      terms: false,
    },
    mode: 'onChange',
  })

  const onSubmit = async (data: UserFormData) => {
    try {
      setSubmitStatus({ type: null, message: '' })
      
      // Your API call or Server Action here
      await new Promise((resolve) => setTimeout(resolve, 1000))
      
      setSubmitStatus({
        type: 'success',
        message: 'User profile created successfully!',
      })
      
      form.reset()
    } catch (error) {
      setSubmitStatus({
        type: 'error',
        message: 'Something went wrong. Please try again.',
      })
    }
  }

  const roleOptions = [
    { value: 'user', label: 'User' },
    { value: 'moderator', label: 'Moderator' },
    { value: 'admin', label: 'Admin' },
  ]

  return (
    <div className="max-w-2xl mx-auto p-6">
      <div className="space-y-2 mb-6">
        <h1 className="text-2xl font-bold">Create User Profile</h1>
        <p className="text-muted-foreground">
          Fill out the form below to create a new user profile.
        </p>
      </div>

      <RHFormContainer
        form={form}
        onSubmit={onSubmit}
        submitText="Create Profile"
        loadingText="Creating Profile..."
        status={submitStatus}
        showDebug={false}
      >
        <RHFInput
          name="username"
          control={form.control}
          type="text"
          label="Username"
          placeholder="johndoe"
          description="This will be your public display name."
        />

        <RHFInput
          name="email"
          control={form.control}
          type="email"
          label="Email"
          placeholder="john@example.com"
        />

        <RHFInput
          name="age"
          control={form.control}
          type="number"
          label="Age"
          placeholder="25"
          description="You must be at least 18 years old."
        />

        <RHFInput
          name="role"
          control={form.control}
          type="select"
          label="Role"
          placeholder="Select a role"
          description="Choose your role in the application."
          options={roleOptions}
        />

        <RHFInput
          name="bio"
          control={form.control}
          type="textarea"
          label="Bio"
          placeholder="Tell us a little bit about yourself"
          description="Optional. Maximum 500 characters."
          rows={4}
        />

        <RHFInput
          name="terms"
          control={form.control}
          type="checkbox"
          label="Accept terms and conditions"
          description="You agree to our Terms of Service and Privacy Policy."
        />
      </RHFormContainer>
    </div>
  )
}

3. Using with Next.js Server Actions

// app/actions.ts
"use server";

import { z } from "zod";
import { userSchema } from "./schema";

export async function createUser(data: z.infer<typeof userSchema>) {
  // Server-side validation
  const validatedData = userSchema.parse(data);
  
  // Your server-side logic here
  console.log("Creating user:", validatedData);
  
  return { success: true, message: "User created successfully!" };
}
// In your form component
import { createUser } from "./actions";

const onSubmit = async (data: UserFormData) => {
  try {
    const result = await createUser(data);
    if (result.success) {
      setSubmitStatus({
        type: 'success',
        message: result.message,
      });
    }
  } catch (error) {
    setSubmitStatus({
      type: 'error',
      message: 'Failed to create user.',
    });
  }
};

πŸ“š Available Components

Core Components

  • RHFormContainer - Main form wrapper with submission handling
  • RHFInput - Universal input component supporting all input types

Supported Input Types

  • text - Standard text input
  • email - Email input with validation
  • password - Password input
  • number - Number input with automatic conversion
  • textarea - Multi-line text input
  • select - Dropdown select with options
  • checkbox - Checkbox input

πŸ”§ Component API

RHFormContainer

interface RHFormContainerProps<TFieldValues extends FieldValues> {
  form: UseFormReturn<TFieldValues>
  onSubmit: SubmitHandler<TFieldValues>
  children: ReactNode
  submitText?: string          // Default: "Submit"
  loadingText?: string         // Default: "Submitting..."
  className?: string           // Default: "space-y-6"
  status?: FormStatus          // For success/error alerts
  showDebug?: boolean          // Default: false
}

RHFInput

// Base props for all input types
interface BaseInputProps<TFieldValues extends FieldValues> {
  name: FieldPath<TFieldValues>
  control: Control<TFieldValues>
  label?: string
  placeholder?: string
  description?: string
  disabled?: boolean
  className?: string
}

// Type-specific props are automatically inferred based on the 'type' prop

πŸ’‘ Examples

Basic Form Example

<RHFormContainer form={form} onSubmit={onSubmit}>
  <RHFInput
    name="name"
    control={form.control}
    type="text"
    label="Full Name"
    placeholder="Enter your name"
  />
  
  <RHFInput
    name="email"
    control={form.control}
    type="email"
    label="Email"
    placeholder="your@email.com"
  />
</RHFormContainer>

Advanced Validation

const advancedSchema = z.object({
  password: z
    .string()
    .min(8, "Password must be at least 8 characters")
    .regex(/[A-Z]/, "Must contain uppercase letter")
    .regex(/[0-9]/, "Must contain number"),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"],
});

Conditional Fields

const conditionalSchema = z.object({
  hasAddress: z.boolean(),
  address: z.string().optional(),
}).refine((data) => {
  if (data.hasAddress) {
    return data.address && data.address.length > 0;
  }
  return true;
}, {
  message: "Address is required when selected",
  path: ["address"],
});

🎨 Customization

Custom Styling

<RHFInput
  name="customField"
  control={form.control}
  type="text"
  label="Custom Field"
  className="border-2 border-blue-500"
/>

Custom Options for Select

const customOptions = [
  { value: 'option1', label: 'Option 1' },
  { value: 'option2', label: 'Option 2' },
  { value: 'option3', label: 'Option 3' },
];

<RHFInput
  name="selection"
  control={form.control}
  type="select"
  label="Choose Option"
  options={customOptions}
/>

πŸ“ TypeScript Support

The components provide full TypeScript support with:

  • Generic form data types from Zod schemas
  • Discriminated unions for input type safety
  • Automatic prop completion based on input type
  • Compile-time error checking for invalid prop combinations
// TypeScript will enforce correct props based on input type
<RHFInput
  name="age"
  control={form.control}
  type="number"        // TypeScript knows this is a number input
  // min, max props are available for number type
/>

<RHFInput
  name="role"
  control={form.control}
  type="select"        // TypeScript knows this is a select input
  options={roleOptions} // options prop is required for select type
/>

🚨 Error Handling

Form-Level Errors

const onSubmit = async (data: FormData) => {
  try {
    await submitData(data);
  } catch (error) {
    setSubmitStatus({
      type: 'error',
      message: 'Submission failed. Please try again.',
    });
  }
};

Field-Level Validation

Validation errors are automatically handled by the Zod schema and displayed under each field.

✨ Best Practices

  1. Always define Zod schemas for type safety and validation
  2. Use mode: 'onChange' for real-time validation feedback
  3. Provide helpful descriptions for complex fields
  4. Handle loading states in your submit handlers
  5. Use the debug mode during development
  6. Reset forms after successful submission

🀝 Contributing

To extend this library:

  1. Add new input types to the InputType union in types.ts
  2. Create corresponding interfaces for new input props
  3. Add implementation in the RHFInput component switch statement
  4. Update this README with examples and documentation

πŸ“„ License

This component library is provided as-is for use in your projects. Feel free to modify and distribute according to your needs.


Happy form building! πŸŽ‰

About

Form component library built with React Hook Form, Zod validation, and shadcn/ui components.

Topics

Resources

Stars

Watchers

Forks

Contributors