Leon

Different approaches to building a contact form in React

Topics
React
Published on 
16 Jan 2025

1. Controlled Input Form (Basic Approach)

Best for: Beginners learning form fundamentals

Step-by-Step Guide

  1. Create state variables for each input
  2. Manual validation using simple conditionals
  3. Handle submission state with a loading indicator
1import { useState } from 'react';
2
3interface FormState {
4  name: string;
5  email: string;
6  message: string;
7}
8
9export default function ControlledForm() {
10  const [form, setForm] = useState<FormState>({
11    name: '',
12    email: '',
13    message: ''
14  });
15  const [errors, setErrors] = useState<Partial<FormState>>({});
16  const [isSubmitting, setIsSubmitting] = useState(false);
17
18  // Simple validation function
19  const validate = () => {
20    const newErrors: Partial<FormState> = {};
21    if (!form.name) newErrors.name = 'Name is required';
22    if (!form.email) newErrors.email = 'Email is required';
23    if (!form.message) newErrors.message = 'Message is required';
24    setErrors(newErrors);
25    return Object.keys(newErrors).length === 0;
26  };
27
28  const handleSubmit = (e: React.FormEvent) => {
29    e.preventDefault();
30    if (!validate()) return;
31
32    setIsSubmitting(true);
33    setTimeout(() => {
34      console.log('Submitted:', form);
35      setIsSubmitting(false);
36    }, 1000);
37  };
38
39  return (
40    <form onSubmit={handleSubmit}>
41      <div>
42        <label>Name:</label>
43        <input
44          value={form.name}
45          onChange={(e) => setForm({...form, name: e.target.value})}
46          disabled={isSubmitting}
47        />
48        {errors.name && <span style={{color: 'red'}}>{errors.name}</span>}
49      </div>
50
51      <div>
52        <label>Email:</label>
53        <input
54          type="email"
55          value={form.email}
56          onChange={(e) => setForm({...form, email: e.target.value})}
57          disabled={isSubmitting}
58        />
59        {errors.email && <span style={{color: 'red'}}>{errors.email}</span>}
60      </div>
61
62      <div>
63        <label>Message:</label>
64        <textarea
65          value={form.message}
66          onChange={(e) => setForm({...form, message: e.target.value})}
67          disabled={isSubmitting}
68        />
69        {errors.message && <span style={{color: 'red'}}>{errors.message}</span>}
70      </div>
71
72      <button type="submit" disabled={isSubmitting}>
73        {isSubmitting ? 'Sending...' : 'Send Message'}
74      </button>
75    </form>
76  );
77}
78

Why Use This?

  • Complete control over form behavior
  • Good for learning fundamentals
  • No external dependencies

2. React Hook Form (Intermediate Approach)

Best for: Most real-world applications

Step-by-Step Guide

  1. Install dependencies: npm install react-hook-form
  2. Use useForm hook for state management
  3. Add inline validation rules
1import { useForm } from 'react-hook-form';
2
3type FormValues = {
4  name: string;
5  email: string;
6  message: string;
7};
8
9export default function RHFBasicForm() {
10  const {
11    register,
12    handleSubmit,
13    formState: { errors, isSubmitting }
14  } = useForm<FormValues>();
15
16  const onSubmit = async (data: FormValues) => {
17    await new Promise(resolve => setTimeout(resolve, 1000));
18    console.log('Submitted:', data);
19  };
20
21  return (
22    <form onSubmit={handleSubmit(onSubmit)}>
23      <div>
24        <label>Name:</label>
25        <input 
26          {...register('name', { required: 'Name is required' })} 
27          disabled={isSubmitting}
28        />
29        {errors.name && <span style={{color: 'red'}}>{errors.name.message}</span>}
30      </div>
31
32      <div>
33        <label>Email:</label>
34        <input
35          type="email"
36          {...register('email', { 
37            required: 'Email is required',
38            pattern: {
39              value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
40              message: 'Invalid email address'
41            }
42          })}
43          disabled={isSubmitting}
44        />
45        {errors.email && <span style={{color: 'red'}}>{errors.email.message}</span>}
46      </div>
47
48      <div>
49        <label>Message:</label>
50        <textarea
51          {...register('message', { 
52            required: 'Message is required',
53            minLength: {
54              value: 10,
55              message: 'Message must be at least 10 characters'
56            }
57          })}
58          disabled={isSubmitting}
59        />
60        {errors.message && <span style={{color: 'red'}}>{errors.message.message}</span>}
61      </div>
62
63      <button type="submit" disabled={isSubmitting}>
64        {isSubmitting ? 'Sending...' : 'Send Message'}
65      </button>
66    </form>
67  );
68}
69

Why Use This?

  • Better performance (fewer re-renders)
  • Less boilerplate code
  • Built-in validation system

3. React Hook Form + Zod (Advanced Validation)

Best for: Complex validation needs

Step-by-Step Guide

  1. Install dependencies: npm install zod @hookform/resolvers
  2. Create Zod validation schema
  3. Integrate with React Hook Form
