We use cookies to enhance your experience on the site
CodeWorlds

Creating Forms in React and Next.js

Forms are a fundamental element of interactive web applications. They allow users to input data, which can then be processed by the application. In the React and Next.js environment, creating forms can be achieved in various ways - from simple native solutions to advanced libraries. In this module, we will look at different approaches to form implementation.

Basic Forms in React

In its most basic form, a form in React is similar to a traditional HTML form, but with an additional layer of state management.

Controlled Form Components

The most commonly used approach in React is "controlled components", where the value of each form element is maintained in React state:

1import { useState, FormEvent } from 'react';
2
3export default function SimpleForm() {
4  const [name, setName] = useState('');
5  const [email, setEmail] = useState('');
6  const [message, setMessage] = useState('');
7  
8  function handleSubmit(event: FormEvent<HTMLFormElement>) {
9    event.preventDefault();
10    console.log('Form data:', { name, email, message });
11    
12    // Here you can send data to the API
13    // fetch('/api/contact', {
14    //   method: 'POST',
15    //   body: JSON.stringify({ name, email, message }),
16    //   headers: { 'Content-Type': 'application/json' }
17    // })
18    
19    // Optionally reset the form
20    setName('');
21    setEmail('');
22    setMessage('');
23  }
24  
25  return (
26    <form onSubmit={handleSubmit} className="space-y-4">
27      <div>
28        <label htmlFor="name" className="block text-sm font-medium text-gray-700">
29          Full name
30        </label>
31        <input
32          type="text"
33          id="name"
34          value={name}
35          onChange={(e) => setName(e.target.value)}
36          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
37          required
38        />
39      </div>
40      
41      <div>
42        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
43          Email
44        </label>
45        <input
46          type="email"
47          id="email"
48          value={email}
49          onChange={(e) => setEmail(e.target.value)}
50          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
51          required
52        />
53      </div>
54      
55      <div>
56        <label htmlFor="message" className="block text-sm font-medium text-gray-700">
57          Message
58        </label>
59        <textarea
60          id="message"
61          value={message}
62          onChange={(e) => setMessage(e.target.value)}
63          rows={4}
64          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
65          required
66        />
67      </div>
68      
69      <button
70        type="submit"
71        className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
72      >
73        Send
74      </button>
75    </form>
76  );
77}

In the example above:

  1. Each form field is connected to a corresponding state variable
  2. The
    onChange
    event updates state for each field
  3. The
    onSubmit
    event for the whole form handles data submission
  4. Using
    event.preventDefault()
    prevents the default page refresh

Managing Multiple Form Fields

When a form has many fields, maintaining a separate state for each one can be cumbersome. A more scalable approach is to use a single state object:

