Pelajari cara mengimplementasikan estimasi progres dan prediksi waktu penyelesaian menggunakan hook useFormStatus React, meningkatkan pengalaman pengguna dalam aplikasi padat data.
Estimasi Progres useFormStatus React: Prediksi Waktu Penyelesaian
Hook useFormStatus dari React, yang diperkenalkan di React 18, memberikan informasi berharga tentang status pengiriman formulir. Meskipun tidak secara langsung menawarkan estimasi progres, kita dapat memanfaatkan propertinya dan teknik lain untuk memberikan umpan balik yang berarti kepada pengguna selama pengiriman formulir yang berpotensi berjalan lama. Postingan ini mengeksplorasi metode untuk mengestimasi progres dan memprediksi waktu penyelesaian saat menggunakan useFormStatus, menghasilkan pengalaman yang lebih menarik dan ramah pengguna.
Memahami useFormStatus
Sebelum mendalami estimasi progres, mari kita rekap secara singkat tujuan dari useFormStatus. Hook ini dirancang untuk digunakan di dalam elemen <form> yang memanfaatkan prop action. Ini mengembalikan objek yang berisi properti berikut:
pending: Sebuah boolean yang menunjukkan apakah formulir sedang dalam proses pengiriman.data: Data yang dikirimkan bersama formulir (jika pengiriman berhasil).method: Metode HTTP yang digunakan untuk pengiriman formulir (misalnya, 'POST', 'GET').action: Fungsi yang diteruskan ke propactionformulir.error: Objek error jika pengiriman gagal.
Meskipun useFormStatus memberi tahu kita jika formulir sedang dikirim, ia tidak memberikan informasi langsung apa pun tentang progres pengiriman, terutama jika fungsi action melibatkan operasi yang kompleks atau panjang.
Tantangan Estimasi Progres
Tantangan utamanya terletak pada kenyataan bahwa eksekusi fungsi action tidak transparan bagi React. Kita tidak secara inheren tahu seberapa jauh prosesnya. Hal ini terutama berlaku untuk operasi di sisi server. Namun, kita dapat menggunakan berbagai strategi untuk mengatasi keterbatasan ini.
Strategi untuk Estimasi Progres
Berikut adalah beberapa pendekatan yang dapat Anda ambil, masing-masing dengan kelebihan dan kekurangannya sendiri:
1. Server-Sent Events (SSE) atau WebSockets
Solusi yang paling tangguh sering kali adalah mendorong pembaruan progres dari server ke klien. Ini dapat dicapai dengan menggunakan:
- Server-Sent Events (SSE): Protokol satu arah (server-ke-klien) yang memungkinkan server mendorong pembaruan ke klien melalui satu koneksi HTTP. SSE ideal ketika klien hanya perlu *menerima* pembaruan.
- WebSockets: Protokol komunikasi dua arah yang menyediakan koneksi persisten antara klien dan server. WebSockets cocok untuk pembaruan waktu nyata di kedua arah.
Contoh (SSE):
Sisi 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); // Simulasi pembaruan progres setiap 500ms
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
Sisi Klien (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>Progres: {progress}%</p>
</div>
);
}
export default MyComponent;
Penjelasan:
- Server mengatur header yang sesuai untuk SSE.
- Server mengirim pembaruan progres sebagai event
data:. Setiap event adalah objek JSON yang berisiprogressdan flagcompleted. - Komponen React menggunakan
EventSourceuntuk mendengarkan event ini. - Komponen memperbarui state (
progress) berdasarkan event yang diterima.
Kelebihan: Pembaruan progres yang akurat, umpan balik waktu nyata.
Kekurangan: Memerlukan perubahan di sisi server, implementasi lebih kompleks.
2. Polling dengan Endpoint API
Jika Anda tidak dapat menggunakan SSE atau WebSockets, Anda dapat mengimplementasikan polling. Klien secara berkala mengirim permintaan ke server untuk memeriksa status operasi.
Contoh:
Sisi Server (Node.js):
const express = require('express');
const app = express();
// Simulasi tugas yang berjalan lama
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); // Hasilkan ID tugas yang unik
// Simulasi pemrosesan di latar belakang
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');
});
Sisi Klien (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); // Lakukan polling setiap 1 detik
return () => clearInterval(interval);
}, [taskId]);
return (
<div>
<button onClick={startTask} disabled={taskId !== null}>Mulai Tugas</button>
{taskId && <p>Progres: {progress}%</p>}
</div>
);
}
export default MyComponent;
Penjelasan:
- Klien memulai tugas dengan memanggil
/start-task, dan menerima sebuahtaskId. - Klien kemudian melakukan polling ke
/task-status/:taskIdsecara berkala untuk mendapatkan progres.
Kelebihan: Relatif mudah diimplementasikan, tidak memerlukan koneksi persisten.
Kekurangan: Bisa kurang akurat dibandingkan SSE/WebSockets, menimbulkan latensi karena interval polling, membebani server karena permintaan yang sering.
3. Pembaruan Optimistis dan Heuristik
Dalam beberapa kasus, Anda dapat menggunakan pembaruan optimistis yang dikombinasikan dengan heuristik untuk memberikan estimasi yang wajar. Misalnya, jika Anda mengunggah file, Anda dapat melacak jumlah byte yang diunggah di sisi klien dan mengestimasi progres berdasarkan ukuran total file.
Contoh (Unggah 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'); // Ganti dengan endpoint unggahan Anda
xhr.send(formData);
xhr.onload = () => {
if (xhr.status === 200) {
console.log('Unggahan selesai!');
} else {
console.error('Unggahan gagal:', xhr.status);
}
};
xhr.onerror = () => {
console.error('Unggahan gagal');
};
} catch (error) {
console.error('Error unggahan:', error);
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input type="file" onChange={handleFileChange} />
<button type="submit" disabled={!file}>Unggah</button>
</form>
<p>Progres: {progress}%</p>
</div>
);
}
export default MyComponent;
Penjelasan:
- Komponen menggunakan objek
XMLHttpRequestuntuk mengunggah file. - Event listener
progresspadaxhr.uploaddigunakan untuk melacak progres unggahan. - Properti
loadeddantotaldari event digunakan untuk menghitung persentase penyelesaian.
Kelebihan: Hanya di sisi klien, dapat memberikan umpan balik segera.
Kekurangan: Akurasi bergantung pada keandalan heuristik, mungkin tidak cocok untuk semua jenis operasi.
4. Memecah Aksi menjadi Langkah-langkah Lebih Kecil
Jika fungsi action melakukan beberapa langkah yang berbeda, Anda dapat memperbarui UI setelah setiap langkah untuk menunjukkan progres. Ini memerlukan modifikasi fungsi action untuk menyediakan pembaruan.
Contoh:
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">Kirim</button>
</form>
<p>Progres: {progress}%</p>
</div>
);
}
export default MyComponent;
Penjelasan:
- Fungsi
myActionmenerima sebuah callbacksetProgress. - Fungsi ini memperbarui state progres di berbagai titik selama eksekusinya.
Kelebihan: Kontrol langsung atas pembaruan progres.
Kekurangan: Memerlukan modifikasi fungsi action, bisa lebih kompleks untuk diimplementasikan jika langkah-langkahnya tidak mudah dibagi.
Memprediksi Waktu Penyelesaian
Setelah Anda memiliki pembaruan progres, Anda dapat menggunakannya untuk memprediksi sisa waktu yang diperkirakan. Pendekatan sederhana adalah dengan melacak waktu yang dibutuhkan untuk mencapai tingkat progres tertentu dan mengekstrapolasinya untuk mengestimasi total waktu.
Contoh (Sederhana):
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)); // Pastikan tidak negatif
}
}, [progress]);
// ... (sisa komponen dan pembaruan progres seperti yang dijelaskan di bagian sebelumnya)
return (
<div>
<p>Progres: {progress}%</p>
{estimatedTimeRemaining !== null && (
<p>Estimasi Sisa Waktu: {Math.round(estimatedTimeRemaining / 1000)} detik</p>
)}
</div>
);
}
export default MyComponent;
Penjelasan:
- Kita menyimpan waktu mulai saat progres pertama kali diperbarui.
- Kita menghitung waktu yang telah berlalu dan menggunakannya untuk mengestimasi total waktu.
- Kita menghitung sisa waktu dengan mengurangi waktu yang telah berlalu dari estimasi total waktu.
Pertimbangan Penting:
- Akurasi: Ini adalah prediksi yang *sangat* disederhanakan. Kondisi jaringan, beban server, dan faktor lainnya dapat secara signifikan memengaruhi akurasi. Teknik yang lebih canggih, seperti merata-ratakan selama beberapa interval, dapat meningkatkan akurasi.
- Umpan Balik Visual: Tunjukkan dengan jelas bahwa waktu tersebut adalah *estimasi*. Menampilkan rentang (misalnya, "Estimasi sisa waktu: 5-10 detik") bisa lebih realistis.
- Kasus Ekstrem: Tangani kasus ekstrem di mana progres sangat lambat pada awalnya. Hindari pembagian dengan nol atau menampilkan estimasi yang terlalu besar.
Menggabungkan useFormStatus dengan Estimasi Progres
Meskipun useFormStatus sendiri tidak menyediakan informasi progres, Anda dapat menggunakan properti pending-nya untuk mengaktifkan atau menonaktifkan indikator progres. Sebagai contoh:
import React, { useState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (Logika estimasi progres dari contoh sebelumnya)
function MyComponent() {
const [progress, setProgress] = useState(0);
const { pending } = useFormStatus();
const handleSubmit = async (formData) => {
// ... (Logika pengiriman formulir Anda, termasuk pembaruan ke progres)
};
return (
<form action={handleSubmit}>
<button type="submit" disabled={pending}>Kirim</button>
{pending && <p>Progres: {progress}%</p>}
</form>
);
}
Dalam contoh ini, indikator progres hanya ditampilkan saat formulir sedang tertunda (yaitu, saat useFormStatus.pending bernilai true).
Praktik Terbaik dan Pertimbangan
- Prioritaskan Akurasi: Pilih teknik estimasi progres yang sesuai untuk jenis operasi yang dilakukan. SSE/WebSockets umumnya memberikan hasil yang paling akurat, sementara heuristik mungkin cukup untuk tugas yang lebih sederhana.
- Berikan Umpan Balik Visual yang Jelas: Gunakan bilah progres, spinner, atau isyarat visual lainnya untuk menunjukkan bahwa suatu operasi sedang berlangsung. Beri label yang jelas pada indikator progres dan, jika berlaku, estimasi sisa waktu.
- Tangani Error dengan Baik: Jika terjadi error selama operasi, tampilkan pesan error yang informatif kepada pengguna. Hindari membiarkan indikator progres macet pada persentase tertentu.
- Optimalkan Kinerja: Hindari melakukan operasi yang memakan banyak komputasi di thread UI, karena ini dapat berdampak negatif pada kinerja. Gunakan web worker atau teknik lain untuk memindahkan pekerjaan ke thread latar belakang.
- Aksesibilitas: Pastikan indikator progres dapat diakses oleh pengguna dengan disabilitas. Gunakan atribut ARIA untuk memberikan informasi semantik tentang progres operasi. Misalnya, gunakan
aria-valuenow,aria-valuemin, danaria-valuemaxpada bilah progres. - Lokalisasi: Saat menampilkan estimasi sisa waktu, perhatikan format waktu yang berbeda dan preferensi regional. Gunakan pustaka seperti
date-fnsataumoment.jsuntuk memformat waktu dengan tepat sesuai dengan lokal pengguna. - Internasionalisasi: Pesan error dan teks lainnya harus diinternasionalkan untuk mendukung berbagai bahasa. Gunakan pustaka seperti
i18nextuntuk mengelola terjemahan.
Kesimpulan
Meskipun hook useFormStatus dari React tidak secara langsung menyediakan kemampuan estimasi progres, Anda dapat menggabungkannya dengan teknik lain untuk memberikan umpan balik yang berarti kepada pengguna selama pengiriman formulir. Dengan menggunakan SSE/WebSockets, polling, pembaruan optimistis, atau memecah aksi menjadi langkah-langkah yang lebih kecil, Anda dapat menciptakan pengalaman yang lebih menarik dan ramah pengguna. Ingatlah untuk memprioritaskan akurasi, memberikan umpan balik visual yang jelas, menangani error dengan baik, dan mengoptimalkan kinerja untuk memastikan pengalaman positif bagi semua pengguna, terlepas dari lokasi atau latar belakang mereka.