Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Tworzenie formularzy w React i Next.js

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:

  1. Każde pole formularza jest połączone z odpowiednią zmienną stanu
  2. Zdarzenie
    onChange
    aktualizuje stan dla każdego pola
  3. Zdarzenie
    onSubmit
    całego formularza obsługuje przesłanie danych
  4. 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:

  1. Pojedyncza funkcja handleChange obsługuje wszystkie pola
  2. Dynamiczne aktualizowanie stanu dzięki składni
    [name]: value
  3. Typowanie TypeScript zapewnia bezpieczeństwo typów
  4. 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:

  1. Uproszczona architektura - nie trzeba tworzyć osobnych API Routes
  2. Progresywne ulepszanie - formularze działają nawet bez JavaScript
  3. Bezpieczne przetwarzanie danych - logika weryfikacji i przetwarzania danych jest wykonywana na serwerze
  4. 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:

  1. useFormState - do zarządzania stanem formularza pomiędzy klientem a serwerem
  2. useFormStatus - do śledzenia stanu wysyłki formularza (pending)
  3. Zod - do validacji danych formularza na serwerze
  4. 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:

  1. Wydajność - minimalnie przerenderowuje komponenty
  2. Łatwa integracja - działa z niemodyfikowanymi elementami HTML formularza
  3. Zaawansowana walidacja - natywna integracja z Zod, Yup i innymi bibliotekami
  4. 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:

  1. Deklaratywny API - komponenty
    Form
    ,
    Field
    ,
    ErrorMessage
  2. Dobra dokumentacja - obszerna dokumentacja i przykłady
  3. Dojrzałość - stabilna i sprawdzona w produkcji
  4. 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:

  1. Natywne formularze React - kontrolowane komponenty z lokalnym stanem
  2. Server Actions w Next.js - nowe podejście po stronie serwera
  3. React Hook Form - wydajna biblioteka z minimalnym renderowaniem
  4. 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.

Przejdź do CodeWorlds