Aprenda a implementar estimativa de progresso e previsão de tempo de conclusão usando o hook useFormStatus do React, melhorando a experiência do usuário em aplicações com muitos dados.
Estimativa de Progresso com useFormStatus do React: Previsão de Tempo de Conclusão
O hook useFormStatus do React, introduzido no React 18, fornece informações valiosas sobre o status de um envio de formulário. Embora não ofereça diretamente uma estimativa de progresso, podemos aproveitar suas propriedades e outras técnicas para fornecer aos usuários feedback significativo durante envios de formulários que podem ser demorados. Este post explora métodos para estimar o progresso e prever o tempo de conclusão ao usar o useFormStatus, resultando em uma experiência mais envolvente e amigável para o usuário.
Entendendo o useFormStatus
Antes de mergulhar na estimativa de progresso, vamos recapitular rapidamente o propósito do useFormStatus. Este hook foi projetado para ser usado dentro de um elemento <form> que utiliza a prop action. Ele retorna um objeto contendo as seguintes propriedades:
pending: Um booleano que indica se o formulário está sendo enviado no momento.data: Os dados que foram enviados com o formulário (se o envio foi bem-sucedido).method: O método HTTP usado para o envio do formulário (ex: 'POST', 'GET').action: A função passada para a propactiondo formulário.error: Um objeto de erro se o envio falhar.
Embora o useFormStatus nos diga se o formulário está sendo enviado, ele não fornece nenhuma informação direta sobre o progresso do envio, especialmente se a função action envolver operações complexas ou demoradas.
O Desafio da Estimativa de Progresso
O principal desafio reside no fato de que a execução da função action é opaca para o React. Não sabemos inerentemente o quão avançado está o processo. Isso é especialmente verdadeiro para operações do lado do servidor. No entanto, podemos empregar várias estratégias para superar essa limitação.
Estratégias para Estimativa de Progresso
Aqui estão várias abordagens que você pode adotar, cada uma com seus próprios prós e contras:
1. Server-Sent Events (SSE) ou WebSockets
A solução mais robusta é, muitas vezes, enviar atualizações de progresso do servidor para o cliente. Isso pode ser alcançado usando:
- Server-Sent Events (SSE): Um protocolo unidirecional (servidor-para-cliente) que permite ao servidor enviar atualizações para o cliente através de uma única conexão HTTP. O SSE é ideal quando o cliente precisa apenas *receber* atualizações.
- WebSockets: Um protocolo de comunicação bidirecional que fornece uma conexão persistente entre o cliente e o servidor. Os WebSockets são adequados para atualizações em tempo real em ambas as direções.
Exemplo (SSE):
Lado do Servidor (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 a atualização de progresso a cada 500ms
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
Lado do Cliente (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>Progress: {progress}%</p>
</div>
);
}
export default MyComponent;
Explicação:
- O servidor define os cabeçalhos apropriados para SSE.
- O servidor envia atualizações de progresso como eventos
data:. Cada evento é um objeto JSON contendo oprogresse uma flagcompleted. - O componente React usa o
EventSourcepara escutar esses eventos. - O componente atualiza o estado (
progress) com base nos eventos recebidos.
Vantagens: Atualizações de progresso precisas, feedback em tempo real.
Desvantagens: Requer alterações no lado do servidor, implementação mais complexa.
2. Polling com um Endpoint de API
Se você não pode usar SSE ou WebSockets, pode implementar o polling. O cliente envia periodicamente requisições ao servidor para verificar o status da operação.
Exemplo:
Lado do Servidor (Node.js):
const express = require('express');
const app = express();
// Simula uma tarefa demorada
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); // Gera um ID de tarefa único
// Simula processamento em segundo plano
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: 'Task not found' });
}
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
Lado do Cliente (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); // Faz polling a cada 1 segundo
return () => clearInterval(interval);
}, [taskId]);
return (
<div>
<button onClick={startTask} disabled={taskId !== null}>Start Task</button>
{taskId && <p>Progress: {progress}%</p>}
</div>
);
}
export default MyComponent;
Explicação:
- O cliente inicia uma tarefa chamando
/start-task, recebendo umtaskId. - O cliente então faz polling em
/task-status/:taskIdperiodicamente para obter o progresso.
Vantagens: Relativamente simples de implementar, não requer conexões persistentes.
Desvantagens: Pode ser menos preciso que SSE/WebSockets, introduz latência devido ao intervalo de polling, sobrecarrega o servidor devido às requisições frequentes.
3. Atualizações Otimistas e Heurísticas
Em alguns casos, você pode usar atualizações otimistas combinadas com heurísticas para fornecer uma estimativa razoável. Por exemplo, se você está enviando arquivos, pode rastrear o número de bytes enviados do lado do cliente e estimar o progresso com base no tamanho total do arquivo.
Exemplo (Upload de Arquivo):
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'); // Substitua pelo seu endpoint de upload
xhr.send(formData);
xhr.onload = () => {
if (xhr.status === 200) {
console.log('Upload complete!');
} else {
console.error('Upload failed:', xhr.status);
}
};
xhr.onerror = () => {
console.error('Upload failed');
};
} catch (error) {
console.error('Upload error:', error);
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input type="file" onChange={handleFileChange} />
<button type="submit" disabled={!file}>Upload</button>
</form>
<p>Progress: {progress}%</p>
</div>
);
}
export default MyComponent;
Explicação:
- O componente usa um objeto
XMLHttpRequestpara fazer o upload do arquivo. - O ouvinte de evento
progressemxhr.uploadé usado para rastrear o progresso do upload. - As propriedades
loadedetotaldo evento são usadas para calcular a porcentagem concluída.
Vantagens: Apenas no lado do cliente, pode fornecer feedback imediato.
Desvantagens: A precisão depende da confiabilidade da heurística, pode não ser adequada para todos os tipos de operações.
4. Dividindo a Ação em Etapas Menores
Se a função action executa várias etapas distintas, você pode atualizar a UI após cada etapa para indicar o progresso. Isso requer a modificação da função action para fornecer atualizações.
Exemplo:
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">Submit</button>
</form>
<p>Progress: {progress}%</p>
</div>
);
}
export default MyComponent;
Explicação:
- A função
myActionaceita um callbacksetProgress. - Ela atualiza o estado de progresso em vários pontos durante sua execução.
Vantagens: Controle direto sobre as atualizações de progresso.
Desvantagens: Requer a modificação da função action, pode ser mais complexo de implementar se as etapas não forem facilmente divisíveis.
Prevendo o Tempo de Conclusão
Uma vez que você tenha atualizações de progresso, pode usá-las para prever o tempo restante estimado. Uma abordagem simples é rastrear o tempo levado para atingir um certo nível de progresso e extrapolar para estimar o tempo total.
Exemplo (Simplificado):
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)); // Garante que não seja negativo
}
}, [progress]);
// ... (resto do componente e atualizações de progresso conforme descrito nas seções anteriores)
return (
<div>
<p>Progress: {progress}%</p>
{estimatedTimeRemaining !== null && (
<p>Estimated Time Remaining: {Math.round(estimatedTimeRemaining / 1000)} seconds</p>
)}
</div>
);
}
export default MyComponent;
Explicação:
- Nós armazenamos o tempo de início quando o progresso é atualizado pela primeira vez.
- Nós calculamos o tempo decorrido e o usamos para estimar o tempo total.
- Nós calculamos o tempo restante subtraindo o tempo decorrido do tempo total estimado.
Considerações Importantes:
- Precisão: Esta é uma previsão *muito* simplificada. Condições de rede, carga do servidor e outros fatores podem impactar significativamente a precisão. Técnicas mais sofisticadas, como fazer a média sobre múltiplos intervalos, podem melhorar a precisão.
- Feedback Visual: Indique claramente que o tempo é uma *estimativa*. Exibir intervalos (ex: "Tempo restante estimado: 5-10 segundos") pode ser mais realista.
- Casos Extremos: Lide com casos extremos onde o progresso é muito lento inicialmente. Evite dividir por zero ou exibir estimativas excessivamente grandes.
Combinando useFormStatus com a Estimativa de Progresso
Embora o useFormStatus em si não forneça informações de progresso, você pode usar sua propriedade pending para habilitar ou desabilitar o indicador de progresso. Por exemplo:
import React, { useState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (Lógica de estimativa de progresso dos exemplos anteriores)
function MyComponent() {
const [progress, setProgress] = useState(0);
const { pending } = useFormStatus();
const handleSubmit = async (formData) => {
// ... (Sua lógica de envio de formulário, incluindo atualizações no progresso)
};
return (
<form action={handleSubmit}>
<button type="submit" disabled={pending}>Submit</button>
{pending && <p>Progress: {progress}%</p>}
</form>
);
}
Neste exemplo, o indicador de progresso é exibido apenas enquanto o formulário está pendente (ou seja, enquanto useFormStatus.pending é true).
Melhores Práticas e Considerações
- Priorize a Precisão: Escolha uma técnica de estimativa de progresso que seja apropriada para o tipo de operação que está sendo realizada. SSE/WebSockets geralmente fornecem os resultados mais precisos, enquanto heurísticas podem ser suficientes para tarefas mais simples.
- Forneça Feedback Visual Claro: Use barras de progresso, spinners ou outras dicas visuais para indicar que uma operação está em andamento. Rotule claramente o indicador de progresso e, se aplicável, o tempo restante estimado.
- Lide com Erros de Forma Elegante: Se ocorrer um erro durante a operação, exiba uma mensagem de erro informativa ao usuário. Evite deixar o indicador de progresso travado em uma determinada porcentagem.
- Otimize o Desempenho: Evite realizar operações computacionalmente caras na thread da UI, pois isso pode impactar negativamente o desempenho. Use web workers ou outras técnicas para descarregar o trabalho para threads em segundo plano.
- Acessibilidade: Garanta que os indicadores de progresso sejam acessíveis para usuários com deficiência. Use atributos ARIA para fornecer informações semânticas sobre o progresso da operação. Por exemplo, use
aria-valuenow,aria-valueminearia-valuemaxem uma barra de progresso. - Localização: Ao exibir o tempo restante estimado, esteja atento aos diferentes formatos de hora e preferências regionais. Use uma biblioteca como
date-fnsoumoment.jspara formatar o tempo apropriadamente para a localidade do usuário. - Internacionalização: Mensagens de erro e outros textos devem ser internacionalizados para suportar múltiplos idiomas. Use uma biblioteca como
i18nextpara gerenciar as traduções.
Conclusão
Embora o hook useFormStatus do React não forneça diretamente capacidades de estimativa de progresso, você pode combiná-lo com outras técnicas para fornecer aos usuários feedback significativo durante os envios de formulário. Usando SSE/WebSockets, polling, atualizações otimistas ou dividindo ações em etapas menores, você pode criar uma experiência mais envolvente e amigável para o usuário. Lembre-se de priorizar a precisão, fornecer feedback visual claro, lidar com erros de forma elegante e otimizar o desempenho para garantir uma experiência positiva para todos os usuários, independentemente de sua localização ou contexto.