Datenmutierung

Im vorherigen Kapitel haben Sie Such- und Paginierungsfunktionen mit URL-Suchparametern und Next.js-APIs implementiert. Lassen Sie uns weiter an der Rechnungsseite arbeiten, indem wir die Möglichkeit hinzufügen, Rechnungen zu erstellen, zu aktualisieren und zu löschen!

Was sind Server Actions?

React Server Actions ermöglichen es Ihnen, asynchronen Code direkt auf dem Server auszuführen. Sie eliminieren die Notwendigkeit, API-Endpunkte zur Datenmutierung zu erstellen. Stattdessen schreiben Sie asynchrone Funktionen, die auf dem Server ausgeführt werden und von Ihren Client- oder Server-Komponenten aufgerufen werden können.

Sicherheit hat oberste Priorität für Webanwendungen, da sie anfällig für verschiedene Bedrohungen sein können. Hier kommen Server Actions ins Spiel. Sie beinhalten Funktionen wie verschlüsselte Closures, strikte Eingabeüberprüfungen, Hashung von Fehlermeldungen, Host-Einschränkungen und mehr – all das trägt dazu bei, die Sicherheit Ihrer Anwendung erheblich zu verbessern.

Verwendung von Formularen mit Server Actions

In React können Sie das action-Attribut im <form>-Element verwenden, um Aktionen aufzurufen. Die Aktion erhält automatisch das native FormData-Objekt, das die erfassten Daten enthält.

Beispiel:

// Server-Komponente
export default function Page() {
  // Aktion
  async function create(formData: FormData) {
    'use server';
 
    // Logik zur Datenmutierung...
  }
 
  // Aktion mit dem "action"-Attribut aufrufen
  return <form action={create}>...</form>;
}

Ein Vorteil des Aufrufs einer Server Action innerhalb einer Server-Komponente ist das progressive Enhancement – Formulare funktionieren auch dann, wenn JavaScript auf dem Client noch nicht geladen wurde, z.B. bei langsamen Internetverbindungen.

Next.js mit Server Actions

Server Actions sind auch tief in das Next.js-Caching integriert. Wenn ein Formular über eine Server Action abgeschickt wird, können Sie nicht nur die Aktion zur Datenmutierung verwenden, sondern auch den zugehörigen Cache mit APIs wie revalidatePath und revalidateTag neu validieren.

Lassen Sie uns sehen, wie das alles zusammen funktioniert!

Erstellen einer Rechnung

Hier sind die Schritte, die Sie unternehmen werden, um eine neue Rechnung zu erstellen:

  1. Erstellen Sie ein Formular, um die Benutzereingaben zu erfassen.
  2. Erstellen Sie eine Server Action und rufen Sie sie aus dem Formular auf.
  3. Extrahieren Sie in Ihrer Server Action die Daten aus dem formData-Objekt.
  4. Validieren und bereiten Sie die Daten für die Einfügung in Ihre Datenbank vor.
  5. Fügen Sie die Daten ein und behandeln Sie etwaige Fehler.
  6. Validieren Sie den Cache neu und leiten Sie den Benutzer zurück zur Rechnungsseite.

1. Erstellen Sie eine neue Route und ein Formular

Erstellen Sie zunächst innerhalb des Ordners /invoices ein neues Routensegment namens /create mit einer page.tsx-Datei:

Invoices-Ordner mit einem verschachtelten create-Ordner und einer page.tsx-Datei darin

Sie werden diese Route verwenden, um neue Rechnungen zu erstellen. Fügen Sie den folgenden Code in Ihre page.tsx-Datei ein und studieren Sie ihn:

/dashboard/invoices/create/page.tsx
import Form from '@/app/ui/invoices/create-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
 
export default async function Page() {
  const customers = await fetchCustomers();
 
  return (
    <main>
      <Breadcrumbs
        breadcrumbs={[
          { label: 'Rechnungen', href: '/dashboard/invoices' },
          {
            label: 'Rechnung erstellen',
            href: '/dashboard/invoices/create',
            active: true,
          },
        ]}
      />
      <Form customers={customers} />
    </main>
  );
}

