W centrum dowodzenia Metropolii Quantum 2150, gdzie operacje na danych odbywaja sie z precyzja kwantowa, poznasz zaawansowane wzorce Server Actions. To potezne narzedzie Next.js, ktore pozwala na bezposrednie wywolywanie funkcji serwerowych z poziomu komponentow React - bez pisania osobnych endpointow API.
Hook
useOptimistic pozwala na natychmiastowe aktualizowanie UI zanim serwer potwierdzi operacje. Uzytkownik widzi efekt dzialania od razu, a jesli cos pojdzie nie tak - stan wraca do poprzedniego.1// app/tasks/page.tsx
2'use client';
3
4import { useOptimistic } from 'react';
5import { addTask, toggleTask } from './actions';
6
7interface Task {
8 id: string;
9 title: string;
10 completed: boolean;
11}
12
13export default function TaskList({ tasks }: { tasks: Task[] }) {
14 const [optimisticTasks, addOptimisticTask] = useOptimistic(
15 tasks,
16 (state: Task[], newTask: Task) => [...state, newTask]
17 );
18
19 async function handleAddTask(formData: FormData) {
20 const title = formData.get('title') as string;
21
22 // Natychmiastowa aktualizacja UI
23 addOptimisticTask({
24 id: 'temp-' + Date.now(),
25 title,
26 completed: false,
27 });
28
29 // Wywolanie Server Action
30 await addTask(formData);
31 }
32
33 return (
34 <div>
35 <form action={handleAddTask}>
36 <input name="title" placeholder="Nowe zadanie..." />
37 <button type="submit">Dodaj</button>
38 </form>
39
40 <ul>
41 {optimisticTasks.map((task) => (
42 <li key={task.id}>{task.title}</li>
43 ))}
44 </ul>
45 </div>
46 );
47}1// app/tasks/actions.ts
2'use server';
3
4import { revalidatePath } from 'next/cache';
5import { db } from '@/lib/db';
6
7export async function addTask(formData: FormData) {
8 const title = formData.get('title') as string;
9
10 await db.tasks.create({
11 data: { title, completed: false },
12 });
13
14 revalidatePath('/tasks');
15}
16
17export async function toggleTask(taskId: string) {
18 const task = await db.tasks.findUnique({ where: { id: taskId } });
19
20 await db.tasks.update({
21 where: { id: taskId },
22 data: { completed: !task?.completed },
23 });
24
25 revalidatePath('/tasks');
26}Po kazdej operacji Server Action nalezy odswiezyc dane. Next.js oferuje dwa mechanizmy:
revalidatePath i revalidateTag.1// actions/products.ts
2'use server';
3
4import { revalidatePath } from 'next/cache';
5
6export async function updateProduct(productId: string, formData: FormData) {
7 const name = formData.get('name') as string;
8 const price = parseFloat(formData.get('price') as string);
9
10 await db.products.update({
11 where: { id: productId },
12 data: { name, price },
13 });
14
15 // Odswiez konkretna strone produktu
16 revalidatePath('/products/' + productId);
17
18 // Odswiez liste produktow
19 revalidatePath('/products');
20
21 // Odswiez caly layout
22 revalidatePath('/', 'layout');
23}1// lib/data.ts
2import { unstable_cache } from 'next/cache';
3
4export const getProducts = unstable_cache(
5 async () => {
6 return await db.products.findMany();
7 },
8 ['products'],
9 { tags: ['products'] }
10);
11
12export const getProduct = unstable_cache(
13 async (id: string) => {
14 return await db.products.findUnique({ where: { id } });
15 },
16 ['product'],
17 { tags: ['products', 'product-detail'] }
18);1// actions/products.ts
2'use server';
3
4import { revalidateTag } from 'next/cache';
5
6export async function deleteProduct(productId: string) {
7 await db.products.delete({ where: { id: productId } });
8
9 // Odswiez wszystkie dane oznaczone tagiem 'products'
10 revalidateTag('products');
11}Prawidlowa obsluga bledow to klucz do niezawodnej aplikacji. Server Actions powinny zwracac ustrukturyzowane odpowiedzi z informacja o sukcesie or bledzie.
1// actions/auth.ts
2'use server';
3
4import { z } from 'zod';
5
6const loginSchema = z.object({
7 email: z.string().email('Nieprawidlowy email'),
8 password: z.string().min(8, 'Haslo musi miec min. 8 znakow'),
9});
10
11interface ActionResult {
12 success: boolean;
13 error?: string;
14 fieldErrors?: Record<string, string[]>;
15}
16
17export async function loginAction(
18 prevState: ActionResult,
19 formData: FormData
20): Promise<ActionResult> {
21 const rawData = {
22 email: formData.get('email'),
23 password: formData.get('password'),
24 };
25
26 // Validate data
27 const validatedFields = loginSchema.safeParse(rawData);
28
29 if (!validatedFields.success) {
30 return {
31 success: false,
32 fieldErrors: validatedFields.error.flatten().fieldErrors,
33 };
34 }
35
36 try {
37 const user = await authenticateUser(
38 validatedFields.data.email,
39 validatedFields.data.password
40 );
41
42 if (!user) {
43 return { success: false, error: 'Nieprawidlowe dane logowania' };
44 }
45
46 // Utworz sesje
47 await createSession(user.id);
48 return { success: true };
49 } catch (error) {
50 return { success: false, error: 'Wystapil blad serwera' };
51 }
52}1// app/login/page.tsx
2'use client';
3
4import { useActionState } from 'react';
5import { loginAction } from './actions';
6
7export default function LoginForm() {
8 const [state, formAction, andsPending] = useActionState(loginAction, {
9 success: false,
10 });
11
12 return (
13 <form action={formAction}>
14 <div>
15 <input name="email" type="email" placeholder="Email" />
16 {state.fieldErrors?.email && (
17 <p className="text-red-500">{state.fieldErrors.email[0]}</p>
18 )}
19 </div>
20
21 <div>
22 <input name="password" type="password" placeholder="Haslo" />
23 {state.fieldErrors?.password && (
24 <p className="text-red-500">{state.fieldErrors.password[0]}</p>
25 )}
26 </div>
27
28 {state.error && (
29 <div className="bg-red-100 text-red-700 p-3 rounded">
30 {state.error}
31 </div>
32 )}
33
34 <button type="submit" disabled={isPending}>
35 {isPending ? 'Logging in...' : 'Zaloguj sie'}
36 </button>
37 </form>
38 );
39}Server Actions dzialaja nawet bez JavaScript po page klienta. Formularz HTML domyslnie wysyla dane metoda POST, wiec caly flow dziala natywnie.
1// app/contact/page.tsx
2import { sendMessage } from './actions';
3
4// Ten formularz dziala nawet z wylaczonym JavaScript!
5export default function ContactForm() {
6 return (
7 <form action={sendMessage}>
8 <input name="name" required placeholder="Imie" />
9 <input name="email" type="email" required placeholder="Email" />
10 <textarea name="message" required placeholder="Wiadomosc" />
11 <button type="submit">Wyslij</button>
12 </form>
13 );
14}1// app/contact/actions.ts
2'use server';
3
4import { redirect } from 'next/navigation';
5
6export async function sendMessage(formData: FormData) {
7 const name = formData.get('name') as string;
8 const email = formData.get('email') as string;
9 const message = formData.get('message') as string;
10
11 await db.messages.create({
12 data: { name, email, message },
13 });
14
15 redirect('/contact/success');
16}Mozna komponowac wiele Server Actions razem, tworzac zlozony flow:
1// actions/checkout.ts
2'use server';
3
4import { revalidatePath } from 'next/cache';
5import { redirect } from 'next/navigation';
6
7export async function processCheckout(formData: FormData) {
8 const cartId = formData.get('cartId') as string;
9
10 // 1. Walidacja koszyka
11 const cart = await validateCart(cartId);
12 if (!cart.valid) {
13 return { error: 'Koszyk jest nieprawidlowy' };
14 }
15
16 // 2. Przetworzenie platnosci
17 const payment = await processPayment({
18 amount: cart.total,
19 method: formData.get('paymentMethod') as string,
20 });
21
22 if (!payment.success) {
23 return { error: 'Platnosc nie powiodla sie' };
24 }
25
26 // 3. Utworzenie zamowienia
27 const order = await createOrder({
28 cartId,
29 paymentId: payment.id,
30 shippingAddress: formData.get('address') as string,
31 });
32
33 // 4. Odswiez dane
34 revalidatePath('/orders');
35 revalidatePath('/cart');
36
37 // 5. Przekieruj do potwierdzenia
38 redirect('/orders/' + order.id + '/confirmation');
39}Server Actions obsluguja rowniez przesylanie plikow:
1// actions/upload.ts
2'use server';
3
4import { writeFile } from 'fs/promises';
5import path from 'path';
6
7export async function uploadFile(formData: FormData) {
8 const file = formData.get('file') as File;
9
10 if (!file || file.size === 0) {
11 return { error: 'Nie wybrano pliku' };
12 }
13
14 // Validation typu i rozmiaru
15 const maxSize = 5 * 1024 * 1024; // 5MB
16 if (file.size > maxSize) {
17 return { error: 'Plik jest za duzy (max 5MB)' };
18 }
19
20 const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
21 if (!allowedTypes.includes(file.type)) {
22 return { error: 'Niedozwolony typ pliku' };
23 }
24
25 // Zapis pliku
26 const bytes = await file.arrayBuffer();
27 const buffer = Buffer.from(bytes);
28 const filename = Date.now() + '-' + file.name;
29 const filepath = path.join(process.cwd(), 'public/uploads', filename);
30
31 await writeFile(filepath, buffer);
32
33 return { success: true, url: '/uploads/' + filename };
34}1// components/FileUpload.tsx
2'use client';
3
4import { useActionState } from 'react';
5import { uploadFile } from '@/actions/upload';
6
7export default function FileUpload() {
8 const [state, formAction, andsPending] = useActionState(uploadFile, null);
9
10 return (
11 <form action={formAction}>
12 <input type="file" name="file" accept="image/*" />
13 <button type="submit" disabled={isPending}>
14 {isPending ? 'Przesylanie...' : 'Wyslij plik'}
15 </button>
16
17 {state?.error && <p className="text-red-500">{state.error}</p>}
18 {state?.success && (
19 <img src={state.url} alt="Przeslany plik" className="mt-4 max-w-xs" />
20 )}
21 </form>
22 );
23}Server Actions to potezny mechanizm Next.js, ktory upraszcza komunikacje klient-serwer:
W Metropolii Quantum 2150 Server Actions sa fundamentem kazdej niezawodnej aplikacji - pozwalaja budowac szybkie, bezpieczne i odporne na bledy systemy.