Dialog Cari Jadi Thread: Satu Array Ganti Tujuh useState
Jawaban AI pertama lenyap tiap nanya lagi. Perbaikannya cuma satu array ThreadEntry plus functional update, bukan library state management.
Habis nanya satu pertanyaan ke Tanya AI di dialog cari, jawabannya muncul bagus. Terus saya mau nanya lagi. Begitu kirim pertanyaan kedua, jawaban pertama lenyap. Diganti total sama pertanyaan baru dan jawaban barunya. Kayak chat yang tiap elo ketik, history-nya di-wipe.
Bukan bug network. Bukan streaming-nya yang gagal. Masalahnya di desain saya sendiri: dialog itu punya satu view state. Satu pertanyaan, satu jawaban, selesai. Pas ada pertanyaan baru, view-nya ditimpa. Simpel, tapi nggak nyata kalau orang mau nanya lanjutan kayak “terus gimana cara deploy-nya?”
Dugaan pertama saya salah
Insting pertama: berarti butuh state terpisah buat history. Array messages, mungkin. Atau lebih “benar”-nya, pasang library state management, sebab ini kan udah mulai kayak chat app beneran. Saya sempat buka tab baru, mulai banding-bandingin Zustand sama Jotai.
Untung nggak jadi.
Pas duduk dan gambar dulu data flow-nya, kelihatan bahwa yang saya butuhkan sebenernya cuma satu array. Yang ribet bukan nyimpan datanya, tapi urutan bersih-bersih pas user nanya lagi: abort stream yang masih jalan, tandai jawaban lama selesai dengan teks parsialnya, baru append pasangan pertanyaan-jawaban baru. Itu urutan eksekusi, bukan soal library.
Satu array, dua bentuk entry
Strukturnya sekarang: satu state entries, isinya ThreadEntry yang bisa berupa user pill atau AssistantEntry. User pill itu bubble kecil pertanyaan, AssistantEntry jawaban AI yang bisa masih streaming atau udah selesai. Urutan render thread = urutan array. Gampang.
Tujuh useState lama (view, question, answer, streaming, askSources, followups, askError) tinggal jadi satu. Yang tadinya saya jaga tujuh nilai yang harus sinkron satu sama lain, sekarang jadi satu sumber kebenaran.
Bagian yang menurut saya paling penting ada di cara nge-patch. Event SSE datang cepat, delta demi delta. Kalau tiap delta saya baca state langsung terus set ulang, ada celah balapan antar update. Jadi tiap event dipatch in-place per id lewat patchEntry yang pakai functional update:
setEntries(prev => prev.map(e =>
e.id === id ? { ...e, answer: e.answer + delta } : e
))
Ini persis pola yang didokumentasikan React: updater function nerima pending state sebagai satu-satunya argumen dan balikin next state, terus React masukin updater itu ke antrean buat re-render [2]. Jadi walau dua delta nyampe nyaris bareng, keduanya baca state terbaru, bukan snapshot basi.
Bersih-bersih sebelum nanya lagi
Urutan pas user kirim pertanyaan baru, dan ini yang bikin saya sempat bingung:
Pertama, abort stream yang masih jalan. AbortController-nya di-abort, dan ini bukan cuma mutusin request, tapi juga ngehentikan konsumsi response body dan stream-nya [1]. Kedua, entry jawaban lama ditandai streaming false, tapi teksnya yang udah ke-parsial tetep dipertahankan. Ketiga, baru append pasangan user pill dan AssistantEntry kosong buat pertanyaan baru.
Hasilnya: jawaban pertama nggak lenyap. Dia duduk di atas, setengah jadi kalau keburu di-abort, lengkap kalau sempat selesai. Persis perilaku chat app yang orang harapkan.
Error handling juga turun ke level entry, bukan level dialog. Error 429 atau stream yang putus ditampilin inline di entry yang terkait, jadi error lama nggak nimpa error baru. Khusus 501, pasangannya user plus assistant dibuang dan baris ask di-disable, sebab itu artinya backend-nya nggak siap, nanya terus nggak ada gunanya.
Satu detail kecil yang saya suka: ngetik di input nggak meng-abort apa pun. Search results dan thread hidup berdampingan. User bisa sambil ngetik pertanyaan lanjutan sambil jawaban masih nyiram teks.
Terus auto-scroll. Sesederhana ngeset scrollTop menyamai scrollHeight tiap entries berubah. Dua baris, nggak pakai library scroll animasi. Iya, dia lompat, nggak glide mulus. Saya pilih terima lompatan ini daripada nambah dependency buat animasi yang kalau dilihat dua detik juga udah kelupaan.
Kalau dipikir balik, thread chat itu memang secara alami ordered list of turns. Bukan graf, bukan state mesin yang kompleks. Jadi masuk akal kalau representasinya satu array dan semua operasinya cuma tiga: append pasangan baru, patch entry by id, tandai selesai. Kebanyakan orang (termasuk saya kemarin) langsung loncat ke “ini butuh state management proper” padahal belum ngecek dulu apakah state-nya sendiri udah cukup.
Dialognya sekarang rasanya kayak ngobrol, bukan kayak formulir yang tiap submit nge-reset dirinya. Dan diff-nya lebih pendek dari yang saya bayangkan waktu masih mau download library.
Kalau mau baca fondasinya dulu, saya pernah tulis soal jawaban AI streaming lewat fetch dan parser SSE manual. Search-nya sendiri ada di artikel soal related semantic, TL;DR, dan search live yang jadi rumah dialog ini.