Formulare und useState()
Der Umgang mit Formularen in React erfordert ein präzises Verständnis des Komponentenstatus. Dabei spielt der useState-Hook eine zentrale Rolle, um Eingabewerte zu erfassen, zu aktualisieren und den Zustand kontrolliert zu verwalten. Besonders bei komplexeren Formularen ist ein sauber strukturiertes State-Management entscheidend für eine nachvollziehbare und wartbare Anwendung.
Inhaltsverzeichnis
Einfaches Formular
Im ersten Beispiel möchte ich ein einfaches Formular aufbauen und die Verwendung von useState()
nochmals aufzeigen. Wir arbeiten mit useState()
, um die Werte aus dem Formular zu überwachen und beim Absenden abrufen zu können.
import { useState } from 'react';
const SimpleForm = () => {
const [email, setEmail] = useState('');
const [formSubmitted, setFormSubmitted] = useState(false);
const handleUpdateEmail = (event) => {
setEmail(currentState => event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
setFormSubmitted(currentState => true);
console.log('Input value:', email);
};
return (
<>
<form onSubmit={handleSubmit}>
<label htmlFor="email_field">E-Mail</label>
<input
type="email"
id="email_field"
name="email_field"
value={email}
onChange={handleUpdateEmail}
/>
<button type="submit">
Absenden
</button>
</form>
{email.length > 0 && email !== '' && (
<p>Aktuelle Eingabe: {email}</p>
)}
{formSubmitted && (
<p>Das Formular wurde abgesendet.</p>
)}
</>
);
};
export default SimpleForm;
Beschreibung des Components
In diesem Component werden zwei Zustandswerte verwendet.
const [email, setEmail] = useState('');
Dieser Zustandswert dient der tatsächlichen Überwachung des Formular-Feldes für die E-Mail.
const [formSubmitted, setFormSubmitted] = useState(false);
Dieser Zustandswert ist eher unterstützend hier eingesetzt, um das Absenden des Formular zu simulieren und ein Element einzublenden, welcher in der UI mitteilt, dass das Formular abgesendet wurde.
Außerdem haben wir hier eine Art Two-Way-Binding am Eingabefeld mit value={email}
definiert. Im JSX-Template verwenden wir hierfür <p>Aktuelle Eingabe: {email}</p>
, um bei jeder Eingabe einen aktualisierten Wert zu erhalten.
Im Absende-Moment setzen wir unseren unterstützenden Zustandswert auf true
, um den Paragraphen mit der Mitteilung anzuzeigen. Ebenfalls haben wir Zugriff auf den aktuellen Wert des E-Mail Zustandswertes. Diesen können wir tatsächlich verwenden, um diese E-Mail über eine API an einen Server zu senden.
Kontrollierte Komponente
In diesem Beispiel ist das Eingabefeld eine kontrollierte Komponente. Das bedeutet, dass der Wert des Feldes vollständig durch den React State gesteuert wird. Jede Änderung wird durch den onChange
Handler erfasst und aktualisiert den State.
Formular mit mehreren Feldern
Im zweiten Beispiel erhöhen wir die Anzahl der Felder. An dieser Stelle macht es definitiv Sinn auf einen objektbasierten State zu wechseln, um den Formularstatus elegant zu verwalten.
Die Hilfsfunktion und den Hilfszustandswert für die Simulation des Absenden von Formular belassen wir weiterhin. So können wir nach dem Klick auf “Absenden” irgendeine Aktion ausführen. In diesem Fall einfach die Werte aus dem Formular anzeigen, die übermittelt werden würden.
Hinweis
Ich verwende bei meinen Projekten immer die Namenskonvention field{Fieldname}
oder field_{fieldname}
. Sowohl in Angular, als auch in React, als auch in sonstigen Technologien, die mit HTML-Formularen arbeiten. Hier versuche ich mich soweit es geht an das Prinzip “Explizit ist besser als implizit” zu halten. Mit dem Zusatz field
(oder field_
) ist mir in jedem Kontext, bei jedem Framework und an jeder Stelle immer gleich klar, dass es sich hierbei um ein Formularfeld handelt. (Bspw. könnte email
auch aus einem anderen Kontext kommen wie eigene State-Verwaltung oder einem anderen, entpackten Objekt, etc.)
Jeder wählt hier für ihn passenden Weg.
import { useState } from 'react';
import './MultiFieldForm.scss';
const MultiFieldForm = () => {
const [formData, setFormData] = useState({
fieldFirstname: '',
fieldLastname: '',
fieldEmail: '',
fieldAge: ''
});
const [formSubmitted, setFormSubmitted] = useState(false);
const handleInputChange = (event) => {
const { name, value } = event.target;
setFormData({
...formData,
[name]: value
});
};
const handleSubmit = (event) => {
event.preventDefault();
setFormSubmitted(currentState => true);
};
return (
<>
<form>
<div className="form_field">
<label htmlFor="fieldFirstname">Vorname</label>
<input
type="text"
id="fieldFirstname"
name="fieldFirstname"
value={formData.fieldFirstname}
onChange={handleInputChange}
/>
</div>
<div className="form_field">
<label htmlFor="fieldLastname">Nachname</label>
<input
type="text"
id="fieldLastname"
name="fieldLastname"
value={formData.fieldLastname}
onChange={handleInputChange}
/>
</div>
<div className="form_field">
<label htmlFor="fieldEmail">E-Mail</label>
<input
type="email"
id="fieldEmail"
name="fieldEmail"
value={formData.fieldEmail}
onChange={handleInputChange}
/>
</div>
<div className="form_field">
<label htmlFor="fieldAge">Alter</label>
<input
type="number"
id="fieldAge"
name="fieldAge"
value={formData.fieldAge}
onChange={handleInputChange}
/>
</div>
<div className="form_actions">
<button onClick={handleSubmit}>Absenden</button>
</div>
</form>
{formSubmitted && (
<div className="submit_result">
<p><strong>Vorname:</strong> {formData.fieldFirstname}</p>
<p><strong>Nachname:</strong> {formData.fieldLastname}</p>
<p><strong>E-Mail:</strong> {formData.fieldEmail}</p>
<p><strong>Alter:</strong> {formData.fieldAge}</p>
</div>
)}
</>
);
};
export default MultiFieldForm;
In diesem Beispiel wird ein State-Objekt eingesetzt. Die Aktualisierung der Felder erfolgt in diesem Beispiel anhand der Feldnamen. Daher können wir die Funktion für die Aktualisierung der Werte onChange
generisch halten.
const handleInputChange = (event) => {
const { name, value } = event.target;
setFormData({
...formData,
[name]: value
});
};
Im [name]
steht unser Feldname, beispielsweise fieldLastname
. Im value
- entsprechend der Wert. All unsere Formulardaten sind stets im Objekt formData
gebündelt und können weiter verwendet werden.
JavaScript Exkurs - Objekt Modifikation
Ich möchte noch etwas genauer auf das verwendete Konstrukt ...formData
und [name]: value
eingehen und kurz erklären, was hier das Verhalten ist und wie man das rekonstruieren kann.
Die ...
Punkte werden verwendet, um das Objekt zu kopieren. Im Grunde erzeugen wir ein neues Objekt auf Basis bereits vorhandenem Objekt.
const person = {
name: 'John',
job: 'Developer',
salary: 50000
};
// Kopie erstellen
const personTwo = { ...person };
Hier haben wir einfach ein Objekt person
erzeugt und davon eine Kopie erstellt und der Variable personTwo
zugewiesen.
Dies sind zwei unterschiedliche Objekte, was wir durch einen Vergleich einfach beweisen können.
console.log(person === personTwo);
false
Wir können es aber dadurch zusätzlich beweisen, indem wir das zweite Objekt ändern.
// Eigenschaft bei personTwo ändern
personTwo.job = 'Mobile Developer';
// Beide Objekte ausgeben
console.log(person);
console.log(personTwo);
{ name: 'John', job: 'Developer', salary: 50000 }
{ name: 'John', job: 'Mobile Developer', salary: 50000 }
Nun werden wir das Kopieren des Objekts und die Modifikation einer Eigenschaft am Objekt kombinieren. Hierzu verwenden wir personThree
als Variable.
const person = {
name: 'John',
job: 'Developer',
salary: 50000
};
// Kopie erstellen & Eigenschaft ändern
const personThree = { ...person, job: 'Manager' };
Hier haben wir nun eine Kopie von person
erstellt, das neue Objekt in der Variable personThree
gespeichert und die Eigenschaft job
am Objekt personThree
modifiert. In diesem Fall haben wir es in einem Schritt getan. So, wie wir es auch in unserem Formular-Beispiel in React verwendet haben.
Nun schauen wir uns [name]
Schreibweise bzw. Verwendung an.
Info
Die Notation [name]: value
nutzt eine besondere JavaScript-Funktion. Wenn man einen Variablennamen in []
eckige Klammern setzt, wird der Wert dieser Variable als Eigenschaftsname verwendet.
Schauen wir uns das Ganze an einem Beispiel an, damit es klarer wird. Für diesen Zweck führen wir eine neue Variable personFour
ein. Zusätzlich werden wir den Namen der Eigenschaft, welche wir am neuen Objekt ändern möchten, in einer Variable speichern. Diese Variable verwenden wir mit der oben beschrieben Notation.
const person = {
name: 'John',
job: 'Developer',
salary: 50000
};
// Variable definieren
const fieldName = 'job';
// Kopie erstellen & Eigenschaft ändern
const personFour = { ...person, [fieldName]: 'Tester' };
Wir haben hier nicht den Eigenschaftsnamen direkt angegeben, sondern eine Variable in eckige Klammern gesetzt. Wie oben in der Regel für die Notation beschrieben, wird der Wert dieser Variablen zum Namen der Eigenschaft am neuen Objekt.
Im Grunde können wir das Ganze auch so schreiben, auch wenn es nicht viel Sinn macht, aber technisch das Gleiche bedeutet.
const person = {
name: 'John',
job: 'Developer',
salary: 50000
};
const personFive = { ...person, ['job']: 'Game Developer' };
console.log(person);
console.log(personFive);
{ name: 'John', job: 'Developer', salary: 50000 }
{ name: 'John', job: 'Game Developer', salary: 50000 }
Auch könnte man eine schlaue Funktion definieren, die uns den Feldnamen liefert. Hier, zur Demonstrationszwecken, ist die Funktion sehr einfach und nutzlos.
const person = {
name: 'John',
job: 'Developer',
salary: 50000
};
// Funktion, die den oder die
// Eigenschaftsnamen generiert/zurückgibt
function getFieldName() {
return 'job';
}
const personSix = { ...person, [getFieldName()]: 'Security Officer' };
console.log(person);
console.log(personSix);
{ name: 'John', job: 'Developer', salary: 50000 }
{ name: 'John', job: 'Security Officer', salary: 50000 }
Formular mit Validierung
Im dritten Beispiel schauen wir uns an, wie man Validierung in Verbindung mit useState()
aufbauen könnte.
import { useState } from 'react';
import './FormValidation.scss';
const FormValidation = () => {
// State für Formulardaten
const [formData, setFormData] = useState({
fieldUsername: '',
fieldEmail: '',
fieldPassword: '',
fieldPasswordConfirm: ''
});
// State für Validierungsfehler
const [formErrors, setFormErrors] = useState({
username: '',
email: '',
password: '',
passwordConfirm: ''
});
// State für das Absenden des Formulars
const [formSubmitted, setFormSubmitted] = useState(false);
const validateEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const handleInputChange = (event) => {
const { name, value } = event.target;
setFormData({ ...formData, [name]: value });
validateField(name, value);
};
const validateField = (name, value) => {
let errorMessage = '';
switch (name) {
case 'fieldUsername':
if (value.trim() === '') {
errorMessage = 'Benutzername ist erforderlich';
} else if (value.length < 3) {
errorMessage = 'Benutzername muss mindestens 3 Zeichen lang sein';
}
break;
case 'fieldEmail':
if (value.trim() === '') {
errorMessage = 'E-Mail ist erforderlich';
} else if (!validateEmail(value)) {
errorMessage = 'E-Mail ist ungültig';
}
break;
case 'fieldPassword':
if (value.trim() === '') {
errorMessage = 'Passwort ist erforderlich';
} else if (value.length < 6) {
errorMessage = 'Password muss mindestens 6 Zeichen lang sein';
}
if (formData.fieldPasswordConfirm && value !== formData.fieldPasswordConfirm) {
setFormErrors(prevErrors => ({
...prevErrors,
fieldPasswordConfirm: ''
}));
} else if (formData.fieldPasswordConfirm) {
setFormErrors(prevErrors => ({
...prevErrors,
fieldPasswordConfirm: ''
}));
}
break;
case 'fieldPasswordConfirm':
if (value.trim() === '') {
errorMessage = 'Passwortbestätigung ist erforderlich';
} else if (value !== formData.fieldPassword) {
errorMessage = 'Passwörter müssen übereinstimmen';
}
break;
default:
break;
}
setFormErrors(prevErrors => ({
...prevErrors,
[name]: errorMessage
}));
return errorMessage === '';
};
const validateForm = () => {
let isValid = true;
Object.keys(formData).forEach(fieldName => {
const fieldValue = formData[fieldName];
const fieldIsValid = validateField(fieldName, fieldValue);
if (!fieldIsValid) isValid = false;
});
return isValid;
};
const handleSubmit = (event) => {
event.preventDefault();
const isValid = validateForm();
if (isValid) {
setFormSubmitted(true);
}
};
const getPasswortOutput = () => {
return formData.fieldPassword.split("").map(() => "*").join("");
};
return (
<>
<form>
<div className="form_field">
<label htmlFor="fieldUsername">Benutzername:</label>
<input
type="text"
id="fieldUsername"
name="fieldUsername"
value={formData.fieldUsername}
onChange={handleInputChange}
/>
{formErrors.fieldUsername && (
<p className="error">{formErrors.fieldUsername}</p>
)}
</div>
<div className="form_field">
<label htmlFor="fieldEmail">E-Mail:</label>
<input
type="email"
id="fieldEmail"
name="fieldEmail"
value={formData.fieldEmail}
onChange={handleInputChange}
/>
{formErrors.fieldEmail && (
<p className="error">{formErrors.fieldEmail}</p>
)}
</div>
<div className="form_field">
<label htmlFor="fieldPassword">Passwort:</label>
<input
type="password"
id="fieldPassword"
name="fieldPassword"
value={formData.fieldPassword}
onChange={handleInputChange}
/>
{formErrors.fieldPassword && (
<p className="error">{formErrors.fieldPassword}</p>
)}
</div>
<div className="form_field">
<label htmlFor="fieldPasswordConfirm">Passwort (Wdh.):</label>
<input
type="password"
id="fieldPasswordConfirm"
name="fieldPasswordConfirm"
value={formData.fieldPasswordConfirm}
onChange={handleInputChange}
/>
{formErrors.fieldPasswordConfirm && (
<p className="error">{formErrors.fieldPasswordConfirm}</p>
)}
</div>
<div className="form_actions">
<button onClick={handleSubmit}>
Absenden
</button>
</div>
</form>
{formSubmitted && (
<>
<p>Registrierung abgeschlossen</p>
<p>Benutzername: {formData.fieldUsername}</p>
<p>E-Mail: {formData.fieldEmail}</p>
<p>Passwort: {getPasswortOutput()}</p>
</>
)}
</>
);
};
export default FormValidation;
In diesem, etwas größeren Beispiel, haben wir eine simple Validierung der einzelnen Felder eingebaut. Sicherlich kann man die Validierungslogik und den Umfang etwas erhöhen. Der aktuelle Stand reicht allerdings aus, um das Prinzip zu verstehen.
Formular mit bedingten Feldern
Im letzten Beispiel in diesem Artikel schauen wir uns, wie man beispielhaft ein Formular mit bedingten Feldern aufbauen kann.
Auch in diesem Formular werden wir Zustandswerte für die Formulardaten und potenzielle Fehler. Außerdem werden wir hier ein paar Validierungen durchführen und ein paar generische Funktionen für die Abfertigung von Eingabefeldern und Checkboxen haben.
import { useState } from 'react';
import './ConditionalForm.scss';
const ConditionalForm = () => {
const [formData, setFormData] = useState({
fieldName: '',
fieldEmail: '',
fieldNotificationType: '',
fieldPhone: '',
fieldFrequency: '',
fieldTopics: [],
fieldCustomTopic: '',
fieldTermsAccepted: false
});
const [fieldErrors, setFieldErrors] = useState({});
const [formSubmitted, setFormSubmitted] = useState(false);
const handleInputChange = (event) => {
const { name, value } = event.target;
setFormData({ ...formData, [name]: value });
// Remove errors, when the field is edited
if (fieldErrors[name]) {
setFieldErrors({ ...fieldErrors, [name]: '' });
}
};
// Checkbox changes
const handleCheckboxChange = (event) => {
const { name, checked } = event.target;
setFormData({ ...formData, [name]: checked });
// Remove errors, when the field is edited
if (fieldErrors[name]) {
setFieldErrors({ ...fieldErrors, [name]: '' });
}
};
// Topics selection changes
const handleTopicChange = (event) => {
const { value, checked } = event.target;
let updatedTopics;
if (checked) {
// Add element
updatedTopics = [...formData.fieldTopics, value];
} else {
// Remove element
updatedTopics = formData.fieldTopics.filter(topic => topic !== value);
}
setFormData({
...formData,
fieldTopics: updatedTopics,
fieldCustomTopic: value === 'other' && !checked ? '' : formData.fieldCustomTopic
});
// Remove errors, when the field is edited
if (fieldErrors.fieldTopics) {
setFieldErrors({ ...fieldErrors, fieldTopics: '' });
}
};
const handleSubmit = (event) => {
event.preventDefault();
const newErrors = {};
// Validate: fieldName
if (!formData.fieldName.trim()) {
newErrors.fieldName = 'Name ist erforderlich';
}
// Validate: fieldEmail
if (!formData.fieldEmail.trim()) {
newErrors.fieldEmail = 'E-Mail ist erforderlich';
} else if (!/\S+@\S+\.\S+/.test(formData.fieldEmail)) {
newErrors.fieldEmail = 'Ungültige E-Mail';
}
// Validate: fieldNotificationType (sms)
if (formData.fieldNotificationType === 'sms' && !formData.fieldPhone) {
newErrors.fieldPhone = 'Telefonnummer ist erforderlich für SMS-Benachrichtigungen';
}
// Validate: fieldNotificationType (none)
if (formData.fieldNotificationType !== 'none' && !formData.fieldFrequency) {
newErrors.fieldFrequency = 'Bitte wähle eine Benachrichtigungshäufigkeit';
}
// Validate: fieldTopics (length)
if (formData.fieldTopics.length === 0) {
newErrors.fieldTopics = 'Bitte wähle mindestens ein Thema';
}
// Validate: fieldTopics (other)
if (formData.fieldTopics.includes('other') && !formData.fieldCustomTopic.trim()) {
newErrors.fieldCustomTopic = 'Bitte gib ein benutzerdefiniertes Thema an';
}
// Validate: fieldTermsAccepted
if (!formData.fieldTermsAccepted) {
newErrors.fieldTermsAccepted = 'Du musst den Bedingungen zustimmen';
}
// Submit form or set errors
if (Object.keys(newErrors).length > 0) {
setFieldErrors(newErrors);
} else {
setFormSubmitted(true);
}
};
return (
<>
<form>
{/* FIELD: fieldName (name) */}
<div className="form_field">
<label htmlFor="fieldName">Name</label>
<input
type="text"
id="fieldName"
name="fieldName"
value={formData.fieldName}
onChange={handleInputChange}
/>
{fieldErrors.fieldName && (
<p className="field_error">{fieldErrors.fieldName}</p>
)}
</div>
{/* FIELD: fieldEmail (email) */}
<div className="form_field">
<label htmlFor="fieldEmail">E-Mail</label>
<input
type="email"
id="fieldEmail"
name="fieldEmail"
value={formData.fieldEmail}
onChange={handleInputChange}
/>
{fieldErrors.fieldEmail && (
<p className="field_error">{fieldErrors.fieldEmail}</p>
)}
</div>
{/* FIELD: fieldNotificationType (notificationType) */}
<div className="form_field">
<label htmlFor="fieldNotificationType">Benachrichtigungsart</label>
<select
name="fieldNotificationType"
value={formData.fieldNotificationType}
onChange={handleInputChange}
>
<option value="">Bitte wählen</option>
<option value="email">E-Mail</option>
<option value="sms">SMS</option>
<option value="none">Keine Benachrichtigung</option>
</select>
{fieldErrors.fieldNotificationType && (
<p className="field_error">{fieldErrors.fieldNotificationType}</p>
)}
</div>
{/* FIELD OPTIONAL: fieldPhone (phone) */}
{formData.fieldNotificationType === 'sms' && (
<div className="form_field">
<label htmlFor="fieldPhone">Telefon</label>
<input
type="tel"
id="fieldPhone"
name="fieldPhone"
value={formData.fieldPhone}
onChange={handleInputChange}
/>
{fieldErrors.fieldPhone && (
<p className="field_error">{fieldErrors.fieldPhone}</p>
)}
</div>
)}
{/* FIELD OPTIONAL: fieldFrequency (frequency) */}
{formData.fieldNotificationType !== '' && formData.fieldNotificationType !== 'none' && (
<div className="form_field">
<label htmlFor="fieldFrequency">Benachrichtigungshäufigkeit</label>
<select
id="fieldFrequency"
name="fieldFrequency"
value={formData.fieldFrequency}
onChange={handleInputChange}
>
<option value="">Bitte wählen</option>
<option value="daily">Täglich</option>
<option value="weekly">Wöchentlich</option>
<option value="monthly">Monatlich</option>
</select>
{fieldErrors.fieldFrequency && (
<p className="field_error">{fieldErrors.fieldFrequency}</p>
)}
</div>
)}
{/* FIELD: fieldTopics (topics) */}
<div className="form_field display_column">
<label>Interessante Themen</label>
<div className="checkbox_wrapper">
<input
type="checkbox"
id="topic_news"
value="news"
checked={formData.fieldTopics.includes("news")}
onChange={handleTopicChange}
/>
<label htmlFor="topic_news">Nachrichten</label>
</div>
<div className="checkbox_wrapper">
<input
type="checkbox"
id="topic_updates"
value="updates"
checked={formData.fieldTopics.includes("updates")}
onChange={handleTopicChange}
/>
<label htmlFor="topic_updates">Produkt Updates</label>
</div>
<div className="checkbox_wrapper">
<input
type="checkbox"
id="topic_events"
value="events"
checked={formData.fieldTopics.includes("events")}
onChange={handleTopicChange}
/>
<label htmlFor="topic_events">Veranstaltungen</label>
</div>
<div className="checkbox_wrapper">
<input
type="checkbox"
id="topic_other"
value="other"
checked={formData.fieldTopics.includes("other")}
onChange={handleTopicChange}
/>
<label htmlFor="topic_other">Eigenes Thema</label>
</div>
{fieldErrors.fieldTopics && (
<p className="field_error">{fieldErrors.fieldTopics}</p>
)}
</div>
{/* FIELD OPTIONAL: fieldCustomTopic (custom topic) */}
{formData.fieldTopics.includes('other') && (
<div className="form_field">
<label htmlFor="fieldCustomTopic">Eigenes Thema</label>
<input
type="text"
id="fieldCustomTopic"
name="fieldCustomTopic"
value={formData.fieldCustomTopic}
onChange={handleInputChange}
/>
{fieldErrors.fieldCustomTopic && (
<p className="field_error">{fieldErrors.fieldCustomTopic}</p>
)}
</div>
)}
{/* FIELD: fieldTermsAccepted (terms) */}
<div className="form_field">
<div className="checkbox_wrapper">
<input
type="checkbox"
id="fieldTermsAccepted"
name="fieldTermsAccepted"
checked={formData.fieldTermsAccepted}
onChange={handleCheckboxChange}
/>
<label htmlFor="fieldTermsAccepted">Ich stimme den Nutzungsbedingungen zu</label>
</div>
{fieldErrors.fieldTermsAccepted && (
<p className="field_error">{fieldErrors.fieldTermsAccepted}</p>
)}
</div>
<div className="form_actions">
<button onClick={handleSubmit}>
Speichern
</button>
</div>
</form>
{formSubmitted && (
<div className="form_submitted_info">
<p>Hallo, {formData.fieldName}, die Einstellungen wurden gespeichert.</p>
</div>
)}
</>
);
};
export default ConditionalForm;