Ihre Seite ist eine Server-Komponente, die customers abruft und an die <Form>-Komponente übergibt. Um Zeit zu sparen, haben wir die <Form>-Komponente bereits für Sie erstellt.

Navigieren Sie zur <Form>-Komponente, und Sie werden sehen, dass das Formular:

  • Ein <select>-Element (Dropdown) mit einer Liste von Kunden enthält.
  • Ein <input>-Element für den Betrag mit type="number" enthält.
  • Zwei <input>-Elemente für den Status mit type="radio" enthält.
  • Einen Button mit type="submit" enthält.

Unter http://localhost:3000/dashboard/invoices/create sollten Sie folgende Benutzeroberfläche sehen:

Seite zum Erstellen von Rechnungen mit Breadcrumbs und Formular

2. Erstellen Sie eine Server Action

Gut, jetzt erstellen wir eine Server Action, die aufgerufen wird, wenn das Formular abgeschickt wird.

Navigieren Sie zu Ihrem lib/-Verzeichnis und erstellen Sie eine neue Datei namens actions.ts. Fügen Sie am Anfang dieser Datei die React-use server-Direktive hinzu:

/app/lib/actions.ts
'use server';

Durch das Hinzufügen von 'use server' markieren Sie alle exportierten Funktionen innerhalb der Datei als Server Actions. Diese Server-Funktionen können dann in Client- und Server-Komponenten importiert und verwendet werden. Alle Funktionen in dieser Datei, die nicht verwendet werden, werden automatisch aus dem endgültigen Anwendungsbundle entfernt.

Sie können Server Actions auch direkt in Server-Komponenten schreiben, indem Sie "use server" innerhalb der Aktion hinzufügen. Für diesen Kurs werden wir sie jedoch in einer separaten Datei organisieren. Wir empfehlen, eine separate Datei für Ihre Aktionen zu verwenden.

Erstellen Sie in Ihrer actions.ts-Datei eine neue asynchrone Funktion, die formData akzeptiert:

/app/lib/actions.ts
'use server';
 
export async function createInvoice(formData: FormData) {}

Importieren Sie dann in Ihrer <Form>-Komponente createInvoice aus Ihrer actions.ts-Datei. Fügen Sie dem <form>-Element ein action-Attribut hinzu und rufen Sie die createInvoice-Aktion auf.

/app/ui/invoices/create-form.tsx
import { CustomerField } from '@/app/lib/definitions';
import Link from 'next/link';
import {
  CheckIcon,
  ClockIcon,
  CurrencyDollarIcon,
  UserCircleIcon,
} from '@heroicons/react/24/outline';
import { Button } from '@/app/ui/button';
import { createInvoice } from '@/app/lib/actions';
 
export default function Form({
  customers,
}: {
  customers: CustomerField[];
}) {
  return (
    <form action={createInvoice}>
      // ...
  )
}

Gut zu wissen: In HTML würden Sie eine URL an das action-Attribut übergeben. Diese URL wäre das Ziel, an das Ihre Formulardaten gesendet werden sollten (normalerweise ein API-Endpunkt).

In React wird das action-Attribut jedoch als spezielles Prop betrachtet – React baut darauf auf, um das Aufrufen von Aktionen zu ermöglichen.

Hinter den Kulissen erstellen Server Actions einen POST-API-Endpunkt. Deshalb müssen Sie keine API-Endpunkte manuell erstellen, wenn Sie Server Actions verwenden.

3. Extrahieren Sie die Daten aus formData

Zurück in Ihrer actions.ts-Datei müssen Sie die Werte von formData extrahieren. Dafür gibt es einige Methoden. Für dieses Beispiel verwenden wir die .get(name)-Methode.

/app/lib/actions.ts
'use server';
 
export async function createInvoice(formData: FormData) {
  const rawFormData = {
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  };
  // Testen Sie es:
  console.log(rawFormData);
}

Tipp: Wenn Sie mit Formularen arbeiten, die viele Felder haben, sollten Sie die entries()-Methode mit JavaScripts Object.fromEntries() in Betracht ziehen.