1import { useForm } from 'react-hook-form';
2import { zodResolver } from '@hookform/resolvers/zod';
3import { z } from 'zod';
4
5const contactSchema = z.object({
6  name: z.string().min(1, 'Name is required'),
7  email: z.string().email('Invalid email address'),
8  message: z.string().min(10, 'Message must be at least 10 characters')
9});
10
11type FormValues = z.infer<typeof contactSchema>;
12
13export default function RHFZodForm() {
14  const {
15    register,
16    handleSubmit,
17    formState: { errors, isSubmitting }
18  } = useForm<FormValues>({
19    resolver: zodResolver(contactSchema)
20  });
21
22  const onSubmit = async (data: FormValues) => {
23    await new Promise(resolve => setTimeout(resolve, 1000));
24    console.log('Submitted:', data);
25  };
26
27  return (
28    <form onSubmit={handleSubmit(onSubmit)}>
29      <div>
30        <label>Name:</label>
31        <input {...register('name')} disabled={isSubmitting} />
32        {errors.name && <span style={{color: 'red'}}>{errors.name.message}</span>}
33      </div>
34
35      <div>
36        <label>Email:</label>
37        <input type="email" {...register('email')} disabled={isSubmitting} />
38        {errors.email && <span style={{color: 'red'}}>{errors.email.message}</span>}
39      </div>
40
41      <div>
42        <label>Message:</label>
43        <textarea {...register('message')} disabled={isSubmitting} />
44        {errors.message && <span style={{color: 'red'}}>{errors.message.message}</span>}
45      </div>
46
47      <button type="submit" disabled={isSubmitting}>
48        {isSubmitting ? 'Sending...' : 'Send Message'}
49      </button>
50    </form>
51  );
52}
53

Why Use This?

  • Centralized validation rules
  • Type-safe schema definitions
  • Reusable validation logic

4. Shadcn UI + React Hook Form + Zod (Production-Ready)

Best for: Professional applications needing polished UI

Step-by-Step Guide

  1. Set up Shadcn: npx shadcn-ui@latest init
  2. Install dependencies: npm install @hookform/resolvers zod
  3. Create form components
1import { useForm } from 'react-hook-form';
2import { zodResolver } from '@hookform/resolvers/zod';
3import { z } from 'zod';
4import {
5  Form,
6  FormControl,
7  FormField,
8  FormItem,
9  FormLabel,
10  FormMessage,
11} from '@/components/ui/form';
12import { Input } from '@/components/ui/input';
13import { Textarea } from '@/components/ui/textarea';
14import { Button } from '@/components/ui/button';
15
16const contactSchema = z.object({
17  name: z.string().min(1, 'Name is required'),
18  email: z.string().email('Invalid email address'),
19  message: z.string().min(10, 'Message must be at least 10 characters')
20});
21
22type FormValues = z.infer<typeof contactSchema>;
23
24export default function ShadcnForm() {
25  const form = useForm<FormValues>({
26    resolver: zodResolver(contactSchema),
27    defaultValues: {
28      name: '',
29      email: '',
30      message: ''
31    }
32  });
33
34  const onSubmit = async (values: FormValues) => {
35    await new Promise(resolve => setTimeout(resolve, 1000));
36    console.log('Submitted:', values);
37  };
38
39  return (
40    <Form {...form}>
41      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
42        <FormField
43          control={form.control}
44          name="name"
45          render={({ field }) => (
46            <FormItem>
47              <FormLabel>Name</FormLabel>
48              <FormControl>
49                <Input 
50                  placeholder="Your name" 
51                  {...field} 
52                  disabled={form.formState.isSubmitting}
53                />
54              </FormControl>
55              <FormMessage />
56            </FormItem>
57          )}
58        />
59
60        <FormField
61          control={form.control}
62          name="email"
63          render={({ field }) => (
64            <FormItem>
65              <FormLabel>Email</FormLabel>
66              <FormControl>
67                <Input
68                  type="email"
69                  placeholder="your@email.com"
70                  {...field}
71                  disabled={form.formState.isSubmitting}
72                />
73              </FormControl>
74              <FormMessage />
75            </FormItem>
76          )}
77        />
78
79        <FormField
80          control={form.control}
81          name="message"
82          render={({ field }) => (
83            <FormItem>
84              <FormLabel>Message</FormLabel>
85              <FormControl>
86                <Textarea
87                  placeholder="Your message..."
88                  {...field}
89                  disabled={form.formState.isSubmitting}
90                />
91              </FormControl>
92              <FormMessage />
93            </FormItem>
94          )}
95        />
96
97        <Button 
98          type="submit" 
99          disabled={form.formState.isSubmitting}
100        >
101          {form.formState.isSubmitting ? 'Sending...' : 'Send Message'}
102        </Button>
103      </form>
104    </Form>
105  );
106}
107

Why Use This?

  • Beautiful, accessible UI components
  • Professional styling out of the box
  • Combines best validation practices