Gerade in Geschäftsanwendungen brauchen wir Formulare, um Daten zu erstellen und zu ändern. In einem Customer Relationship Management (CRM)-System bearbeiten Benutzer beispielsweise Kundendaten wie Namen, Adressen und Kontaktinformationen.

Remix, als modernes Fullstack-Framework, kann sicherstellen, dass diese Daten in Echtzeit geladen und aktualisiert werden, ohne dass die Seite neu geladen wird. Durch „Actions“ und „Loaders“ werden nur die Daten aktualisiert, die wir brauchen, was die Benutzererfahrung verbessert und Systemressourcen spart.

Remix folgt da seiner Philosophie, baut auf Webstandards und nutzt HTML-Formulare als zentrales Element für die Kommunikation von Datenänderungen mit dem Backend, statt alles auf einer separaten REST-Kommunikation aufzubauen.

In diesem dritten Teil unserer Blogserie zu Remix wollen wir uns das in der Praxis ansehen. Dabei bleiben wir in der Musikbranche:

  1. Beispiele für Kundendaten aus dem Musikvertrieb bekommen wir uns in der Chinook-Datenbank.
  2. Dann erstellen wir ein Formular, um die Daten zu ändern.

Du bist neu in dieser Blogserie? Hier kannst du Teil 1 und Teil 2 nachlesen:

Teil 1: Remix – eine Alternative für Geschäftsanwendungen
Teil 2: Remix: Routes und Data Loading

So geht’s:

Zunächst erstellen wir diese Routen: (Mehr über Routen in Remix erfahren)

  • customers als Layout, das aktuell nur den Header bereitstellt
  • customers._index für die Übersicht der Kunden
  • customers.edit.$id für die Bearbeitung eines Kunden
  • customers.create für das Anlegen eines neuen Kunden
  • customers.delete für das Löschen eines Kunden

Action-Funktion

Die Route-Module haben einen weiteren Export: die action-Funktion.

Diese Funktion wird bei Non-GET-Anfragen (DELETEPOSTPUTPATCH) aufgerufen, bevor die loader-Funktion aufgerufen wird. Die Loader-Funktion wird aufgerufen, um die geänderten Daten zu laden und zu revalidieren. Damit wird ein Statusabgleich zwischen Server und Client hergestellt.

Remix weiß in der Regel, welche Loader-Funktionen optimaler Weise aufgerufen werden müssen und wann. Zusätzlich, falls benötigt, kann dies über die Funktion shouldRevalidate gesteuert werden.

Wie der loader ist action Teil der öffentlichen API. Das heißt, Daten, die wir über die Action-Funktion empfangen, müssen – wie alle Daten aus dem Web – validiert werden.

Und damit kommen wir zu einem weiteren wesentlichen Aspekt:

Benutzereingaben validieren

Daten, die eine Person zum Beispiel über Formulare eingibt, müssen validiert werden. Bei Webanwendungen sorgen wir für eine Validierung in der Regel sowohl auf der Client- als auch auf der Server-Seite. Vor allem die Validierung auf der Server-Seite ist ein Muss, da die Client-Seite manipuliert werden kann.

Wie aber schaffen wir es, dass wir die Validierungslogik nur einmal schreiben müssen und sie danach sowohl auf der Client- wie auch auf der Serverseite verwenden können? Warum?

Dies wäre ein großer Vorteil für die Wartbarkeit und die Sicherheit der Anwendung. Eine doppelte Implementierung erzeugt nicht nur mehr Aufwand bei der initialen Erstellung und auch bei der Wartung, sondern birgt auch die Gefahr, dass die Regeln unterschiedlich implementiert werden. Dies hätte zur FOlge, dass das Frontend Eingaben zulässt, die das Backend dann zurückweist. Der Frust bei denjenigen, die die Anwendung benutzen, wäre vorstellbar.

Viele Ansätze für die Validierung setzen in dem Fall auf die Verwendung von Schemata. Eine Bibliothek, die dies ermöglicht, ist zod.

Zod selbst ist für jede Art von TypeScript-Anwendung einsetzbar, weiß aber selbst zunächst noch nichts von HTML-Formularen. Hier kommen weitere Bibliotheken ins Spiel. Eine beliebte Bibliothek für die Formularvalidierung in React ist react-hook-form. Diese Bibliothek kann mit zod kombiniert werden, um die Validierung auf der Clientseite zu übernehmen.

Mit remix-hook-form gibt es eine für Remix abgestimmte Integration von react-hook-form. Dieser Adapter ist im Kern keine 300 Zeilen lang.

Andere Vertreter sind

Jede dieser Bibliotheken kann die Kombination mit Zod. Deshalb sind sie für die Validierung auf Client- und Serverseite verwendbar. Wir haben uns übrigens auch deshalb für Remix Hook Form entschieden, weil es auf React Hook Form aufsetzt, das wir bereits für andere Projekte verwenden.

„Unsere“ Action-Funktion

