Skip to content

Mirror Peta Transisi Status di Frontend

Adityo Guni Waluyo

Peta transisi status di frontend bikin opsi mustahil mati sebelum diklik. Backend tetap satu-satunya penjaga; kodenya cuma union type dan satu map.

Ringkasan

Awalnya flowStageOf isinya ternary berlapis dan tombol admin masih nawarin status yang udah gak ada sampai backend nolak pakai 409. Akhirnya dibikin union type buat enam status plus map NEXT_STATUSES biar UI cuma nyalain opsi yang boleh, sisanya biar backend yang mutusin. Ternary diganti switch, label dipisah, aria-disabled ternyata cuma semantik jadi validasi tetap dobel di handler sama backend.

Saya buka layanan-flow.ts sore itu, niatnya nambah satu status baru dari backend. Yang saya temui: fungsi flowStageOf berisi nested ternary tiga lapis, satu cabang untuk tiap status lama. Di layar sebelahnya, form admin masih riang menawarkan tombol transisi ke status yang udah nggak ada. Ditekan pun, yang jawab bukan UI kita melainkan backend, dengan 409.

Dugaan pertama: salin saja seluruh aturan transisi backend ke frontend. Duplikat penuh, biar nggak ada yang kelewat. Semakin lama saya pikirkan, semakin nggak enak: sekarang ada dua tempat yang sama-sama mengaku sebagai aturan.

Yang akhirnya saya tulis jauh lebih ramping. Union type LayananStatus menyebut enam status sebagai string, dari pengajuan sampai selesai: verifikasi dan diproses di tengah, disetujui, ditolak, dan selesai di ujung. Lalu satu map NEXT_STATUSES yang cuma menjawab satu pertanyaan: dari status ini, opsi mana yang boleh menyala di UI. Sekian. Keputusan transisi yang sah tetap milik backend; kalau ada yang nekat, dia yang mengeluarkan 409.

Bukan duplikasi logika

Panduan gaya Redux merangkum alasannya dalam satu kalimat: simpan state seminimal mungkin dan turunkan sisanya saat dibutuhkan, karena satu sumber kebenaran bikin aliran data bisa diprediksi [2]. Map di frontend saya jelas bukan sumber kebenaran. Dia komponen turunan yang dibaca saat render untuk menurunkan opsi aktif, bukan disalin ke state terpisah [4] — dan dibiarkan mati kalau backend berubah. Risiko yang saya terima: sinkronisasi manual. Status baru memang harus lewat file ini dulu, dan justru lewat pintu itu review kode jadi tempat cek yang alami.

Ternary jadi switch, string liar jadi union

flowStageOf sekarang switch biasa yang memetakan enam status ke empat tahap tampilan bertipe FlowStage: pengajuan, verifikasi, diproses, dan decision. Type guard isLayananStatus membuat TypeScript melakukan narrowing dengan aman [1]; string aneh dari luar nggak akan kebagian label mana pun. Label rapi per status tersimpan terpisah di map label, jadi revisi copywriting nggak menyentuh logika. Ternary tiga lapisnya? Pensiun tanpa upacara.

Satu bagian lama sengaja belum saya hapus. Konstanta catatan otomatis untuk tiket yang masih menunggu masih dipakai dua komponen yang penulisannya ulang di tugas berikutnya, jadi dia tinggal sementara di file yang sama, lengkap dengan catatan kapan boleh dihapus. Saya makin yakin sama pola begini: migrasi tipe yang jujur itu bertahap. Yang penting setiap sisa masa transisi ninggalin jejak tertulis, bukan cuma modal ingatan orang yang besok bisa cuti.

Satu detail kecil yang bikin komponen ini awet: fungsi penampil label menerima string apa pun. Nilai yang dikenali langsung dapat label rapi, sisanya ditampilkan mentah apa adanya. Halaman nggak pernah blank cuma karena backend mengirim status yang belum sempat didaftarkan di frontend.

Atribut yang nggak menolak klik

Sisi yang saya salah paham cukup lama: aria-disabled. Saya kira begitu dipasang, tombolnya otomatis aman. Ternyata atribut itu sifatnya semantik; yang mematikan fungsinya tetap kode kita sendiri [3]. Urutan kerjanya jadi begini: NEXT_STATUSES mematikan opsi mustahil di mata user, handler tetap mengecek ulang sebelum kirim, dan backend menolak yang berhasil menyelip. Tiga lapis, dan yang paling awet ya lapis paling belakang itu.

Jangka menengah saya mau melepas sinkron manual ini ke satu sumber: tipe dari kontrak. Di repo yang sama, skema OpenAPI sudah di-generate jadi tipe [6] untuk endpoint-nya, dan lintasan yang sama bisa dipakai untuk vocabulary status. Soal data waktu aja, string yang formatnya ngawur langsung berakhir jadi NaN [5] — apalagi seluruh keputusan UI di komponen ini menggantung pada satu string status. Makanya di commit ini saya memilih eksplisit: enam status yang disebut nama, satu map yang bisa dibaca mata, tanpa ilusi bahwa frontend ikut memutuskan.

Sources

Artikel terkait