Um zu überprüfen, ob alles korrekt verbunden ist, testen Sie das Formular. Nach dem Absenden sollten Sie die Daten, die Sie gerade eingegeben haben, in Ihrem Terminal (nicht im Browser) protokolliert sehen.

Jetzt, da Ihre Daten in Form eines Objekts vorliegen, wird es viel einfacher sein, damit zu arbeiten.

4. Validieren und bereiten Sie die Daten vor

Bevor Sie die Formulardaten an Ihre Datenbank senden, sollten Sie sicherstellen, dass sie im richtigen Format und mit den richtigen Typen vorliegen. Wenn Sie sich an den Anfang des Kurses erinnern, erwartet Ihre Rechnungstabelle Daten im folgenden Format:

/app/lib/definitions.ts
export type Invoice = {
  id: string; // Wird in der Datenbank erstellt
  customer_id: string;
  amount: number; // In Cent gespeichert
  status: 'pending' | 'paid';
  date: string;
};

Bisher haben Sie nur die customer_id, den amount und den status aus dem Formular.

Typvalidierung und -umwandlung

Es ist wichtig zu validieren, dass die Daten aus Ihrem Formular mit den erwarteten Typen in Ihrer Datenbank übereinstimmen. Wenn Sie beispielsweise ein console.log in Ihre Aktion einfügen:

console.log(typeof rawFormData.amount);

Werden Sie feststellen, dass amount vom Typ string und nicht number ist. Das liegt daran, dass input-Elemente mit type="number" tatsächlich einen String und keine Zahl zurückgeben!

Für die Typvalidierung haben Sie mehrere Möglichkeiten. Während Sie Typen manuell validieren können, kann eine Typvalidierungsbibliothek Ihnen Zeit und Mühe sparen. Für Ihr Beispiel verwenden wir Zod, eine TypeScript-first-Validierungsbibliothek, die diese Aufgabe vereinfachen kann.

Importieren Sie Zod in Ihrer actions.ts-Datei und definieren Sie ein Schema, das der Struktur Ihres Formularobjekts entspricht. Dieses Schema validiert die formData, bevor sie in einer Datenbank gespeichert werden.

/app/lib/actions.ts
'use server';
 
import { z } from 'zod';
 
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
  status: z.enum(['pending', 'paid']),
  date: z.string(),
});
 
const CreateInvoice = FormSchema.omit({ id: true, date: true });
 
export async function createInvoice(formData: FormData) {
  // ...
}

Das amount-Feld ist speziell darauf ausgelegt, von einem String in eine Zahl umgewandelt zu werden, während gleichzeitig der Typ validiert wird.

Sie können dann Ihre rawFormData an CreateInvoice übergeben, um die Typen zu validieren:

/app/lib/actions.ts
// ...
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
}

Speichern von Werten in Cent

Es ist in der Regel eine gute Praxis, Geldbeträge in Cent in Ihrer Datenbank zu speichern, um JavaScript-Gleitkommafehler zu vermeiden und eine höhere Genauigkeit zu gewährleisten.

Lassen Sie uns den Betrag in Cent umrechnen:

/app/lib/actions.ts
// ...
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
}

Erstellen neuer Datumsangaben

Erstellen wir schließlich ein neues Datum mit dem Format "YYYY-MM-DD" für das Erstellungsdatum der Rechnung:

/app/lib/actions.ts
// ...
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0];
}

5. Einfügen der Daten in Ihre Datenbank

Jetzt, da Sie alle Werte für Ihre Datenbank haben, können Sie eine SQL-Abfrage erstellen, um die neue Rechnung in Ihre Datenbank einzufügen und die Variablen zu übergeben:

/app/lib/actions.ts
import { z } from 'zod';
import postgres from 'postgres';
 
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 
// ...
 
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0];
 
  await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;
}

Im Moment behandeln wir keine Fehler. Darüber werden wir im nächsten Kapitel sprechen. Lassen Sie uns zunächst mit dem nächsten Schritt fortfahren.

6. Revalidierung und Weiterleitung

