Next.js course Β· Module 6: Forms and Server Actions
Creating Forms in React and Next.js
In this lesson4
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:
- Each form field is connected to a corresponding state variable
- The
onChangeevent updates state for each field - The
onSubmitevent for the whole form handles data submission - 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:
- A single handleChange function handles all fields
- Dynamic state updates thanks to the
[name]: valuesyntax - TypeScript typing ensures type safety
- 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:
- Simplified architecture - no need to create separate API Routes
- Progressive enhancement - forms work even without JavaScript
- Secure data processing - verification and processing logic runs on the server
- 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:
- useFormState - for managing form state between client and server
- useFormStatus - for tracking form submission status (pending)
- Zod - for validating form data on the server
- 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:
- Performance - minimally re-renders components
- Easy integration - works with unmodified HTML form elements
- Advanced validation - native integration with Zod, Yup, and other libraries
- 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:
- Deklaratywny API - komponenty
Form,Field,ErrorMessage - Good documentation - extensive documentation and examples
- Maturity - stable and proven in production
- 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:
- Natywne formularze React - kontrolowane komponenty z lokalnym stanem
- Server Actions in Next.js - a new server-side approach
- React Hook Form - an efficient library with minimal rendering
- 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.
Code for this lesson: App.tsx
1// Demo: Creating forms in React and Next.js
2// Form basics: controlled components, events, submit
3import React, { useState, FormEvent } from 'react';
4
5// In React forms work as "controlled components"
6// React state controls form field values
7// onChange updates state -> React re-renders input with new value
8
9// Basic pattern:
10// const [value, setValue] = useState('');
11// <input value={value} onChange={e => setValue(e.target.value)} />
12
13interface ContactData {
14 name: string;
15 email: string;
16 subject: string;
17 message: string;
18 priority: 'low' | 'medium' | 'high';
19}
20
21const initialData: ContactData = {
22 name: '',
23 email: '',
24 subject: '',
25 message: '',
26 priority: 'medium',
27};
28
29function FormInput({ label, id, type = 'text', value, onChange, placeholder }: {
30 label: string; id: string; type?: string;
31 value: string; onChange: (val: string) => void; placeholder?: string;
32}) {
33 return (
34 <div style={{ marginBottom: '16px' }}>
35 <label htmlFor={id} style={{
36 display: 'block', color: '#b0bec5', fontSize: '0.85rem', marginBottom: '6px',
37 }}>{label}</label>
38 <input
39 id={id} type={type} value={value}
40 onChange={e => onChange(e.target.value)}
41 placeholder={placeholder}
42 style={{
43 width: '100%', padding: '12px 16px', boxSizing: 'border-box',
44 background: 'rgba(255,255,255,0.05)',
45 border: '1px solid rgba(100,255,218,0.3)',
46 borderRadius: '8px', color: '#fff', fontSize: '1rem', outline: 'none',
47 }}
48 />
49 </div>
50 );
51}
52
53export default function ContactFormDemo() {
54 const [formData, setFormData] = useState<ContactData>(initialData);
55 const [submitted, setSubmitted] = useState(false);
56 const [submittedData, setSubmittedData] = useState<ContactData | null>(null);
57
58 const updateField = (field: keyof ContactData) => (value: string) => {
59 setFormData(prev => ({ ...prev, [field]: value }));
60 };
61
62 const handleSubmit = (e: FormEvent) => {
63 e.preventDefault(); // Prevent default page reload
64 setSubmittedData(formData);
65 setSubmitted(true);
66 };
67
68 const handleReset = () => {
69 setFormData(initialData);
70 setSubmitted(false);
71 setSubmittedData(null);
72 };
73
74 const priorityColors = { low: '#4caf50', medium: '#ff9800', high: '#f44336' };
75
76 return (
77 <div style={{
78 background: '#0f0f23', minHeight: '100vh', padding: '24px',
79 color: '#fff', fontFamily: 'system-ui, sans-serif',
80 display: 'flex', justifyContent: 'center',
81 }}>
82 <div style={{ maxWidth: '500px', width: '100%' }}>
83 <h1 style={{ color: '#64ffda', marginBottom: '4px' }}>Quantum Contact Form</h1>
84 <p style={{ color: '#b0bec5', marginBottom: '24px', fontSize: '0.9rem' }}>
85 Controlled components - React manages form state
86 </p>
87
88 <form onSubmit={handleSubmit}>
89 <FormInput label="Name" id="name" value={formData.name}
90 onChange={updateField('name')} placeholder="Agent name..." />
91 <FormInput label="Email" id="email" type="email" value={formData.email}
92 onChange={updateField('email')} placeholder="agent@quantum.io" />
93 <FormInput label="Subject" id="subject" value={formData.subject}
94 onChange={updateField('subject')} placeholder="Subject..." />
95
96 <div style={{ marginBottom: '16px' }}>
97 <label style={{ display: 'block', color: '#b0bec5', fontSize: '0.85rem', marginBottom: '8px' }}>
98 Priority
99 </label>
100 <div style={{ display: 'flex', gap: '8px' }}>
101 {(['low', 'medium', 'high'] as const).map(p => (
102 <button key={p} type="button"
103 onClick={() => updateField('priority')(p)}
104 style={{
105 flex: 1, padding: '10px', borderRadius: '8px',
106 background: formData.priority === p ? `${priorityColors[p]}20` : 'transparent',
107 border: `1px solid ${formData.priority === p ? priorityColors[p] : 'rgba(255,255,255,0.1)'}`,
108 color: formData.priority === p ? priorityColors[p] : '#78909c',
109 cursor: 'pointer', textTransform: 'capitalize',
110 }}
111 >{p}</button>
112 ))}
113 </div>
114 </div>
115
116 <div style={{ marginBottom: '16px' }}>
117 <label htmlFor="message" style={{
118 display: 'block', color: '#b0bec5', fontSize: '0.85rem', marginBottom: '6px',
119 }}>Message</label>
120 <textarea id="message" rows={4} value={formData.message}
121 onChange={e => updateField('message')(e.target.value)}
122 style={{
123 width: '100%', padding: '12px', boxSizing: 'border-box',
124 background: 'rgba(255,255,255,0.05)',
125 border: '1px solid rgba(100,255,218,0.3)',
126 borderRadius: '8px', color: '#fff', fontSize: '1rem',
127 outline: 'none', resize: 'vertical',
128 }}
129 />
130 </div>
131
132 <div style={{ display: 'flex', gap: '10px' }}>
133 <button type="submit" style={{
134 flex: 2, background: '#64ffda', color: '#0f0f23',
135 border: 'none', padding: '14px', borderRadius: '8px',
136 cursor: 'pointer', fontWeight: 600, fontSize: '1rem',
137 }}>Submit</button>
138 <button type="button" onClick={handleReset} style={{
139 flex: 1, background: 'transparent', color: '#78909c',
140 border: '1px solid rgba(255,255,255,0.1)', padding: '14px',
141 borderRadius: '8px', cursor: 'pointer',
142 }}>Reset</button>
143 </div>
144 </form>
145
146 {/* Live React state preview */}
147 <div style={{
148 marginTop: '24px', background: 'rgba(0,0,0,0.3)',
149 borderRadius: '8px', padding: '16px',
150 }}>
151 <h4 style={{ color: '#78909c', marginBottom: '8px', fontSize: '0.8rem' }}>
152 React State (live):
153 </h4>
154 <pre style={{ margin: 0, color: '#b0bec5', fontSize: '0.75rem', lineHeight: 1.5 }}>
155 {JSON.stringify(formData, null, 2)}
156 </pre>
157 </div>
158
159 {submitted && submittedData && (
160 <div style={{
161 marginTop: '16px', background: 'rgba(76,175,80,0.1)',
162 border: '1px solid rgba(76,175,80,0.3)',
163 borderRadius: '8px', padding: '16px',
164 }}>
165 <h4 style={{ color: '#4caf50', marginBottom: '8px' }}>Sent!</h4>
166 <p style={{ color: '#b0bec5', fontSize: '0.85rem', margin: 0 }}>
167 From: {submittedData.name} ({submittedData.email})
168 </p>
169 </div>
170 )}
171 </div>
172 </div>
173 );
174}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. In React, an <input> element in a form is controlled when: