Langsung ke konten
~/rifqi_abrory

Halaman

Beranda
Tentang
Stack
Pengalaman
Proyek
Tulisan
Kontak
Semua proyek

Proyek

Avrist PINTAR — Agent Portal
Avrist e-Application
Avrist Academy — LMS CMS
Avrist e-Payment
PRUHub
SIYAP

Artikel

Angular signals untuk halaman admin sehari-hari
Typed reactive forms di Angular, tanpa any
Error API yang konsisten di ASP.NET Core dengan ProblemDetails
Meneruskan CancellationToken sampai ke lapisan terbawah di ASP.NET Core
Graceful shutdown untuk service HTTP di Go
Idempotency key: membuat API pembayaran aman untuk di-retry
Form kontak yang tahan spam dengan Next.js Server Actions
Form yang ramah offline di React Native: draft dan antrean submit
Validasi request dan penanganan error di Spring Boot 3
Keyset pagination di SQL Server: ketika OFFSET mulai lambat

Aksi

Ganti ke mode gelap
Read in English
Salin alamat email
Semua artikel

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:

  1. Aplikasi ditutup paksa atau crash di tengah jalan, dan ketikan selama dua puluh menit hilang.
  2. 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.