Erfahren Sie, wie Sie mit dem useFormStatus-Hook von React eine Fortschrittsschätzung und die Vorhersage der Fertigstellungszeit implementieren, um die Benutzererfahrung in datenintensiven Anwendungen zu verbessern.
React useFormStatus Fortschrittsschätzung: Vorhersage der Fertigstellungszeit
Der useFormStatus-Hook von React, eingeführt in React 18, liefert wertvolle Informationen über den Status einer Formularübermittlung. Obwohl er keine direkte Fortschrittsschätzung bietet, können wir seine Eigenschaften und andere Techniken nutzen, um Benutzern während potenziell lang andauernder Formularübermittlungen aussagekräftiges Feedback zu geben. Dieser Beitrag untersucht Methoden zur Schätzung des Fortschritts und zur Vorhersage der Fertigstellungszeit bei Verwendung von useFormStatus, was zu einer ansprechenderen und benutzerfreundlicheren Erfahrung führt.
Verständnis von useFormStatus
Bevor wir uns mit der Fortschrittsschätzung befassen, lassen Sie uns kurz den Zweck von useFormStatus zusammenfassen. Dieser Hook ist für die Verwendung innerhalb eines <form>-Elements konzipiert, das die action-Prop verwendet. Er gibt ein Objekt zurück, das die folgenden Eigenschaften enthält:
pending: Ein boolescher Wert, der anzeigt, ob das Formular gerade übermittelt wird.data: Die Daten, die mit dem Formular übermittelt wurden (wenn die Übermittlung erfolgreich war).method: Die für die Formularübermittlung verwendete HTTP-Methode (z. B. 'POST', 'GET').action: Die Funktion, die an dieaction-Prop des Formulars übergeben wurde.error: Ein Fehlerobjekt, falls die Übermittlung fehlgeschlagen ist.
Obwohl useFormStatus uns mitteilt, ob das Formular übermittelt wird, gibt es keine direkten Informationen über den Fortschritt der Übermittlung, insbesondere wenn die action-Funktion komplexe oder langwierige Operationen beinhaltet.
Die Herausforderung der Fortschrittsschätzung
Die zentrale Herausforderung liegt darin, dass die Ausführung der action-Funktion für React undurchsichtig ist. Wir wissen von Natur aus nicht, wie weit der Prozess fortgeschritten ist. Dies gilt insbesondere für serverseitige Operationen. Wir können jedoch verschiedene Strategien anwenden, um diese Einschränkung zu überwinden.
Strategien zur Fortschrittsschätzung
Hier sind mehrere Ansätze, die Sie verfolgen können, jeder mit seinen eigenen Vor- und Nachteilen:
1. Server-Sent Events (SSE) oder WebSockets
Die robusteste Lösung besteht oft darin, Fortschrittsaktualisierungen vom Server an den Client zu senden (push). Dies kann erreicht werden durch:
- Server-Sent Events (SSE): Ein unidirektionales (Server-zu-Client) Protokoll, das es dem Server ermöglicht, Aktualisierungen über eine einzige HTTP-Verbindung an den Client zu senden. SSE ist ideal, wenn der Client nur Updates *empfangen* muss.
- WebSockets: Ein bidirektionales Kommunikationsprotokoll, das eine dauerhafte Verbindung zwischen Client und Server bietet. WebSockets eignen sich für Echtzeit-Updates in beide Richtungen.
Beispiel (SSE):
Serverseitig (Node.js):
const express = require('express');
const app = express();
app.get('/progress', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
let progress = 0;
const interval = setInterval(() => {
progress += 10;
if (progress > 100) {
progress = 100;
clearInterval(interval);
res.write(`data: {"progress": ${progress}, "completed": true}\n\n`);
res.end();
} else {
res.write(`data: {"progress": ${progress}, "completed": false}\n\n`);
}
}, 500); // Simuliert Fortschritts-Update alle 500ms
});
app.listen(3000, () => {
console.log('Server lauscht auf Port 3000');
});
Clientseitig (React):
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
useEffect(() => {
const eventSource = new EventSource('/progress');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
setProgress(data.progress);
if (data.completed) {
eventSource.close();
}
};
eventSource.onerror = (error) => {
console.error('EventSource ist fehlgeschlagen:', error);
eventSource.close();
};
return () => {
eventSource.close();
};
}, []);
return (
<div>
<p>Fortschritt: {progress}%</p>
</div>
);
}
export default MyComponent;
Erklärung:
- Der Server setzt die entsprechenden Header für SSE.
- Der Server sendet Fortschrittsaktualisierungen als
data:-Events. Jedes Event ist ein JSON-Objekt, das denprogressund eincompleted-Flag enthält. - Die React-Komponente verwendet
EventSource, um auf diese Events zu lauschen. - Die Komponente aktualisiert den Zustand (
progress) basierend auf den empfangenen Events.
Vorteile: Genaue Fortschrittsaktualisierungen, Echtzeit-Feedback.
Nachteile: Erfordert serverseitige Änderungen, komplexere Implementierung.
2. Abfragen (Polling) eines API-Endpunkts
Wenn Sie SSE oder WebSockets nicht verwenden können, können Sie Polling implementieren. Der Client sendet periodisch Anfragen an den Server, um den Status der Operation zu überprüfen.
Beispiel:
Serverseitig (Node.js):
const express = require('express');
const app = express();
// Simuliert eine lang andauernde Aufgabe
let taskProgress = 0;
let taskId = null;
app.post('/start-task', (req, res) => {
taskProgress = 0;
taskId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); // Generiert eine eindeutige Aufgaben-ID
// Simuliert Hintergrundverarbeitung
const interval = setInterval(() => {
taskProgress += 10;
if (taskProgress >= 100) {
taskProgress = 100;
clearInterval(interval);
}
}, 500);
res.json({ taskId });
});
app.get('/task-status/:taskId', (req, res) => {
if (req.params.taskId === taskId) {
res.json({ progress: taskProgress });
} else {
res.status(404).json({ message: 'Aufgabe nicht gefunden' });
}
});
app.listen(3000, () => {
console.log('Server lauscht auf Port 3000');
});
Clientseitig (React):
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [taskId, setTaskId] = useState(null);
const startTask = async () => {
const response = await fetch('/start-task', { method: 'POST' });
const data = await response.json();
setTaskId(data.taskId);
};
useEffect(() => {
if (!taskId) return;
const interval = setInterval(async () => {
const response = await fetch(`/task-status/${taskId}`);
const data = await response.json();
setProgress(data.progress);
if (data.progress === 100) {
clearInterval(interval);
}
}, 1000); // Alle 1 Sekunde abfragen
return () => clearInterval(interval);
}, [taskId]);
return (
<div>
<button onClick={startTask} disabled={taskId !== null}>Aufgabe starten</button>
{taskId && <p>Fortschritt: {progress}%</p>}
</div>
);
}
export default MyComponent;
Erklärung:
- Der Client startet eine Aufgabe durch den Aufruf von
/start-taskund erhält einetaskId. - Der Client fragt dann periodisch
/task-status/:taskIdab, um den Fortschritt zu erhalten.
Vorteile: Relativ einfach zu implementieren, erfordert keine dauerhaften Verbindungen.
Nachteile: Kann ungenauer als SSE/WebSockets sein, führt Latenz durch das Abfrageintervall ein, belastet den Server durch häufige Anfragen.
3. Optimistische Aktualisierungen und Heuristiken
In einigen Fällen können Sie optimistische Aktualisierungen in Kombination mit Heuristiken verwenden, um eine vernünftige Schätzung zu liefern. Wenn Sie beispielsweise Dateien hochladen, können Sie die Anzahl der clientseitig hochgeladenen Bytes verfolgen und den Fortschritt basierend auf der Gesamtdateigröße schätzen.
Beispiel (Dateiupload):
import React, { useState } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [file, setFile] = useState(null);
const handleFileChange = (event) => {
setFile(event.target.files[0]);
};
const handleSubmit = async (event) => {
event.preventDefault();
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentage = Math.round((event.loaded * 100) / event.total);
setProgress(percentage);
}
});
xhr.open('POST', '/upload'); // Durch Ihren Upload-Endpunkt ersetzen
xhr.send(formData);
xhr.onload = () => {
if (xhr.status === 200) {
console.log('Upload abgeschlossen!');
} else {
console.error('Upload fehlgeschlagen:', xhr.status);
}
};
xhr.onerror = () => {
console.error('Upload fehlgeschlagen');
};
} catch (error) {
console.error('Upload-Fehler:', error);
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input type="file" onChange={handleFileChange} />
<button type="submit" disabled={!file}>Hochladen</button>
</form>
<p>Fortschritt: {progress}%</p>
</div>
);
}
export default MyComponent;
Erklärung:
- Die Komponente verwendet ein
XMLHttpRequest-Objekt, um die Datei hochzuladen. - Der
progress-Event-Listener aufxhr.uploadwird verwendet, um den Upload-Fortschritt zu verfolgen. - Die
loaded- undtotal-Eigenschaften des Events werden verwendet, um den prozentualen Abschluss zu berechnen.
Vorteile: Nur clientseitig, kann sofortiges Feedback geben.
Nachteile: Die Genauigkeit hängt von der Zuverlässigkeit der Heuristik ab, möglicherweise nicht für alle Arten von Operationen geeignet.
4. Aufteilen der Aktion in kleinere Schritte
Wenn die action-Funktion mehrere verschiedene Schritte ausführt, können Sie die Benutzeroberfläche nach jedem Schritt aktualisieren, um den Fortschritt anzuzeigen. Dies erfordert eine Änderung der action-Funktion, um Aktualisierungen bereitzustellen.
Beispiel:
import React, { useState } from 'react';
async function myAction(setProgress) {
setProgress(10);
await someAsyncOperation1();
setProgress(40);
await someAsyncOperation2();
setProgress(70);
await someAsyncOperation3();
setProgress(100);
}
function MyComponent() {
const [progress, setProgress] = useState(0);
const handleSubmit = async () => {
await myAction(setProgress);
};
return (
<div>
<form onSubmit={handleSubmit}>
<button type="submit">Absenden</button>
</form>
<p>Fortschritt: {progress}%</p>
</div>
);
}
export default MyComponent;
Erklärung:
- Die
myAction-Funktion akzeptiert einensetProgress-Callback. - Sie aktualisiert den Fortschrittszustand an verschiedenen Punkten während ihrer Ausführung.
Vorteile: Direkte Kontrolle über Fortschrittsaktualisierungen.
Nachteile: Erfordert die Änderung der action-Funktion, kann komplexer zu implementieren sein, wenn die Schritte nicht leicht teilbar sind.
Vorhersage der Fertigstellungszeit
Sobald Sie Fortschrittsaktualisierungen haben, können Sie diese verwenden, um die geschätzte verbleibende Zeit vorherzusagen. Ein einfacher Ansatz besteht darin, die Zeit zu verfolgen, die benötigt wird, um ein bestimmtes Fortschrittsniveau zu erreichen, und daraus die Gesamtzeit zu extrapolieren.
Beispiel (Vereinfacht):
import React, { useState, useEffect, useRef } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(null);
const startTimeRef = useRef(null);
useEffect(() => {
if (progress > 0 && startTimeRef.current === null) {
startTimeRef.current = Date.now();
}
if (progress > 0) {
const elapsedTime = Date.now() - startTimeRef.current;
const estimatedTotalTime = (elapsedTime / progress) * 100;
const remainingTime = estimatedTotalTime - elapsedTime;
setEstimatedTimeRemaining(Math.max(0, remainingTime)); // Stellt sicher, dass der Wert nicht negativ ist
}
}, [progress]);
// ... (Rest der Komponente und Fortschrittsaktualisierungen wie in den vorherigen Abschnitten beschrieben)
return (
<div>
<p>Fortschritt: {progress}%</p>
{estimatedTimeRemaining !== null && (
<p>Geschätzte verbleibende Zeit: {Math.round(estimatedTimeRemaining / 1000)} Sekunden</p>
)}
</div>
);
}
export default MyComponent;
Erklärung:
- Wir speichern die Startzeit, wenn der Fortschritt zum ersten Mal aktualisiert wird.
- Wir berechnen die verstrichene Zeit und verwenden sie, um die Gesamtzeit zu schätzen.
- Wir berechnen die verbleibende Zeit, indem wir die verstrichene Zeit von der geschätzten Gesamtzeit abziehen.
Wichtige Überlegungen:
- Genauigkeit: Dies ist eine *sehr* vereinfachte Vorhersage. Netzwerkbedingungen, Serverlast und andere Faktoren können die Genauigkeit erheblich beeinflussen. Ausgefeiltere Techniken, wie die Mittelwertbildung über mehrere Intervalle, können die Genauigkeit verbessern.
- Visuelles Feedback: Geben Sie deutlich an, dass die Zeit eine *Schätzung* ist. Die Anzeige von Bereichen (z. B. "Geschätzte verbleibende Zeit: 5-10 Sekunden") kann realistischer sein.
- Grenzfälle: Behandeln Sie Grenzfälle, in denen der Fortschritt anfangs sehr langsam ist. Vermeiden Sie die Division durch Null oder die Anzeige übermäßig großer Schätzungen.
Kombination von useFormStatus mit der Fortschrittsschätzung
Obwohl useFormStatus selbst keine Fortschrittsinformationen liefert, können Sie seine pending-Eigenschaft verwenden, um die Fortschrittsanzeige zu aktivieren oder zu deaktivieren. Zum Beispiel:
import React, { useState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (Logik zur Fortschrittsschätzung aus den vorherigen Beispielen)
function MyComponent() {
const [progress, setProgress] = useState(0);
const { pending } = useFormStatus();
const handleSubmit = async (formData) => {
// ... (Ihre Logik zur Formularübermittlung, einschließlich Aktualisierungen des Fortschritts)
};
return (
<form action={handleSubmit}>
<button type="submit" disabled={pending}>Absenden</button>
{pending && <p>Fortschritt: {progress}%</p>}
</form>
);
}
In diesem Beispiel wird die Fortschrittsanzeige nur angezeigt, während das Formular ausstehend ist (d. h. während useFormStatus.pending true ist).
Best Practices und Überlegungen
- Priorisieren Sie Genauigkeit: Wählen Sie eine Technik zur Fortschrittsschätzung, die für die Art der durchgeführten Operation geeignet ist. SSE/WebSockets liefern im Allgemeinen die genauesten Ergebnisse, während Heuristiken für einfachere Aufgaben ausreichend sein können.
- Geben Sie klares visuelles Feedback: Verwenden Sie Fortschrittsbalken, Ladeindikatoren oder andere visuelle Hinweise, um anzuzeigen, dass eine Operation läuft. Beschriften Sie die Fortschrittsanzeige und gegebenenfalls die geschätzte verbleibende Zeit deutlich.
- Behandeln Sie Fehler ordnungsgemäß: Wenn während der Operation ein Fehler auftritt, zeigen Sie dem Benutzer eine informative Fehlermeldung an. Vermeiden Sie es, die Fortschrittsanzeige bei einem bestimmten Prozentsatz stehen zu lassen.
- Optimieren Sie die Leistung: Vermeiden Sie rechenintensive Operationen im UI-Thread, da dies die Leistung negativ beeinflussen kann. Verwenden Sie Web Worker oder andere Techniken, um Arbeit in Hintergrund-Threads auszulagern.
- Barrierefreiheit: Stellen Sie sicher, dass Fortschrittsanzeigen für Benutzer mit Behinderungen zugänglich sind. Verwenden Sie ARIA-Attribute, um semantische Informationen über den Fortschritt der Operation bereitzustellen. Verwenden Sie beispielsweise
aria-valuenow,aria-valueminundaria-valuemaxbei einem Fortschrittsbalken. - Lokalisierung: Achten Sie bei der Anzeige der geschätzten verbleibenden Zeit auf unterschiedliche Zeitformate und regionale Präferenzen. Verwenden Sie eine Bibliothek wie
date-fnsodermoment.js, um die Zeit für das Gebietsschema des Benutzers entsprechend zu formatieren. - Internationalisierung: Fehlermeldungen und andere Texte sollten internationalisiert werden, um mehrere Sprachen zu unterstützen. Verwenden Sie eine Bibliothek wie
i18next, um Übersetzungen zu verwalten.
Fazit
Obwohl der useFormStatus-Hook von React keine direkten Funktionen zur Fortschrittsschätzung bietet, können Sie ihn mit anderen Techniken kombinieren, um Benutzern während der Formularübermittlung aussagekräftiges Feedback zu geben. Durch die Verwendung von SSE/WebSockets, Polling, optimistischen Aktualisierungen oder der Aufteilung von Aktionen in kleinere Schritte können Sie eine ansprechendere und benutzerfreundlichere Erfahrung schaffen. Denken Sie daran, Genauigkeit zu priorisieren, klares visuelles Feedback zu geben, Fehler ordnungsgemäß zu behandeln und die Leistung zu optimieren, um eine positive Erfahrung für alle Benutzer zu gewährleisten, unabhängig von ihrem Standort oder Hintergrund.