Kurs Next.js · Moduł 6: Formularze i Server Actions
Tworzenie formularzy w React i Next.js
W tej lekcji4
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.
Podstawowe formularze w React
W najbardziej podstawowej formie, formularz w React jest podobny do tradycyjnego formularza HTML, ale z dodatkową warstwą zarządzania stanem.
Kontrolowane komponenty formularza
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:
- Każde pole formularza jest połączone z odpowiednią zmienną stanu
- Zdarzenie
onChangeaktualizuje stan dla każdego pola - Zdarzenie
onSubmitcałego formularza obsługuje przesłanie danych - Użycie
event.preventDefault()zapobiega domyślnemu odświeżaniu strony
Zarządzanie wieloma polami formularza
Kiedy 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:
- Pojedyncza funkcja handleChange obsługuje wszystkie pola
- Dynamiczne aktualizowanie stanu dzięki składni
[name]: value - Typowanie TypeScript zapewnia bezpieczeństwo typów
- Walidacja z komunikatami o błędach poprawia doświadczenie użytkownika
Formularze w Next.js z Server Actions
Next.js 13+ wprowadził Server Actions, które umożliwiają bezpośrednie przetwarzanie formularzy na serwerze bez konieczności tworzenia osobnych endpointów API.
Podstawowy formularz z 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 // 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:
- Uproszczona architektura - nie trzeba tworzyć osobnych API Routes
- Progresywne ulepszanie - formularze działają nawet bez JavaScript
- Bezpieczne przetwarzanie danych - logika weryfikacji i przetwarzania danych jest wykonywana na serwerze
- Typowanie end-to-end - TypeScript może zapewnić typowanie od formularza po bazę danych
Formularze ze stanami ładowania i walidacją
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:
- useFormState - do zarządzania stanem formularza pomiędzy klientem a serwerem
- useFormStatus - do śledzenia stanu wysyłki formularza (pending)
- Zod - do validacji danych formularza na serwerze
- Komponentów funkcyjnych - do organizacji logiki UI
Popularne biblioteki do formularzy w React
Poza natywnym podejściem, istnieje kilka doskonałych bibliotek, które upraszczają pracę z formularzami w React i Next.js.
React Hook Form
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:
- Wydajność - minimalnie przerenderowuje komponenty
- Łatwa integracja - działa z niemodyfikowanymi elementami HTML formularza
- Zaawansowana walidacja - natywna integracja z Zod, Yup i innymi bibliotekami
- Niekonwencjonalne formy walidacji - walidacja zależna od innych pól, warunkowa walidacja, itp.
Formik
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:
- Deklaratywny API - komponenty
Form,Field,ErrorMessage - Dobra dokumentacja - obszerna dokumentacja i przykłady
- Dojrzałość - stabilna i sprawdzona w produkcji
- Integracja z Yup - łatwa walidacja z bibliotekami Yup
Podsumowanie
W tym module poznaliśmy różne podejścia do tworzenia formularzy w React i Next.js:
- Natywne formularze React - kontrolowane komponenty z lokalnym stanem
- Server Actions w Next.js - nowe podejście po stronie serwera
- React Hook Form - wydajna biblioteka z minimalnym renderowaniem
- Formik - dojrzała biblioteka z deklaratywnym API
Wybór odpowiedniego podejścia zależy od specyfiki projektu:
- Dla prostych formularzy: natywne rozwiązania React
- Dla aplikacji Next.js: Server Actions z Progressive Enhancement
- Dla złożonych formularzy klienckich: React Hook Form lub Formik
Niezależnie od wybranego podejścia, warto pamiętać o kilku dobrych praktykach:
- Walidacja na wielu poziomach - klient + serwer
- Dostępność - poprawne etykiety, atrybuty ARIA, działanie z klawiaturą
- Informacje zwrotne - ładowanie, błędy, potwierdzenia
- Bezpieczeństwo - zabezpieczenie przed CSRF, walidacja danych na serwerze
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.
Kod do tej lekcji: App.tsx
1// Demo: Tworzenie formularzy w React i Next.js
2// Podstawy formularzy: controlled components, eventy, submit
3import React, { useState, FormEvent } from 'react';
4
5// W React formularze działają jako "controlled components"
6// Stan React kontroluje wartości pól formularza
7// onChange aktualizuje stan -> React re-renderuje input z nową wartością
8
9// Podstawowy wzorzec:
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(); // Zapobiegamy domyślnemu przeładowaniu strony
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 zarządza stanem formularza
86 </p>
87
88 <form onSubmit={handleSubmit}>
89 <FormInput label="Imię" 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="Temat" 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 Priorytet
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 }}>Wiadomość</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 {/* Podgląd stanu React na żywo */}
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 (na żywo):
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' }}>Wysłano!</h4>
166 <p style={{ color: '#b0bec5', fontSize: '0.85rem', margin: 0 }}>
167 Od: {submittedData.name} ({submittedData.email})
168 </p>
169 </div>
170 )}
171 </div>
172 </div>
173 );
174}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. W React, element <input> w formularzu jest kontrolowany, gdy: