เกิดอะไรขึ้น
ช่องค้นหาของเรา debounce อยู่แล้ว แต่คนที่พิมพ์เร็วก็ยังเห็นผลลัพธ์กะพริบ บางครั้ง list แสดงผลของ query ก่อนหน้า หลังจากผลที่ถูกต้องขึ้นมาแล้วไม่นาน
debounce ทำงานถูกต้องตามที่ตั้งใจ ปัญหาอยู่ที่อื่น คือมีสอง request วิ่งอยู่พร้อมกัน และเสร็จไม่ตรงลำดับที่ส่ง
ทำไมถึงเป็นแบบนั้น
debounce ลดแค่จำนวน request ที่ส่ง ไม่ได้บอกอะไรเรื่องลำดับที่มันกลับมา ถ้าผู้ใช้หยุดพิมพ์หลัง "re" แล้วพิมพ์ต่อเป็น "react" ทั้งสอง request จะอยู่ในอากาศพร้อมกัน และ network หรือ server อาจตอบอันเก่าเป็นอันสุดท้ายก็ได้
response ไหนมาถึงทีหลังสุดจะชนะตอน set state ถ้าเป็นอันเก่า มันก็เขียนทับผลที่ใหม่กว่า นี่คือ race condition แบบคลาสสิก และ server ก็อาจยังทำงานกับ query ที่ไม่มีใครรอแล้ว
ตอนนี้เราทำอย่างไร
เราสร้าง AbortController ต่อหนึ่ง request และยกเลิกอันก่อนหน้าก่อนเริ่มอันใหม่ fetch ที่ถูกยกเลิกจะ reject ด้วย AbortError ซึ่งเราเมินไป ทำให้มีแค่ request ล่าสุดที่อัปเดต UI ได้
การยกเลิกยังช่วย backend ได้ถ้ามันสังเกตเห็นว่า connection ปิดแล้ว แต่จะประหยัดได้แค่ไหนขึ้นกับว่า server หยุดทำงานเมื่อ client ตัดการเชื่อมต่อหรือไม่ ต่อให้ไม่หยุด UI ก็ยังปลอดภัยจากข้อมูลเก่า
let controller: AbortController | undefined
async function search(query: string) {
controller?.abort()
controller = new AbortController()
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
})
return await res.json()
} catch (err) {
if ((err as Error).name === 'AbortError') return undefined
throw err
}
}เช็กลิสต์สั้น ๆ
- 1เก็บ ref ของ AbortController ตัวปัจจุบันไว้
- 2abort มันก่อนเริ่ม request ใหม่
- 3ส่ง signal ของมันให้ fetch
- 4เมิน error ที่ error.name เป็น AbortError
- 5abort ตอน unmount ด้วย response ที่มาช้าจะได้ไม่ set state
จำไว้สั้น ๆ
debounce จำกัดจำนวน request ที่ส่ง ส่วนการยกเลิกทำให้มีแค่อันล่าสุดที่ชนะได้