Formularze są fundamentalnym elementem interaktywnych aplikacji webowych. Pozwalają użytkownikom na wprowadzanie danych, które następnie mogą być przetwarzane przez aplikację. W środowisku React i Next.js tworzenie formularzy może być realizowane na różne sposoby - od prostych rozwiązań natywnych po zaawansowane biblioteki. W tym module przyjrzymy się różnym podejściom do implementacji formularzy.
W najbardziej podstawowej formie, formularz w React jest podobny do tradycyjnego formularza HTML, ale z dodatkową warstwą zarządzania stanem.
Najczęściej stosowanym podejściem w React są "kontrolowane komponenty", gdzie wartość każdego elementu formularza jest utrzymywana w stanie React:
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('Dane formularza:', { name, email, message });
11
12 // Tutaj możesz wysłać dane do API
13 // fetch('/api/contact', {
14 // method: 'POST',
15 // body: JSON.stringify({ name, email, message }),
16 // headers: { 'Content-Type': 'application/json' }
17 // })
18
19 // Opcjonalnie zresetuj formularz
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 Imię i nazwisko
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 Wiadomość
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 Wyślij
74 </button>
75 </form>
76 );
77}W powyższym przykładzie:
onChange aktualizuje stan dla każdego polaonSubmit całego formularza obsługuje przesłanie danychevent.preventDefault() zapobiega domyślnemu odświeżaniu stronyKiedy formularz ma wiele pól, utrzymywanie osobnego stanu dla każdego z nich może być uciążliwe. Bardziej skalowalne podejście to używanie jednego obiektu stanu:
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 // Resetowanie błędu podczas edycji
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 = 'Imię jest wymagane';
43 isValid = false;
44 }
45
46 if (!formData.email.trim()) {
47 newErrors.email = 'Email jest wymagany';
48 isValid = false;
49 } else if (!/^S+@S+.S+$/.test(formData.email)) {
50 newErrors.email = 'Email jest niepoprawny';
51 isValid = false;
52 }
53
54 if (!formData.message.trim()) {
55 newErrors.message = 'Wiadomość jest wymagana';
56 isValid = false;
57 }
58
59 if (!formData.agreeToTerms) {
60 newErrors.agreeToTerms = 'Wymagana jest zgoda na warunki';
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('Przesyłanie formularza:', formData);
73 // Wysyłka do API...
74
75 // Resetowanie formularza po udanym przesłaniu
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 Imię i nazwisko *
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 Temat
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="">Wybierz temat</option>
130 <option value="general">Zapytanie ogólne</option>
131 <option value="support">Wsparcie techniczne</option>
132 <option value="feedback">Opinia</option>
133 </select>
134 </div>
135
136 <div>
137 <label htmlFor="message" className="block text-sm font-medium">
138 Wiadomość *
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 Zgadzam się na warunki i politykę prywatności *
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 Wyślij wiadomość
173 </button>
174 </form>
175 );
176}To podejście oferuje następujące zalety:
[name]: valueNext.js 13+ wprowadził Server Actions, które umożliwiają bezpośrednie przetwarzanie formularzy na serwerze bez konieczności tworzenia osobnych endpointów API.
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 // Wywołanie Server Action
19 const result = await registerUser(formData);
20
21 if (result.success) {
22 // Przekierowanie po udanej rejestracji
23 router.push('/signup/confirm');
24 } else {
25 // Wyświetlenie błędu
26 setError(result.error || 'Something went wrong');
27 }
28 } catch (e) {
29 setError('Wystąpił nieoczekiwany błąd.');
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">Rejestracja</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 Imię i nazwisko
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 Hasło
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 ? 'Rejestracja...' : 'Zarejestruj się'}
92 </button>
93 </form>
94 </div>
95 );
96}A oto plik z Server Action:
1// app/signup/actions.ts
2'use server';
3
4import { z } from 'zod';
5
6// Schemat walidacji formularza
7const UserSchema = z.object({
8 name: z.string().min(2, { message: 'Imię jest zbyt krótkie' }),
9 email: z.string().email({ message: 'Nieprawidłowy adres email' }),
10 password: z.string().min(8, { message: 'Hasło musi mieć co najmniej 8 znaków' })
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. Walidacja danych formularza
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 || 'Niepoprawne dane formularza'
29 };
30 }
31
32 const { name, email, password } = validatedFields.data;
33
34 try {
35 // 2. Sprawdzenie czy użytkownik już istnieje
36 const existingUser = false; // Tu powinna być prawdziwa logika sprawdzenia w bazie danych
37
38 if (existingUser) {
39 return {
40 success: false,
41 error: 'Użytkownik o podanym adresie email już istnieje'
42 };
43 }
44
45 // 3. Tworzenie nowego użytkownika (w rzeczywistej aplikacji - zapis do bazy)
46 // const newUser = await db.user.create({ data: { name, email, password: hashedPassword } });
47
48 // 4. Dodatkowe operacje - wysyłka emaila powitalnego, logowanie, itp.
49 // await sendWelcomeEmail(email, name);
50
51 return {
52 success: true,
53 userId: '123' // W rzeczywistej aplikacji id utworzonego użytkownika
54 };
55 } catch (error) {
56 console.error('Registration error:', error);
57 return {
58 success: false,
59 error: 'Wystąpił błąd podczas rejestracji. Spróbuj ponownie później.'
60 };
61 }
62}Zalety Server Actions:
Możemy połączyć Server Actions z React Hooks, aby zapewnić jeszcze lepsze doświadczenie użytkownika:
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// Komponent przycisku z obsługą stanu ładowania
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 ? 'Wysyłanie...' : 'Wyślij wiadomość'}
19 </button>
20 );
21}
22
23export default function ContactPage() {
24 // Początkowy stan formularza
25 const initialState = {
26 message: '',
27 errors: {},
28 success: false
29 };
30
31 // Użycie hooka useFormState do zarządzania stanem formularza
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">Formularz kontaktowy</h1>
37
38 {state.success ? (
39 <div className="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
40 Dziękujemy za wiadomość! Odpowiemy najszybciej jak to możliwe.
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 Imię i nazwisko *
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 Wiadomość *
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}I powiązany plik akcji:
1// app/contact/actions.ts
2'use server';
3
4import { z } from 'zod';
5
6// Schemat walidacji
7const ContactSchema = z.object({
8 name: z.string().min(1, 'Imię jest wymagane'),
9 email: z.string().email('Nieprawidłowy adres email'),
10 messageContent: z.string().min(10, 'Wiadomość musi mieć co najmniej 10 znaków')
11});
12
13// Typ stanu formularza
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 // Przygotowanie danych do walidacji
22 const validationData = {
23 name: formData.get('name'),
24 email: formData.get('email'),
25 messageContent: formData.get('message')
26 };
27
28 // Walidacja
29 const result = ContactSchema.safeParse(validationData);
30
31 if (!result.success) {
32 // Przygotowanie struktury błędów
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: 'Formularz zawiera błędy. Popraw je i spróbuj ponownie.',
42 errors,
43 success: false
44 };
45 }
46
47 try {
48 // Symulacja opóźnienia sieciowego
49 await new Promise(resolve => setTimeout(resolve, 1000));
50
51 // Tutaj byłby kod do wysyłki wiadomości, np. zapis do bazy lub wysyłka emaila
52 // await sendContactEmail(result.data);
53
54 // Zwracamy informację o sukcesie
55 return {
56 message: '',
57 success: true
58 };
59 } catch (error) {
60 return {
61 message: 'Wystąpił błąd podczas wysyłania wiadomości. Spróbuj ponownie później.',
62 success: false
63 };
64 }
65}W tym przykładzie używamy:
Poza natywnym podejściem, istnieje kilka doskonałych bibliotek, które upraszczają pracę z formularzami w React i Next.js.
React Hook Form to jedna z najpopularniejszych bibliotek do obsługi formularzy w Reakcie, która minimalizuje renderowanie i optymalizuje wydajność.
1import { useForm, SubmitHandler } from 'react-hook-form';
2import { zodResolver } from '@hookform/resolvers/zod';
3import { z } from 'zod';
4
5// Definicja schematu walidacji
6const loginFormSchema = z.object({
7 email: z.string().email('Nieprawidłowy adres email'),
8 password: z.string().min(6, 'Hasło musi mieć co najmniej 6 znaków')
9});
10
11// Typ danych formularza inferowany ze schematu Zod
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 // Symulacja opóźnienia sieciowego
30 await new Promise(resolve => setTimeout(resolve, 1000));
31
32 console.log('Login data:', data);
33 // W rzeczywistej aplikacji: wywołanie API do logowania
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 Hasło
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 ? 'Logowanie...' : 'Zaloguj się'}
78 </button>
79 </form>
80 );
81}Zalety React Hook Form:
Formik to inna popularna biblioteka do formularzy w React, która oferuje bardziej deklaratywne podejście:
1import { Formik, Form, Field, ErrorMessage } from 'formik';
2import * as Yup from 'yup';
3
4// Schemat walidacji Yup
5const CheckoutSchema = Yup.object().shape({
6 firstName: Yup.string().required('Imię jest wymagane'),
7 lastName: Yup.string().required('Nazwisko jest wymagane'),
8 email: Yup.string().email('Nieprawidłowy email').required('Email jest wymagany'),
9 address: Yup.string().required('Adres jest wymagany'),
10 city: Yup.string().required('Miasto jest wymagane'),
11 postalCode: Yup.string()
12 .matches(/^d{2}-d{3}$/, 'Nieprawidłowy kod pocztowy (np. 00-000)')
13 .required('Kod pocztowy jest wymagany'),
14 paymentMethod: Yup.string().required('Wybierz metodę płatności')
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">Finalizacja zamówienia</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 // Symulacja opóźnienia
36 await new Promise(resolve => setTimeout(resolve, 1000));
37 console.log('Checkout data:', values);
38
39 // W rzeczywistej aplikacji: wywołanie API
40 // await processCheckout(values);
41
42 resetForm();
43 alert('Zamówienie przyjęte!');
44 } catch (error) {
45 console.error('Checkout error:', error);
46 alert('Wystąpił błąd podczas składania zamówienia');
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 Imię
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 Nazwisko
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 Adres
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 Miasto
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 Kod pocztowy
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 Metoda płatności
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 Karta płatnicza
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 Przelew bankowy
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 ? 'Przetwarzanie...' : 'Złóż zamówienie'}
167 </button>
168 </Form>
169 )}
170 </Formik>
171 </div>
172 );
173}Zalety Formik:
Form, Field, ErrorMessageW tym module poznaliśmy różne podejścia do tworzenia formularzy w React i Next.js:
Wybór odpowiedniego podejścia zależy od specyfiki projektu:
Niezależnie od wybranego podejścia, warto pamiętać o kilku dobrych praktykach:
W następnych modułach zgłębimy bardziej zaawansowane techniki obsługi formularzy, w tym przesyłanie plików, wielokrokowe formularze i integracje z zewnętrznymi API.