Impara come implementare la stima del progresso e la previsione del tempo di completamento utilizzando l'hook useFormStatus di React, migliorando l'esperienza utente in applicazioni complesse.
Stima del Progresso con useFormStatus di React: Previsione del Tempo di Completamento
L'hook useFormStatus di React, introdotto in React 18, fornisce informazioni preziose sullo stato di invio di un modulo. Sebbene non offra direttamente una stima del progresso, possiamo sfruttare le sue proprietà e altre tecniche per fornire agli utenti un feedback significativo durante invii di moduli potenzialmente lunghi. Questo post esplora i metodi per stimare il progresso e prevedere il tempo di completamento quando si utilizza useFormStatus, ottenendo un'esperienza più coinvolgente e user-friendly.
Comprendere useFormStatus
Prima di addentrarci nella stima del progresso, ricapitoliamo rapidamente lo scopo di useFormStatus. Questo hook è progettato per essere utilizzato all'interno di un elemento <form> che utilizza la prop action. Restituisce un oggetto contenente le seguenti proprietà:
pending: Un booleano che indica se il modulo è attualmente in fase di invio.data: I dati che sono stati inviati con il modulo (se l'invio ha avuto successo).method: Il metodo HTTP utilizzato per l'invio del modulo (es. 'POST', 'GET').action: La funzione passata alla propactiondel modulo.error: Un oggetto di errore se l'invio è fallito.
Anche se useFormStatus ci dice se il modulo è in fase di invio, non fornisce alcuna informazione diretta sull'avanzamento dell'invio, specialmente se la funzione action comporta operazioni complesse o lunghe.
La Sfida della Stima del Progresso
La sfida principale risiede nel fatto che l'esecuzione della funzione action è opaca per React. Non sappiamo intrinsecamente a che punto sia il processo. Questo è particolarmente vero per le operazioni lato server. Tuttavia, possiamo impiegare varie strategie per superare questa limitazione.
Strategie per la Stima del Progresso
Ecco diversi approcci che si possono adottare, ognuno con i propri compromessi:
1. Server-Sent Events (SSE) o WebSockets
La soluzione più robusta è spesso quella di inviare aggiornamenti sul progresso dal server al client. Ciò può essere ottenuto utilizzando:
- Server-Sent Events (SSE): Un protocollo unidirezionale (dal server al client) che consente al server di inviare aggiornamenti al client tramite una singola connessione HTTP. SSE è ideale quando il client ha solo bisogno di *ricevere* aggiornamenti.
- WebSockets: Un protocollo di comunicazione bidirezionale che fornisce una connessione persistente tra client e server. I WebSockets sono adatti per aggiornamenti in tempo reale in entrambe le direzioni.
Esempio (SSE):
Lato server (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); // Simula aggiornamento del progresso ogni 500ms
});
app.listen(3000, () => {
console.log('Server in ascolto sulla porta 3000');
});
Lato client (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 failed:', error);
eventSource.close();
};
return () => {
eventSource.close();
};
}, []);
return (
<div>
<p>Progresso: {progress}%</p>
</div>
);
}
export default MyComponent;
Spiegazione:
- Il server imposta gli header appropriati per SSE.
- Il server invia aggiornamenti sul progresso come eventi
data:. Ogni evento è un oggetto JSON contenente ilprogresse un flagcompleted. - Il componente React utilizza
EventSourceper ascoltare questi eventi. - Il componente aggiorna lo stato (
progress) in base agli eventi ricevuti.
Vantaggi: Aggiornamenti accurati del progresso, feedback in tempo reale.
Svantaggi: Richiede modifiche lato server, implementazione più complessa.
2. Polling con un Endpoint API
Se non è possibile utilizzare SSE o WebSockets, si può implementare il polling. Il client invia periodicamente richieste al server per verificare lo stato dell'operazione.
Esempio:
Lato server (Node.js):
const express = require('express');
const app = express();
// Simula un'attività a lunga esecuzione
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); // Genera un ID attività univoco
// Simula elaborazione in background
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: 'Attività non trovata' });
}
});
app.listen(3000, () => {
console.log('Server in ascolto sulla porta 3000');
});
Lato client (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); // Interroga ogni 1 secondo
return () => clearInterval(interval);
}, [taskId]);
return (
<div>
<button onClick={startTask} disabled={taskId !== null}>Avvia Attività</button>
{taskId && <p>Progresso: {progress}%</p>}
</div>
);
}
export default MyComponent;
Spiegazione:
- Il client avvia un'attività chiamando
/start-task, ricevendo untaskId. - Il client quindi interroga periodicamente
/task-status/:taskIdper ottenere il progresso.
Vantaggi: Relativamente semplice da implementare, non richiede connessioni persistenti.
Svantaggi: Può essere meno accurato di SSE/WebSockets, introduce latenza a causa dell'intervallo di polling, aumenta il carico sul server a causa delle richieste frequenti.
3. Aggiornamenti Ottimistici ed Euristiche
In alcuni casi, è possibile utilizzare aggiornamenti ottimistici combinati con euristiche per fornire una stima ragionevole. Ad esempio, se si stanno caricando file, è possibile monitorare il numero di byte caricati lato client e stimare il progresso in base alla dimensione totale del file.
Esempio (Caricamento File):
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'); // Sostituire con il proprio endpoint di caricamento
xhr.send(formData);
xhr.onload = () => {
if (xhr.status === 200) {
console.log('Caricamento completato!');
} else {
console.error('Caricamento fallito:', xhr.status);
}
};
xhr.onerror = () => {
console.error('Caricamento fallito');
};
} catch (error) {
console.error('Errore di caricamento:', error);
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input type="file" onChange={handleFileChange} />
<button type="submit" disabled={!file}>Carica</button>
</form>
<p>Progresso: {progress}%</p>
</div>
);
}
export default MyComponent;
Spiegazione:
- Il componente utilizza un oggetto
XMLHttpRequestper caricare il file. - L'event listener
progresssuxhr.uploadviene utilizzato per monitorare l'avanzamento del caricamento. - Le proprietà
loadedetotaldell'evento vengono utilizzate per calcolare la percentuale di completamento.
Vantaggi: Solo lato client, può fornire un feedback immediato.
Svantaggi: L'accuratezza dipende dall'affidabilità dell'euristica, potrebbe non essere adatta a tutti i tipi di operazioni.
4. Scomporre l'Azione in Passaggi Più Piccoli
Se la funzione action esegue più passaggi distinti, è possibile aggiornare l'interfaccia utente dopo ogni passaggio per indicare il progresso. Ciò richiede la modifica della funzione action per fornire aggiornamenti.
Esempio:
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">Invia</button>
</form>
<p>Progresso: {progress}%</p>
</div>
);
}
export default MyComponent;
Spiegazione:
- La funzione
myActionaccetta una callbacksetProgress. - Aggiorna lo stato del progresso in vari punti durante la sua esecuzione.
Vantaggi: Controllo diretto sugli aggiornamenti del progresso.
Svantaggi: Richiede la modifica della funzione action, può essere più complesso da implementare se i passaggi non sono facilmente divisibili.
Prevedere il Tempo di Completamento
Una volta ottenuti gli aggiornamenti sul progresso, è possibile utilizzarli per prevedere il tempo residuo stimato. Un approccio semplice consiste nel monitorare il tempo impiegato per raggiungere un certo livello di progresso ed estrapolare per stimare il tempo totale.
Esempio (Semplificato):
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)); // Assicura che non sia negativo
}
}, [progress]);
// ... (resto del componente e aggiornamenti del progresso come descritto nelle sezioni precedenti)
return (
<div>
<p>Progresso: {progress}%</p>
{estimatedTimeRemaining !== null && (
<p>Tempo Stimato Rimanente: {Math.round(estimatedTimeRemaining / 1000)} secondi</p>
)}
</div>
);
}
export default MyComponent;
Spiegazione:
- Memorizziamo l'ora di inizio quando il progresso viene aggiornato per la prima volta.
- Calcoliamo il tempo trascorso e lo utilizziamo per stimare il tempo totale.
- Calcoliamo il tempo rimanente sottraendo il tempo trascorso dal tempo totale stimato.
Considerazioni Importanti:
- Precisione: Questa è una previsione *molto* semplificata. Le condizioni di rete, il carico del server e altri fattori possono influire significativamente sulla precisione. Tecniche più sofisticate, come la media su più intervalli, possono migliorare la precisione.
- Feedback Visivo: Indicare chiaramente che il tempo è una *stima*. Visualizzare intervalli (es. "Tempo stimato rimanente: 5-10 secondi") può essere più realistico.
- Casi Limite: Gestire i casi limite in cui il progresso è molto lento inizialmente. Evitare la divisione per zero o la visualizzazione di stime eccessivamente grandi.
Combinare useFormStatus con la Stima del Progresso
Sebbene useFormStatus di per sé non fornisca informazioni sul progresso, è possibile utilizzare la sua proprietà pending per attivare o disattivare l'indicatore di progresso. Ad esempio:
import React, { useState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (Logica di stima del progresso dagli esempi precedenti)
function MyComponent() {
const [progress, setProgress] = useState(0);
const { pending } = useFormStatus();
const handleSubmit = async (formData) => {
// ... (La tua logica di invio del modulo, inclusi gli aggiornamenti al progresso)
};
return (
<form action={handleSubmit}>
<button type="submit" disabled={pending}>Invia</button>
{pending && <p>Progresso: {progress}%</p>}
</form>
);
}
In questo esempio, l'indicatore di progresso viene visualizzato solo mentre il modulo è in attesa (cioè, mentre useFormStatus.pending è true).
Best Practice e Considerazioni
- Dare Priorità alla Precisione: Scegliere una tecnica di stima del progresso appropriata per il tipo di operazione eseguita. SSE/WebSockets forniscono generalmente i risultati più accurati, mentre le euristiche possono essere sufficienti per compiti più semplici.
- Fornire un Feedback Visivo Chiaro: Utilizzare barre di avanzamento, spinner o altri indicatori visivi per indicare che un'operazione è in corso. Etichettare chiaramente l'indicatore di progresso e, se applicabile, il tempo stimato rimanente.
- Gestire gli Errori con Eleganza: Se si verifica un errore durante l'operazione, visualizzare un messaggio di errore informativo all'utente. Evitare di lasciare l'indicatore di progresso bloccato a una certa percentuale.
- Ottimizzare le Prestazioni: Evitare di eseguire operazioni computazionalmente onerose nel thread dell'interfaccia utente, poiché ciò può influire negativamente sulle prestazioni. Utilizzare web worker o altre tecniche per delegare il lavoro a thread in background.
- Accessibilità: Assicurarsi che gli indicatori di progresso siano accessibili agli utenti con disabilità. Utilizzare attributi ARIA per fornire informazioni semantiche sull'avanzamento dell'operazione. Ad esempio, usare
aria-valuenow,aria-valueminearia-valuemaxsu una barra di avanzamento. - Localizzazione: Quando si visualizza il tempo stimato rimanente, tenere conto dei diversi formati di ora e delle preferenze regionali. Utilizzare una libreria come
date-fnsomoment.jsper formattare l'ora in modo appropriato per la localizzazione dell'utente. - Internazionalizzazione: I messaggi di errore e altri testi dovrebbero essere internazionalizzati per supportare più lingue. Utilizzare una libreria come
i18nextper gestire le traduzioni.
Conclusione
Sebbene l'hook useFormStatus di React non fornisca direttamente funzionalità di stima del progresso, è possibile combinarlo con altre tecniche per offrire agli utenti un feedback significativo durante l'invio di moduli. Utilizzando SSE/WebSockets, polling, aggiornamenti ottimistici o scomponendo le azioni in passaggi più piccoli, è possibile creare un'esperienza più coinvolgente e user-friendly. Ricorda di dare priorità alla precisione, fornire un feedback visivo chiaro, gestire gli errori con eleganza e ottimizzare le prestazioni per garantire un'esperienza positiva a tutti gli utenti, indipendentemente dalla loro posizione o background.