1import { useState, ChangeEvent, FormEvent } from 'react';
2
3interface FormData {
4  name: string;
5  email: string;
6  subject: string;
7  message: string;
8  agreeToTerms: boolean;
9}
10
11export default function ContactForm() {
12  const [formData, setFormData] = useState<FormData>({
13    name: '',
14    email: '',
15    subject: '',
16    message: '',
17    agreeToTerms: false
18  });
19  
20  const [errors, setErrors] = useState<Partial<FormData>>({});
21  
22  function handleChange(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) {
23    const { name, value, type } = e.target;
24    const checked = (e.target as HTMLInputElement).checked;
25    
26    setFormData(prev => ({
27      ...prev,
28      [name]: type === 'checkbox' ? checked : value
29    }));
30    
31    // Reset error during editing
32    if (errors[name as keyof FormData]) {
33      setErrors(prev => ({ ...prev, [name]: undefined }));
34    }
35  }
36  
37  function validateForm(): boolean {
38    const newErrors: Partial<FormData> = {};
39    let isValid = true;
40    
41    if (!formData.name.trim()) {
42      newErrors.name = 'Name is required';
43      isValid = false;
44    }
45    
46    if (!formData.email.trim()) {
47      newErrors.email = 'Email is required';
48      isValid = false;
49    } else if (!/^S+@S+.S+$/.test(formData.email)) {
50      newErrors.email = 'Email is invalid';
51      isValid = false;
52    }
53    
54    if (!formData.message.trim()) {
55      newErrors.message = 'Message is required';
56      isValid = false;
57    }
58    
59    if (!formData.agreeToTerms) {
60      newErrors.agreeToTerms = 'You must agree to the terms';
61      isValid = false;
62    }
63    
64    setErrors(newErrors);
65    return isValid;
66  }
67  
68  function handleSubmit(e: FormEvent<HTMLFormElement>) {
69    e.preventDefault();
70    
71    if (validateForm()) {
72      console.log('Submitting form:', formData);
73      // Send to API...
74      
75      // Reset form after successful submission
76      setFormData({
77        name: '',
78        email: '',
79        subject: '',
80        message: '',
81        agreeToTerms: false
82      });
83    }
84  }
85  
86  return (
87    <form onSubmit={handleSubmit} className="space-y-4">
88      <div>
89        <label htmlFor="name" className="block text-sm font-medium">
90          Full name *
91        </label>
92        <input
93          type="text"
94          id="name"
95          name="name"
96          value={formData.name}
97          onChange={handleChange}
98          className={`mt-1 block w-full rounded-md ${errors.name ? 'border-red-500' : 'border-gray-300'}`}
99        />
100        {errors.name && <p className="text-red-500 text-xs mt-1">{errors.name}</p>}
101      </div>
102      
103      <div>
104        <label htmlFor="email" className="block text-sm font-medium">
105          Email *
106        </label>
107        <input
108          type="email"
109          id="email"
110          name="email"
111          value={formData.email}
112          onChange={handleChange}
113          className={`mt-1 block w-full rounded-md ${errors.email ? 'border-red-500' : 'border-gray-300'}`}
114        />
115        {errors.email && <p className="text-red-500 text-xs mt-1">{errors.email}</p>}
116      </div>
117      
118      <div>
119        <label htmlFor="subject" className="block text-sm font-medium">
120          Subject
121        </label>
122        <select
123          id="subject"
124          name="subject"
125          value={formData.subject}
126          onChange={handleChange}
127          className="mt-1 block w-full rounded-md border-gray-300"
128        >
129          <option value="">Select subject</option>
130          <option value="general">General inquiry</option>
131          <option value="support">Technical support</option>
132          <option value="feedback">Feedback</option>
133        </select>
134      </div>
135      
136      <div>
137        <label htmlFor="message" className="block text-sm font-medium">
138          Message *
139        </label>
140        <textarea
141          id="message"
142          name="message"
143          value={formData.message}
144          onChange={handleChange}
145          rows={4}
146          className={`mt-1 block w-full rounded-md ${errors.message ? 'border-red-500' : 'border-gray-300'}`}
147        />
148        {errors.message && <p className="text-red-500 text-xs mt-1">{errors.message}</p>}
149      </div>
150      
151      <div className="flex items-center">
152        <input
153          type="checkbox"
154          id="agreeToTerms"
155          name="agreeToTerms"
156          checked={formData.agreeToTerms}
157          onChange={handleChange}
158          className={`h-4 w-4 ${errors.agreeToTerms ? 'border-red-500' : 'border-gray-300'}`}
159        />
160        <label htmlFor="agreeToTerms" className="ml-2 block text-sm">
161          I agree to the terms and privacy policy *
162        </label>
163        {errors.agreeToTerms && (
164          <p className="text-red-500 text-xs ml-2">{errors.agreeToTerms}</p>
165        )}
166      </div>
167      
168      <button
169        type="submit"
170        className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
171      >
172        Send message
173      </button>
174    </form>
175  );
176}

This approach offers the following advantages:

  1. A single handleChange function handles all fields
  2. Dynamic state updates thanks to the
    [name]: value
    syntax
  3. TypeScript typing ensures type safety
  4. Validation with error messages improves user experience

Forms in Next.js with Server Actions

Next.js 13+ introduced Server Actions, which enable direct form processing on the server without the need to create separate API endpoints.

Basic Form with Server Action

