Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Server Actions - zaawansowane wzorce

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.

Optimistic Updates z useOptimistic

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.

Podstawowy wzorzec

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}

Server Action z optimistic toggle

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}

Rewalidacja po mutacji

Po kazdej operacji Server Action nalezy odswiezyc dane. Next.js oferuje dwa mechanizmy:

revalidatePath
i
revalidateTag
.

revalidatePath - odswiezanie sciezki

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}

revalidateTag - odswiezanie po tagu

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}

Obsluga bledow w Server Actions

Prawidlowa obsluga bledow to klucz do niezawodnej aplikacji. Server Actions powinny zwracac ustrukturyzowane odpowiedzi z informacja o sukcesie lub bledzie.

Wzorzec z obsluga bledow

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  // Walidacja danych
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}

Komponent formularza z useActionState

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, isPending] = 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 ? 'Logowanie...' : 'Zaloguj sie'}
36      </button>
37    </form>
38  );
39}

Progressive Enhancement

Server Actions dzialaja nawet bez JavaScript po stronie klienta. Formularz HTML domyslnie wysyla dane metoda POST, wiec caly flow dziala natywnie.

Formularz z progressive enhancement

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}

Kompozycja Server Actions

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}

Upload plikow z Server Actions

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  // Walidacja 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, isPending] = 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}

Podsumowanie

Server Actions to potezny mechanizm Next.js, ktory upraszcza komunikacje klient-serwer:

  1. useOptimistic - natychmiastowa aktualizacja UI przed odpowiedzia serwera
  2. revalidatePath / revalidateTag - precyzyjna rewalidacja danych po mutacji
  3. Obsluga bledow - ustrukturyzowane odpowiedzi z walidacja Zod
  4. Progressive enhancement - formularze dzialaja bez JavaScript
  5. Kompozycja - laczenie wielu operacji w jeden flow
  6. Upload plikow - natywna obsluga przesylania plikow

W Metropolii Quantum 2150 Server Actions sa fundamentem kazdej niezawodnej aplikacji - pozwalaja budowac szybkie, bezpieczne i odporne na bledy systemy.

Vai a CodeWorlds