Form yang ramah offline di React Native: draft dan antrean submit
Aplikasi lapangan sering kehilangan sinyal. Menyimpan draft saat pengguna mengetik dan mengantrekan pengiriman mencegah form panjang hilang.
3 menit baca
Di halaman ini
Form panjang bertemu koneksi buruk
Aplikasi yang dipakai di lapangan, oleh agen penjualan atau surveyor, sering punya form panjang: data nasabah, dokumen, tanda tangan. Pengguna mengisinya di basement, di perjalanan, atau di area dengan sinyal lemah. Ada dua hal yang sering salah:
- Aplikasi ditutup paksa atau crash di tengah jalan, dan ketikan selama dua puluh menit hilang.
- Pengguna menekan Submit tanpa koneksi, dan request-nya gagal.
Artikel ini membahas dua blok kecil yang menyelesaikan sebagian besar masalah itu: draft yang
tersimpan otomatis dan antrean submit yang persisten. Library yang dipakai:
@react-native-async-storage/async-storage, @react-native-community/netinfo, dan
react-native-uuid.
1. Simpan draft otomatis
Simpan state form selagi pengguna mengetik, dengan debounce supaya storage tidak ditulis di setiap ketukan tombol:
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useEffect, useRef, useState } from "react";
export function useDraft<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(initial);
const [restored, setRestored] = useState(false);
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
// Restore once on mount.
useEffect(() => {
AsyncStorage.getItem(key)
.then((saved) => saved && setValue(JSON.parse(saved) as T))
.finally(() => setRestored(true));
}, [key]);
// Save 500 ms after the last change.
useEffect(() => {
if (!restored) return;
clearTimeout(timer.current);
timer.current = setTimeout(() => {
AsyncStorage.setItem(key, JSON.stringify(value));
}, 500);
return () => clearTimeout(timer.current);
}, [key, value, restored]);
const clear = () => AsyncStorage.removeItem(key);
return { value, setValue, restored, clear };
}Pakai satu key per record, misalnya draft:customer:new atau draft:application:${id}, dan hapus
setelah submit berhasil:
const draft = useDraft("draft:customer:new", { name: "", phone: "", city: "" });
if (!draft.restored) return <ActivityIndicator />;
<TextInput
value={draft.value.name}
onChangeText={(name) => draft.setValue((v) => ({ ...v, name }))}
/>;Menunggu restored sebelum menampilkan form mencegah field kosong sempat berkedip, dan yang lebih
penting, mencegah state awal yang kosong menimpa draft yang sudah tersimpan.
2. Antrean submit yang persisten
Saat pengguna menekan submit, jangan panggil API langsung dari layar. Masukkan request ke antrean yang disimpan di perangkat, lalu coba kirimkan:
import AsyncStorage from "@react-native-async-storage/async-storage";
import uuid from "react-native-uuid";
type Job = { id: string; url: string; body: unknown; attempts: number };
const QUEUE_KEY = "submit-queue";
async function readQueue(): Promise<Job[]> {
return JSON.parse((await AsyncStorage.getItem(QUEUE_KEY)) ?? "[]") as Job[];
}
async function writeQueue(jobs: Job[]) {
await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(jobs));
}
export async function enqueue(url: string, body: unknown) {
// The id doubles as the Idempotency-Key, so a retry never creates a duplicate on the server.
const job: Job = { id: uuid.v4() as string, url, body, attempts: 0 };
await writeQueue([...(await readQueue()), job]);
void flush();
}3. Kirim saat online
import NetInfo from "@react-native-community/netinfo";
let flushing = false;
export async function flush() {
if (flushing) return;
flushing = true;
try {
for (const job of await readQueue()) {
const state = await NetInfo.fetch();
if (!state.isConnected) break;
const response = await fetch(job.url, {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": job.id },
body: JSON.stringify(job.body),
}).catch(() => null);
const remaining = (await readQueue()).filter((j) => j.id !== job.id);
if (response?.ok || (response && response.status >= 400 && response.status < 500)) {
// Done, or rejected for good: remove it. Surface 4xx errors to the user separately.
await writeQueue(remaining);
} else {
// Network or server error: keep it and try again later.
await writeQueue([...remaining, { ...job, attempts: job.attempts + 1 }]);
break;
}
}
} finally {
flushing = false;
}
}
// Retry whenever the device comes back online.
NetInfo.addEventListener((state) => {
if (state.isConnected) void flush();
});Panggil juga flush() ketika aplikasi kembali ke foreground, memakai AppState.
Beri tahu pengguna apa yang terjadi
Antrean offline hanya bisa dipercaya kalau terlihat. Tampilkan banner kecil seperti "2 data menunggu koneksi", dan tandai record yang masih antre di daftar sebagai Pending. Pengguna berhenti mengisi ulang data yang sama ketika mereka bisa melihat datanya sudah tersimpan.
Yang tidak boleh disimpan
AsyncStorage tidak terenkripsi. Jangan simpan token, password, atau dokumen identitas yang sensitif di
draft atau antrean sebagai teks biasa. Pakai keychain atau keystore bawaan platform (misalnya
react-native-keychain) untuk rahasia, dan storage terenkripsi kalau draft berisi data pribadi yang
sensitif. Hapus draft saat logout.
Ringkasan
- Simpan draft otomatis dengan hook ber-debounce, dan pulihkan sebelum form ditampilkan.
- Kirim lewat antrean persisten, bukan memanggil API langsung dari layar.
- Kirim ulang ketika koneksi kembali, dengan idempotency key per job.
- Buat pekerjaan yang tertunda terlihat, dan jauhkan data sensitif dari storage biasa.