In customers._create.tsx haben wir die folgende Action-Funktion:

const resolver = zodResolver(customerCreateForm);

export const action = async ({ request }: ActionFunctionArgs) => {
        const {
                errors,
                data,
                receivedValues: defaultValues,
        } = await getValidatedFormData<CustomerCreateForm>(request, resolver);
        if (errors) {
                return json({ errors, defaultValues });
        }
        const insertedCustomers = await db
                .insert(customerInChinook)
                .values(data)
                .returning();
        if (insertedCustomers.length === 0) {
                return json({ errors: { root: { type: "insert_failed" } } });
        }
        return redirect(`/customers/edit/${insertedCustomers[0].customer_id}`);
};

Die Funktion verwendet die getValidatedFormData-Funktion von Remix Hook Form, um die Daten gegen das CustomerCreateForm-Schema zu validieren. Falls Fehler auftreten, werden diese zurückgegeben. Remix Hook Form kümmert sich um die Interpretion von Fehlern und defaultValues für das Formular. Die Magic passiert dabei im Hook useRemixForm, das einen angepassten submitHandler bereitstellt.

Die Nutzung in der Komponente ist dabei sehr einfach:

export default function CustomersCreate() {
        const {
                handleSubmit,
                formState: { errors },
                register,
        } = useRemixForm<CustomerCreateForm>({
                mode: "onSubmit",
                reValidateMode: "onBlur",
                resolver,
        });

        return (
                <>
            <Form onSubmit={handleSubmit} method={"post"}>
                        <TextInput
                            placeholder="First Name"
                            label={"First Name"}
                            {...register("first_name")}
                            error={errors.first_name?.message}
                        />
                    <Button type="submit">
                        Submit
                    </Button>
            </Form>
        </>
    );
}

Etwas mehr Aufwand ist notwendig, wenn es sich um Controlled-Komponenten handelt. Dies wird in der Dokumentation von React Hook Form beschrieben. Controlled-Komponenten führen schnell dazu, dass es für eine Select-Komponente (hier customer_.edit.$id.tsx) umfangreicher werden kann:

<Controller
    name="support_rep_id"
    control={control}
    render={({ field }) => {
        // Map salesReps to Select options
        const options: SelectOption[] = [
            {
                value: "",
                label: "None",
            },
            ...salesReps.map(
                (salesRep) =>
                    ({
                        value: salesRep.employee_id?.toString(),
                        label: salesRep.name,
                    }) as SelectOption,
            ),
        ];

        // Set the Select component's value to match the current field value
        const selectedValue = options.find(
            (option) => option.value === (field.value?.toString() || ""),
        );

        return (
            <Select
                label="Sales Agent"
                placeholder="Select sales agent"
                {...field}
                data={options}
                value={selectedValue?.value}
                onChange={(value) => {
                    field.onChange(
                        value ? Number.parseInt(value || "") : null,
                    );
                }}
            />
        );
    }}
/>

Zwei kleine Hinweise zu unserem Beispiel

1. Zugriffe und Routen trennen

Im Beispielcode mixen wir Routen-Definition und Datenbankzugriff, da der Zugriff auf die Datenbank hier sehr einfach ist und die Konzepte von Remix einfacher erklärt werden können. Im Sinne einer produktionsreifen Implementierung wäre es allerdings es sinnvoll, diese Zugriffe aus den Routen herauszunehmen und in eigene Module zu kapseln, um eine Strukturierung entsprechend der Hauptaufgaben (Seperation of Concerns) zu erreichen.

Das macht die Routen übersichtlicher und erleichtert das Testen und ermöglicht mehr Flexibilität.

2. Formularmodell braucht weitere Daten

Häufig kommen in action und loader noch kleine Logiken hinzu, etwa um weitere Daten zu laden und zu speichern. Dies kommt daher, dass häufig das Formularmodell nicht 1:1 mit dem Datenbankmodell übereinstimmt und noch weitere Daten benötigt werden.

Für unseren Code überlassen wir das Refactoring den Leser:innen.

Summary

Wir haben gesehen, dass die Erstellung von Formularen in Remix ähnlich komplex ist wie in React. Aber durch die Verwendung von zod und remix-hook-form können wir die Validierung auf der Client- und Serverseite vereinheitlichen. Wir haben also alle Zutaten, um CRUD-Funktionen in Remix zu implementieren.

Alle Teile dieser Blogserie:

Teil 1: Remix – eine Alternative für Geschäftsanwendungen
Teil 2: Remix – Routes und Data Loading
Teil 3: Remix – Formulare, Validierung und Datenänderungen

Alle Beiträge von Richard Attermeyer

Richard Attermeyer arbeitet als Chief Solution Architect bei OPITZ CONSULTING. Er ist seit vielen Jahren als Entwickler, Architekt und Coach für die Themen Modernisierung, Architektur und agile Projekte tätig und hilft Unternehmen, mit motivierten Teams erfolgreiche Projekte zu realisieren.

Schreibe einen Kommentar