1// app/signup/page.tsx
2'use client';
3
4import { useState } from 'react';
5import { useRouter } from 'next/navigation';
6import { registerUser } from './actions';
7
8export default function SignupPage() {
9  const router = useRouter();
10  const [error, setError] = useState<string>('');
11  const [loading, setLoading] = useState(false);
12  
13  async function handleSubmit(formData: FormData) {
14    setLoading(true);
15    setError('');
16    
17    try {
18      // Call Server Action
19      const result = await registerUser(formData);
20      
21      if (result.success) {
22        // Redirect after successful registration
23        router.push('/signup/confirm');
24      } else {
25        // Display error
26        setError(result.error || 'Something went wrong');
27      }
28    } catch (e) {
29      setError('An unexpected error occurred.');
30    } finally {
31      setLoading(false);
32    }
33  }
34  
35  return (
36    <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
37      <h1 className="text-2xl font-bold mb-6">Registration</h1>
38      
39      {error && (
40        <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
41          {error}
42        </div>
43      )}
44      
45      <form action={handleSubmit}>
46        <div className="mb-4">
47          <label htmlFor="name" className="block text-sm font-medium mb-1">
48            Full name
49          </label>
50          <input
51            type="text"
52            id="name"
53            name="name"
54            required
55            className="w-full px-3 py-2 border border-gray-300 rounded-md"
56          />
57        </div>
58        
59        <div className="mb-4">
60          <label htmlFor="email" className="block text-sm font-medium mb-1">
61            Email
62          </label>
63          <input
64            type="email"
65            id="email"
66            name="email"
67            required
68            className="w-full px-3 py-2 border border-gray-300 rounded-md"
69          />
70        </div>
71        
72        <div className="mb-4">
73          <label htmlFor="password" className="block text-sm font-medium mb-1">
74            Password
75          </label>
76          <input
77            type="password"
78            id="password"
79            name="password"
80            required
81            minLength={8}
82            className="w-full px-3 py-2 border border-gray-300 rounded-md"
83          />
84        </div>
85        
86        <button
87          type="submit"
88          disabled={loading}
89          className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
90        >
91          {loading ? 'Registering...' : 'Sign up'}
92        </button>
93      </form>
94    </div>
95  );
96}

And here is the Server Action file:

1// app/signup/actions.ts
2'use server';
3
4import { z } from 'zod';
5
6// Form validation schema
7const UserSchema = z.object({
8  name: z.string().min(2, { message: 'Name is too short' }),
9  email: z.string().email({ message: 'Invalid email address' }),
10  password: z.string().min(8, { message: 'Password must be at least 8 characters' })
11});
12
13type SignupResult = 
14  | { success: true; userId: string }
15  | { success: false; error: string };
16
17export async function registerUser(formData: FormData): Promise<SignupResult> {
18  // 1. Validate form data
19  const validatedFields = UserSchema.safeParse({
20    name: formData.get('name'),
21    email: formData.get('email'),
22    password: formData.get('password')
23  });
24  
25  if (!validatedFields.success) {
26    return {
27      success: false,
28      error: validatedFields.error.errors[0]?.message || 'Invalid form data'
29    };
30  }
31  
32  const { name, email, password } = validatedFields.data;
33  
34  try {
35    // 2. Check if user already exists
36    const existingUser = false; // Here should be the actual database check logic
37    
38    if (existingUser) {
39      return {
40        success: false,
41        error: 'A user with this email address already exists'
42      };
43    }
44    
45    // 3. Create new user (in a real application - save to database)
46    // const newUser = await db.user.create({ data: { name, email, password: hashedPassword } });
47    
48    // 4. Additional operations - sending welcome email, logging in, etc.
49    // await sendWelcomeEmail(email, name);
50    
51    return {
52      success: true,
53      userId: '123' // In a real application, the created user ID
54    };
55  } catch (error) {
56    console.error('Registration error:', error);
57    return {
58      success: false,
59      error: 'An error occurred during registration. Please try again later.'
60    };
61  }
62}

Advantages of Server Actions:

  1. Simplified architecture - no need to create separate API Routes
  2. Progressive enhancement - forms work even without JavaScript
  3. Secure data processing - verification and processing logic runs on the server
  4. End-to-end typing - TypeScript can provide typing from form to database

Forms with Loading States and Validation

We can combine Server Actions with React Hooks to provide an even better user experience:

