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:
- Beispiele für Kundendaten aus dem Musikvertrieb bekommen wir uns in der Chinook-Datenbank.
- 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 bereitstelltcustomers._index
für die Übersicht der Kundencustomers.edit.$id
für die Bearbeitung eines Kundencustomers.create
für das Anlegen eines neuen Kundencustomers.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 (DELETE
, POST
, PUT
, PATCH
) 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