Next.js verfügt über einen Client-seitigen Router-Cache, der die Routensegmente für eine gewisse Zeit im Browser des Nutzers speichert. Zusammen mit Prefetching stellt dieser Cache sicher, dass Nutzer schnell zwischen Routen navigieren können, während gleichzeitig die Anzahl der Anfragen an den Server reduziert wird.

Da Sie die in der Rechnungsroute angezeigten Daten aktualisieren, möchten Sie diesen Cache leeren und eine neue Anfrage an den Server auslösen. Dies können Sie mit der revalidatePath-Funktion von Next.js erreichen:

/app/lib/actions.ts
'use server';
 
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import postgres from 'postgres';
 
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 
// ...
 
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0];
 
  await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;
 
  revalidatePath('/dashboard/invoices');
}

Sobald die Datenbank aktualisiert wurde, wird der Pfad /dashboard/invoices revalidiert und frische Daten werden vom Server abgerufen.

An diesem Punkt möchten Sie den Nutzer auch zurück zur /dashboard/invoices-Seite weiterleiten. Dies können Sie mit der redirect-Funktion von Next.js tun:

/app/lib/actions.ts
'use server';
 
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import postgres from 'postgres';
 
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 
// ...
 
export async function createInvoice(formData: FormData) {
  // ...
 
  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}

Glückwunsch! Sie haben gerade Ihre erste Server-Aktion implementiert. Testen Sie sie, indem Sie eine neue Rechnung hinzufügen. Wenn alles korrekt funktioniert:

  1. Sie sollten nach dem Absenden zur /dashboard/invoices-Route weitergeleitet werden.
  2. Sie sollten die neue Rechnung oben in der Tabelle sehen.

Rechnung aktualisieren

Das Formular zur Rechnungsaktualisierung ähnelt dem Formular zur Rechnungserstellung, mit dem Unterschied, dass Sie die Rechnungs-id übergeben müssen, um den Datensatz in Ihrer Datenbank zu aktualisieren. Sehen wir uns an, wie Sie die Rechnungs-id erhalten und übergeben können.

Diese Schritte führen Sie durch, um eine Rechnung zu aktualisieren:

  1. Erstellen Sie ein neues dynamisches Routensegment mit der Rechnungs-id.
  2. Lesen Sie die Rechnungs-id aus den Seiten-Parametern.
  3. Holen Sie die spezifische Rechnung aus Ihrer Datenbank.
  4. Füllen Sie das Formular mit den Rechnungsdaten vor.
  5. Aktualisieren Sie die Rechnungsdaten in Ihrer Datenbank.

1. Dynamisches Routensegment mit der Rechnungs-id erstellen

Next.js ermöglicht es Ihnen, Dynamische Routensegmente zu erstellen, wenn Sie den genauen Segmentnamen nicht kennen und Routen basierend auf Daten erstellen möchten. Dies könnte Blogpost-Titel, Produktseiten usw. sein. Sie können dynamische Routensegmente erstellen, indem Sie einen Ordnernamen in eckige Klammern setzen. Zum Beispiel [id], [post] oder [slug].

Erstellen Sie in Ihrem /invoices-Ordner ein neues dynamisches Route namens [id], dann eine neue Route namens edit mit einer page.tsx-Datei. Ihre Dateistruktur sollte so aussehen:

Invoices-Ordner mit einem verschachtelten [id]-Ordner und einem edit-Ordner darin

In Ihrer <Table>-Komponente fällt auf, dass es einen <UpdateInvoice />-Button gibt, der die id der Rechnung aus den Tabellendatensätzen erhält.

/app/ui/invoices/table.tsx
export default async function InvoicesTable({
  query,
  currentPage,
}: {
  query: string;
  currentPage: number;
}) {
  return (
    // ...
    <td className="flex justify-end gap-2 whitespace-nowrap px-6 py-4 text-sm">
      <UpdateInvoice id={invoice.id} />
      <DeleteInvoice id={invoice.id} />
    </td>
    // ...
  );
}

Navigieren Sie zu Ihrer <UpdateInvoice />-Komponente und aktualisieren Sie den href des Link, um die id-Prop zu akzeptieren. Sie können Template-Literale verwenden, um auf ein dynamisches Routensegment zu verlinken:

/app/ui/invoices/buttons.tsx
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
 
// ...
 
export function UpdateInvoice({ id }: { id: string }) {
  return (
    <Link
      href={`/dashboard/invoices/${id}/edit`}
      className="rounded-md border p-2 hover:bg-gray-100"
    >
      <PencilIcon className="w-5" />
    </Link>
  );
}

2. Rechnungs-id aus den Seiten-params lesen

Fügen Sie in Ihrer <Page>-Komponente den folgenden Code ein:

/app/dashboard/invoices/[id]/edit/page.tsx
import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
 
export default async function Page() {
  return (
    <main>
      <Breadcrumbs
        breadcrumbs={[
          { label: 'Rechnungen', href: '/dashboard/invoices' },
          {
            label: 'Rechnung bearbeiten',
            href: `/dashboard/invoices/${id}/edit`,
            active: true,
          },
        ]}
      />
      <Form invoice={invoice} customers={customers} />
    </main>
  );
}

Beachten Sie, dass es Ihrer /create-Rechnungsseite ähnelt, außer dass es ein anderes Formular importiert (aus der edit-form.tsx-Datei). Dieses Formular sollte mit einem defaultValue für den Kundennamen, den Rechnungsbetrag und den Status vorausgefüllt sein. Um die Formularfelder vorauszufüllen, müssen Sie die spezifische Rechnung mit id abrufen.

Zusätzlich zu searchParams akzeptieren Page-Komponenten auch eine Prop namens params, die Sie verwenden können, um auf die id zuzugreifen. Aktualisieren Sie Ihre <Page>-Komponente, um die Prop zu empfangen:

/app/dashboard/invoices/[id]/edit/page.tsx
import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
 
export default async function Page(props: { params: Promise<{ id: string }> }) {
  const params = await props.params;
  const id = params.id;
  // ...
}

3. Spezifische Rechnung abrufen

Dann:

  • Importieren Sie eine neue Funktion namens fetchInvoiceById und übergeben Sie die id als Argument.
  • Importieren Sie fetchCustomers, um die Kundennamen für das Dropdown-Menü abzurufen.

Sie können Promise.all verwenden, um sowohl die Rechnung als auch die Kunden parallel abzurufen:

/dashboard/invoices/[id]/edit/page.tsx
import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';
 
export default async function Page(props: { params: Promise<{ id: string }> }) {
  const params = await props.params;
  const id = params.id;
  const [invoice, customers] = await Promise.all([
    fetchInvoiceById(id),
    fetchCustomers(),
  ]);
  // ...
}

Sie werden einen temporären TypeScript-Fehler für die invoice-Prop in Ihrem Terminal sehen, weil invoice potenziell undefined sein könnte. Machen Sie sich darüber vorerst keine Sorgen, Sie werden dies im nächsten Kapitel beheben, wenn Sie die Fehlerbehandlung hinzufügen.

Großartig! Testen Sie nun, ob alles korrekt verbunden ist. Besuchen Sie http://localhost:3000/dashboard/invoices und klicken Sie auf das Stiftsymbol, um eine Rechnung zu bearbeiten. Nach der Navigation sollten Sie ein Formular sehen, das mit den Rechnungsdetails vorausgefüllt ist:

Rechnungsbearbeitungsseite mit Breadcrumbs und Formular

Die URL sollte auch mit einer id aktualisiert werden, wie folgt: http://localhost:3000/dashboard/invoice/uuid/edit

UUIDs vs. Auto-incrementing Keys

Wir verwenden UUIDs anstelle von inkrementierenden Schlüsseln (z.B. 1, 2, 3 usw.). Dadurch wird die URL länger; jedoch eliminieren UUIDs das Risiko von ID-Kollisionen, sind global eindeutig und reduzieren das Risiko von Enumeration-Angriffen – was sie ideal für große Datenbanken macht.

Wenn Sie jedoch sauberere URLs bevorzugen, könnten Sie auto-incrementing Keys bevorzugen.

4. id an die Server-Aktion übergeben

Schließlich möchten Sie die id an die Server-Aktion übergeben, damit Sie den richtigen Datensatz in Ihrer Datenbank aktualisieren können. Sie können die id nicht wie folgt als Argument übergeben:

/app/ui/invoices/edit-form.tsx
// Die Übergabe einer id als Argument funktioniert nicht
<form action={updateInvoice(id)}>

Stattdessen können Sie die id mit JS bind an die Server-Aktion übergeben. Dies stellt sicher, dass alle an die Server-Aktion übergebenen Werte kodiert sind.

/app/ui/invoices/edit-form.tsx
// ...
import { updateInvoice } from '@/app/lib/actions';
 
export default function EditInvoiceForm({
  invoice,
  customers,
}: {
  invoice: InvoiceForm;
  customers: CustomerField[];
}) {
  const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
 
  return <form action={updateInvoiceWithId}>{/* ... */}</form>;
}

Hinweis: Die Verwendung eines versteckten Eingabefelds in Ihrem Formular funktioniert ebenfalls (z.B. <input type="hidden" name="id" value={invoice.id} />). Allerdings werden die Werte im HTML-Quelltext als Klartext erscheinen, was für sensible Daten nicht ideal ist.

Erstellen Sie dann in Ihrer actions.ts-Datei eine neue Aktion namens updateInvoice:

/app/lib/actions.ts
// Verwenden Sie Zod, um die erwarteten Typen zu aktualisieren
const UpdateInvoice = FormSchema.omit({ id: true, date: true });
 
// ...
 
export async function updateInvoice(id: string, formData: FormData) {
  const { customerId, amount, status } = UpdateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
 
  const amountInCents = amount * 100;
 
  await sql`
    UPDATE invoices
    SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
    WHERE id = ${id}
  `;
 
  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}

Ähnlich wie bei der createInvoice-Aktion:

  1. Extrahieren Sie die Daten aus formData.
  2. Validieren Sie die Typen mit Zod.
  3. Konvertieren Sie den Betrag in Cent.
  4. Übergeben Sie die Variablen an Ihre SQL-Abfrage.
  5. Rufen Sie revalidatePath auf, um den Client-Cache zu leeren und eine neue Server-Anfrage auszulösen.
  6. Rufen Sie redirect auf, um den Nutzer zur Rechnungsseite weiterzuleiten.

Testen Sie es, indem Sie eine Rechnung bearbeiten. Nach dem Absenden des Formulars sollten Sie zur Rechnungsseite weitergeleitet werden, und die Rechnung sollte aktualisiert sein.

Rechnung löschen

Um eine Rechnung mit einer Server-Aktion zu löschen, umschließen Sie den Lösch-Button mit einem <form>-Element und übergeben Sie die id mit bind an die Server-Aktion:

/app/ui/invoices/buttons.tsx
import { deleteInvoice } from '@/app/lib/actions';
 
// ...
 
export function DeleteInvoice({ id }: { id: string }) {
  const deleteInvoiceWithId = deleteInvoice.bind(null, id);
 
  return (
    <form action={deleteInvoiceWithId}>
      <button type="submit" className="rounded-md border p-2 hover:bg-gray-100">
        <span className="sr-only">Löschen</span>
        <TrashIcon className="w-4" />
      </button>
    </form>
  );
}

Erstellen Sie in Ihrer actions.ts-Datei eine neue Aktion namens deleteInvoice.

/app/lib/actions.ts
export async function deleteInvoice(id: string) {
  await sql`DELETE FROM invoices WHERE id = ${id}`;
  revalidatePath('/dashboard/invoices');
}

Da diese Aktion im /dashboard/invoices-Pfad aufgerufen wird, müssen Sie redirect nicht aufrufen. Der Aufruf von revalidatePath löst eine neue Server-Anfrage aus und rendert die Tabelle neu.

Weiterführende Lektüre

In diesem Kapitel haben Sie gelernt, wie Sie Server-Aktionen verwenden, um Daten zu mutieren. Sie haben auch gelernt, wie Sie die revalidatePath-API verwenden, um den Next.js-Cache zu revalidieren, und redirect, um den Nutzer zu einer neuen Seite weiterzuleiten.

Sie können auch mehr über Sicherheit mit Server-Aktionen lesen, um weiter zu lernen.