1// app/contact/page.tsx
2'use client';
3
4import { useFormState } from 'react-dom';
5import { useFormStatus } from 'react-dom';
6import { submitContactForm } from './actions';
7
8// Button component with loading state handling
9function SubmitButton() {
10  const { pending } = useFormStatus();
11  
12  return (
13    <button
14      type="submit"
15      disabled={pending}
16      className="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
17    >
18      {pending ? 'Sending...' : 'Send message'}
19    </button>
20  );
21}
22
23export default function ContactPage() {
24  // Initial form state
25  const initialState = {
26    message: '',
27    errors: {},
28    success: false
29  };
30  
31  // Using the useFormState hook to manage form state
32  const [state, formAction] = useFormState(submitContactForm, initialState);
33  
34  return (
35    <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
36      <h1 className="text-2xl font-bold mb-6">Contact form</h1>
37      
38      {state.success ? (
39        <div className="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
40          Thank you for your message! We will respond as soon as possible.
41        </div>
42      ) : (
43        <form action={formAction} className="space-y-4">
44          {state.message && (
45            <div className="p-3 bg-red-100 border border-red-400 text-red-700 rounded">
46              {state.message}
47            </div>
48          )}
49          
50          <div>
51            <label htmlFor="name" className="block text-sm font-medium mb-1">
52              Full name *
53            </label>
54            <input
55              type="text"
56              id="name"
57              name="name"
58              className={`w-full px-3 py-2 border rounded-md ${
59                state.errors?.name ? 'border-red-500' : 'border-gray-300'
60              }`}
61            />
62            {state.errors?.name && (
63              <p className="text-red-500 text-xs mt-1">{state.errors.name}</p>
64            )}
65          </div>
66          
67          <div>
68            <label htmlFor="email" className="block text-sm font-medium mb-1">
69              Email *
70            </label>
71            <input
72              type="email"
73              id="email"
74              name="email"
75              className={`w-full px-3 py-2 border rounded-md ${
76                state.errors?.email ? 'border-red-500' : 'border-gray-300'
77              }`}
78            />
79            {state.errors?.email && (
80              <p className="text-red-500 text-xs mt-1">{state.errors.email}</p>
81            )}
82          </div>
83          
84          <div>
85            <label htmlFor="message" className="block text-sm font-medium mb-1">
86              Message *
87            </label>
88            <textarea
89              id="message"
90              name="message"
91              rows={4}
92              className={`w-full px-3 py-2 border rounded-md ${
93                state.errors?.messageContent ? 'border-red-500' : 'border-gray-300'
94              }`}
95            />
96            {state.errors?.messageContent && (
97              <p className="text-red-500 text-xs mt-1">{state.errors.messageContent}</p>
98            )}
99          </div>
100          
101          <SubmitButton />
102        </form>
103      )}
104    </div>
105  );
106}

And the related actions file:

1// app/contact/actions.ts
2'use server';
3
4import { z } from 'zod';
5
6// Validation schema
7const ContactSchema = z.object({
8  name: z.string().min(1, 'Name is required'),
9  email: z.string().email('Invalid email address'),
10  messageContent: z.string().min(10, 'Message must be at least 10 characters')
11});
12
13// Form state type
14type FormState = {
15  message: string;
16  errors?: Record<string, string>;
17  success: boolean;
18};
19
20export async function submitContactForm(prevState: FormState, formData: FormData): Promise<FormState> {
21  // Prepare data for validation
22  const validationData = {
23    name: formData.get('name'),
24    email: formData.get('email'),
25    messageContent: formData.get('message')
26  };
27  
28  // Validation
29  const result = ContactSchema.safeParse(validationData);
30  
31  if (!result.success) {
32    // Prepare error structure
33    const errors: Record<string, string> = {};
34    result.error.errors.forEach(error => {
35      if (error.path[0]) {
36        errors[error.path[0].toString()] = error.message;
37      }
38    });
39    
40    return {
41      message: 'The form contains errors. Fix them and try again.',
42      errors,
43      success: false
44    };
45  }
46  
47  try {
48    // Simulate network delay
49    await new Promise(resolve => setTimeout(resolve, 1000));
50    
51    // Here would be the code to send the message, e.g. save to database or send email
52    // await sendContactEmail(result.data);
53    
54    // Return success information
55    return {
56      message: '',
57      success: true
58    };
59  } catch (error) {
60    return {
61      message: 'An error occurred while sending the message. Please try again later.',
62      success: false
63    };
64  }
65}

In this example, we use:

  1. useFormState - for managing form state between client and server
  2. useFormStatus - for tracking form submission status (pending)
  3. Zod - for validating form data on the server
  4. Functional components - for organizing UI logic

Popular Form Libraries in React

Beyond the native approach, there are several excellent libraries that simplify working with forms in React and Next.js.

React Hook Form

React Hook Form is one of the most popular form handling libraries in React, which minimizes rendering and optimizes performance.

1import { useForm, SubmitHandler } from 'react-hook-form';
2import { zodResolver } from '@hookform/resolvers/zod';
3import { z } from 'zod';
4
5// Validation schema definition
6const loginFormSchema = z.object({
7  email: z.string().email('Invalid email address'),
8  password: z.string().min(6, 'Password must be at least 6 characters')
9});
10
11// Form data type inferred from Zod schema
12type LoginFormInputs = z.infer<typeof loginFormSchema>;
13
14export default function LoginForm() {
15  const {
16    register,
17    handleSubmit,
18    formState: { errors, isSubmitting }
19  } = useForm<LoginFormInputs>({
20    resolver: zodResolver(loginFormSchema),
21    defaultValues: {
22      email: '',
23      password: ''
24    }
25  });
26  
27  const onSubmit: SubmitHandler<LoginFormInputs> = async (data) => {
28    try {
29      // Simulate network delay
30      await new Promise(resolve => setTimeout(resolve, 1000));
31      
32      console.log('Login data:', data);
33      // In a real application: call login API
34      // await signIn('credentials', { email: data.email, password: data.password });
35    } catch (error) {
36      console.error('Login error:', error);
37    }
38  };
39  
40  return (
41    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
42      <div>
43        <label htmlFor="email" className="block text-sm font-medium">
44          Email
45        </label>
46        <input
47          id="email"
48          type="email"
49          className={`mt-1 block w-full rounded-md ${errors.email ? 'border-red-500' : 'border-gray-300'}`}
50          {...register('email')}
51        />
52        {errors.email && (
53          <p className="text-red-500 text-xs mt-1">{errors.email.message}</p>
54        )}
55      </div>
56      
57      <div>
58        <label htmlFor="password" className="block text-sm font-medium">
59          Password
60        </label>
61        <input
62          id="password"
63          type="password"
64          className={`mt-1 block w-full rounded-md ${errors.password ? 'border-red-500' : 'border-gray-300'}`}
65          {...register('password')}
66        />
67        {errors.password && (
68          <p className="text-red-500 text-xs mt-1">{errors.password.message}</p>
69        )}
70      </div>
71      
72      <button
73        type="submit"
74        disabled={isSubmitting}
75        className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
76      >
77        {isSubmitting ? 'Logging in...' : 'Log in'}
78      </button>
79    </form>
80  );
81}

Advantages of React Hook Form:

  1. Performance - minimally re-renders components
  2. Easy integration - works with unmodified HTML form elements
  3. Advanced validation - native integration with Zod, Yup, and other libraries
  4. Unconventional validation forms - validation dependent on other fields, conditional validation, etc.

Formik

Formik is another popular form library in React that offers a more declarative approach:

1import { Formik, Form, Field, ErrorMessage } from 'formik';
2import * as Yup from 'yup';
3
4// Yup validation schema
5const CheckoutSchema = Yup.object().shape({
6  firstName: Yup.string().required('Name is required'),
7  lastName: Yup.string().required('Last name is required'),
8  email: Yup.string().email('Invalid email').required('Email is required'),
9  address: Yup.string().required('Address is required'),
10  city: Yup.string().required('City is required'),
11  postalCode: Yup.string()
12    .matches(/^d{2}-d{3}$/, 'Invalid postal code (e.g. 00-000)')
13    .required('Postal code is required'),
14  paymentMethod: Yup.string().required('Select payment method')
15});
16
17export default function CheckoutForm() {
18  return (
19    <div className="max-w-md mx-auto mt-10">
20      <h1 className="text-2xl font-bold mb-6">Order Checkout</h1>
21      
22      <Formik
23        initialValues={{
24          firstName: '',
25          lastName: '',
26          email: '',
27          address: '',
28          city: '',
29          postalCode: '',
30          paymentMethod: ''
31        }}
32        validationSchema={CheckoutSchema}
33        onSubmit={async (values, { setSubmitting, resetForm }) => {
34          try {
35            // Simulate delay
36            await new Promise(resolve => setTimeout(resolve, 1000));
37            console.log('Checkout data:', values);
38            
39            // In a real application: call API
40            // await processCheckout(values);
41            
42            resetForm();
43            alert('Order accepted!');
44          } catch (error) {
45            console.error('Checkout error:', error);
46            alert('An error occurred while placing the order');
47          } finally {
48            setSubmitting(false);
49          }
50        }}
51      >
52        {({ isSubmitting }) => (
53          <Form className="space-y-4">
54            <div className="grid grid-cols-2 gap-4">
55              <div>
56                <label htmlFor="firstName" className="block text-sm font-medium">
57                  First name
58                </label>
59                <Field
60                  id="firstName"
61                  name="firstName"
62                  type="text"
63                  className="mt-1 block w-full rounded-md border-gray-300"
64                />
65                <ErrorMessage name="firstName" component="div" className="text-red-500 text-xs mt-1" />
66              </div>
67              
68              <div>
69                <label htmlFor="lastName" className="block text-sm font-medium">
70                  Last name
71                </label>
72                <Field
73                  id="lastName"
74                  name="lastName"
75                  type="text"
76                  className="mt-1 block w-full rounded-md border-gray-300"
77                />
78                <ErrorMessage name="lastName" component="div" className="text-red-500 text-xs mt-1" />
79              </div>
80            </div>
81            
82            <div>
83              <label htmlFor="email" className="block text-sm font-medium">
84                Email
85              </label>
86              <Field
87                id="email"
88                name="email"
89                type="email"
90                className="mt-1 block w-full rounded-md border-gray-300"
91              />
92              <ErrorMessage name="email" component="div" className="text-red-500 text-xs mt-1" />
93            </div>
94            
95            <div>
96              <label htmlFor="address" className="block text-sm font-medium">
97                Address
98              </label>
99              <Field
100                id="address"
101                name="address"
102                type="text"
103                className="mt-1 block w-full rounded-md border-gray-300"
104              />
105              <ErrorMessage name="address" component="div" className="text-red-500 text-xs mt-1" />
106            </div>
107            
108            <div className="grid grid-cols-2 gap-4">
109              <div>
110                <label htmlFor="city" className="block text-sm font-medium">
111                  City
112                </label>
113                <Field
114                  id="city"
115                  name="city"
116                  type="text"
117                  className="mt-1 block w-full rounded-md border-gray-300"
118                />
119                <ErrorMessage name="city" component="div" className="text-red-500 text-xs mt-1" />
120              </div>
121              
122              <div>
123                <label htmlFor="postalCode" className="block text-sm font-medium">
124                  Postal code
125                </label>
126                <Field
127                  id="postalCode"
128                  name="postalCode"
129                  type="text"
130                  className="mt-1 block w-full rounded-md border-gray-300"
131                />
132                <ErrorMessage name="postalCode" component="div" className="text-red-500 text-xs mt-1" />
133              </div>
134            </div>
135            
136            <div>
137              <label className="block text-sm font-medium mb-2">
138                Payment method
139              </label>
140              
141              <div className="space-y-2">
142                <label className="flex items-center">
143                  <Field type="radio" name="paymentMethod" value="card" className="mr-2" />
144                  Credit card
145                </label>
146                
147                <label className="flex items-center">
148                  <Field type="radio" name="paymentMethod" value="blik" className="mr-2" />
149                  BLIK
150                </label>
151                
152                <label className="flex items-center">
153                  <Field type="radio" name="paymentMethod" value="transfer" className="mr-2" />
154                  Bank transfer
155                </label>
156              </div>
157              
158              <ErrorMessage name="paymentMethod" component="div" className="text-red-500 text-xs mt-1" />
159            </div>
160            
161            <button
162              type="submit"
163              disabled={isSubmitting}
164              className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
165            >
166              {isSubmitting ? 'Processing...' : 'Place order'}
167            </button>
168          </Form>
169        )}
170      </Formik>
171    </div>
172  );
173}

Advantages of Formik:

  1. Deklaratywny API - komponenty
    Form
    ,
    Field
    ,
    ErrorMessage
  2. Good documentation - extensive documentation and examples
  3. Maturity - stable and proven in production
  4. Integration with Yup - easy validation with the Yup library

Summary

In this module, we explored different approaches to creating forms in React and Next.js:

  1. Natywne formularze React - kontrolowane komponenty z lokalnym stanem
  2. Server Actions in Next.js - a new server-side approach
  3. React Hook Form - an efficient library with minimal rendering
  4. Formik - a mature library with declarative API

Choosing the right approach depends on the specifics of the project:

  • For simple forms: native React solutions
  • For Next.js applications: Server Actions with Progressive Enhancement
  • For complex client-side forms: React Hook Form or Formik

Regardless of the chosen approach, it is worth keeping in mind a few good practices:

  • Walidacja na wielu poziomach - klient + serwer
  • Accessibility - proper labels, ARIA attributes, keyboard support
  • Feedback - loading states, errors, confirmations
  • Security - CSRF protection, server-side data validation

In the next modules, we will explore more advanced form handling techniques, including file uploads, multi-step forms, and integrations with external APIs.

Go to CodeWorlds