การอัปเดต UI แบบ optimistic ใน React ทำให้แอปรู้สึกทันที เรียนรู้รูปแบบปลอดภัยในการประสานความจริงจากเซิร์ฟเวอร์ จัดการความล้มเหลว และป้องกันการคลาดเคลื่อนของข้อมูล

Optimistic UI ใน React หมายถึงการอัปเดตหน้าจอ alsof ว่าการเปลี่ยนแปลงสำเร็จแล้ว ก่อนที่เซิร์ฟเวอร์จะยืนยัน ผู้ใช้กด Like แล้วตัวนับกระโดดทันที และคำขอทำงานอยู่เบื้องหลัง
ฟีดแบ็กที่ทันทีนี้ทำให้แอปดูรวดเร็ว ในเครือข่ายช้า มันมักเป็นความต่างระหว่าง "รู้สึกตอบสนอง" กับ "มันทำงานไหม?"
ข้อแลกเปลี่ยนคือการคลาดเคลื่อนของข้อมูล (data drift): สิ่งที่ผู้ใช้เห็นอาจเริ่มไม่ตรงกับความจริงบนเซิร์ฟเวอร์ ความคลาดเคลื่อนมักแสดงเป็นความไม่สอดคล้องเล็ก ๆ ที่น่าหงุดหงิด ขึ้นกับจังหวะเวลาและยากที่จะทำซ้ำ
ผู้ใช้จะสังเกตเห็นการคลาดเคลื่อนเมื่อสิ่งต่าง ๆ "เปลี่ยนใจ" ในภายหลัง: ตัวนับกระโดดแล้วเด้งกลับ รายการปรากฏแล้วหายไปหลังรีเฟรช การแก้ไขดูเหมือนจะติดจนกว่าจะกลับมาที่หน้าอื่น หรือสองแท็บแสดงค่าสองอย่างต่างกัน
สิ่งนี้เกิดขึ้นเพราะ UI กำลังเดาค่า และเซิร์ฟเวอร์อาจตอบด้วยความจริงที่ต่างออกไป กฎการตรวจสอบ ความพยายามป้องกันการซ้ำ สิทธิ์ การจำกัดอัตรา หรืออุปกรณ์อื่นที่แก้ไขเรคคอร์ดเดียวกัน ล้วนเปลี่ยนผลลัพธ์สุดท้ายได้ อีกสาเหตุทั่วไปคือคำขอทับซ้อนกัน: คำตอบเก่ากลับมาล่าช้าและเขียนทับการกระทำใหม่ของผู้ใช้
ตัวอย่าง: คุณเปลี่ยนชื่อโปรเจกต์เป็น "Q1 Plan" และแสดงทันทีในเฮดเดอร์ เซิร์ฟเวอร์อาจตัดช่องว่าง ปฏิเสธตัวอักษรบางอย่าง หรือสร้าง slug ถ้าคุณไม่แทนที่ค่าที่เดาไว้ด้วยค่าจริงจากเซิร์ฟเวอร์ UI จะดูถูกต้องจนกระทั่งรีเฟรชครั้งถัดไป ซึ่งจะ "เปลี่ยนอย่างลึกลับ"
Optimistic UI ไม่ใช่ตัวเลือกที่เหมาะกับทุกกรณี ระมัดระวัง (หรือหลีกเลี่ยง) กับเรื่องเงินและการเรียกเก็บเงิน การกระทำที่ไม่สามารถย้อนกลับได้ การเปลี่ยนแปลงสิทธิ์ เวิร์กโฟลว์ที่มีกฎซับซ้อนจากเซิร์ฟเวอร์ หรือสิ่งที่มีผลข้างเคียงที่ผู้ใช้ต้องยืนยันเป็นพิเศษ
ถ้าใช้ดี Optimistic updates ทำให้แอปรู้สึกทันที แต่ต้องวางแผนเรื่องการประสาน ความเรียงลำดับ และการจัดการความล้มเหลว
Optimistic UI ทำงานได้ดีที่สุดเมื่อคุณแยกสองประเภทของสถานะ:
ความคลาดเคลื่อนส่วนใหญ่เริ่มเมื่อค่าที่เดาจากฝั่งท้องถิ่นถูกปฏิบัติราวกับเป็นความจริงที่ยืนยันแล้ว
กฎง่าย ๆ: ถ้าค่ามีความหมายทางธุรกิจนอกหน้าจอนี้ ให้ถือว่าเซิร์ฟเวอร์เป็นแหล่งความจริง ถ้ามันส่งผลแค่การแสดงผลบนหน้าจอ (เปิด/ปิด ช่องที่กำลังแก้ไข ข้อความร่าง) ให้เก็บท้องถิ่น
ในทางปฏิบัติ ให้เก็บความจริงจากเซิร์ฟเวอร์สำหรับสิ่งอย่างสิทธิ์ ราคา ยอดเงิน สต็อก ฟิลด์ที่คำนวณหรือถูกตรวจสอบ และทุกอย่างที่อาจเปลี่ยนจากที่อื่น (แท็บอื่น ผู้ใช้อื่น) เก็บสถานะ UI ท้องถิ่นสำหรับร่าง แฟล็ก "กำลังแก้ไข" ตัวกรองชั่วคราว แถวที่ขยาย และตัวสลับแอนิเมชัน
บางการกระทำ "ปลอดภัยที่จะเดา" เพราะเซิร์ฟเวอร์มักยอมรับและง่ายต่อการย้อนกลับ เช่น การกดดาวไอเท็มหรือสวิตช์การตั้งค่าพื้นฐาน
เมื่อฟิลด์ไม่ปลอดภัยที่จะเดา คุณยังทำให้แอปรู้สึกเร็วได้โดยไม่ทำเหมือนการเปลี่ยนแปลงเป็นค่าถาวร เก็บค่าที่ยืนยันล่าสุด และแสดงสัญญาณรอดำเนินการอย่างชัดเจน
ตัวอย่าง: บนหน้าจอ CRM ที่คุณคลิก "Mark as paid" เซิร์ฟเวอร์อาจปฏิเสธ (สิทธิ์ การตรวจสอบ ย้อนเงินแล้ว) แทนที่จะเขียนทุกตัวเลขใหม่ทันที ให้เปลี่ยนสถานะพร้อมป้าย "Saving..." เล็ก ๆ ปิดใช้งานการกระทำชั่วคราว (หรือเปลี่ยนเป็น Undo) จนกว่าคำขอจะเสร็จ และอัปเดตยอดรวมเมื่อได้รับการยืนยันเท่านั้น
รูปแบบที่ดีต้องเรียบง่ายและสม่ำเสมอ: ป้าย "Saving..." ใกล้ไอเท็มที่เปลี่ยน ปิดการกระทำชั่วคราว (หรือเปลี่ยนเป็น Undo) จนกว่าคำขอจะเสร็จ หรือทำให้ค่าที่เดาดูเป็นชั่วคราว (ข้อความจางเล็กน้อยหรือสปินเนอร์เล็ก ๆ)
ถ้าคำตอบจากเซิร์ฟเวอร์อาจกระทบหลายจุด (ยอดรวม การเรียง ฟิลด์ที่คำนวณ สิทธิ์) การรีเฟตช์มักปลอดภัยกว่าพยายามแพตช์ทุกอย่าง หากเป็นการเปลี่ยนแปลงเล็ก ๆ ที่เฉพาะเจาะจง (เปลี่ยนชื่อโน้ต สลับแฟล็ก) การแพตช์ท้องถิ่นมักพอเพียง
กฎที่เป็นประโยชน์: แพตช์เฉพาะสิ่งที่ผู้ใช้เปลี่ยน แล้วรีเฟตช์ข้อมูลที่เป็นผลสรุป การคำนวณ หรือแชร์ข้ามหน้าจอ
Optimistic UI ทำงานเมื่อโมเดลข้อมูลของคุณติดตามสิ่งที่ยืนยันแล้วเทียบกับสิ่งที่ยังเป็นการเดาได้อย่างชัดเจน หากคุณออกแบบช่องว่างนั้นอย่างชัดเจน ช่วงเวลาที่ผู้ใช้สงสัยว่า "ทำไมมันกลับไป" จะเกิดน้อยลง
สำหรับไอเท็มที่สร้างใหม่ ให้กำหนดไอดีชั่วคราวฝั่งไคลเอนต์ (เช่น temp_12345 หรือ UUID) แล้วสลับเป็นไอดีจริงเมื่อคำตอบมาถึง วิธีนี้ทำให้รายการ การเลือก และสถานะการแก้ไขประสานได้เรียบร้อย
ตัวอย่าง: ผู้ใช้เพิ่มงาน คุณเร็นเดอร์ทันทีด้วย id: "temp_a1" เมื่อเซิร์ฟเวอร์ตอบกลับด้วย id: 981 ให้แทนที่ไอดีที่จุดเดียว แล้วทุกอย่างที่ใช้ key เป็นไอดียังคงทำงาน
แฟล็กโหลดระดับหน้าจอเดียวมักหยาบเกินไป ติดตามสถานะที่ไอเท็ม (หรือแม้แต่ฟิลด์) ที่เปลี่ยน วิธีนี้คุณจะแสดง UI รอได้อย่างละเอียด รีเทิร์นเฉพาะที่ล้มเหลว และหลีกเลี่ยงการบล็อกการกระทำที่ไม่เกี่ยวข้อง
รูปแบบไอเท็มที่ใช้งานได้จริง:
id: จริงหรือชั่วคราวstatus: pending | confirmed | failedoptimisticPatch: สิ่งที่คุณเปลี่ยนในเครื่อง (เล็กและเฉพาะ)serverValue: ข้อมูลที่ยืนยันล่าสุด (หรือ confirmedAt timestamp)rollbackSnapshot: ค่าก่อนหน้าที่คุณสามารถเรียกคืนได้การอัปเดตแบบ optimistic ปลอดภัยที่สุดเมื่อคุณแตะเฉพาะสิ่งที่ผู้ใช้เปลี่ยนจริง ๆ (เช่น สลับ completed) แทนการแทนที่อ็อบเจกต์ทั้งชิ้นด้วยเวอร์ชันที่เดาว่าเป็น "ของใหม่" การแทนที่ทั้งอ็อบเจกต์ทำให้ล้างการแก้ไขใหม่กว่า ฟิลด์ที่เซิร์ฟเวอร์เพิ่ม หรือการเปลี่ยนแปลงพร้อมกันได้ง่าย
การอัปเดตแบบ optimistic ที่ดีย่อมให้ความรู้สึกทันที แต่สุดท้ายต้องตรงกับสิ่งที่เซิร์ฟเวอร์บอก ปฏิบัติต่อการเปลี่ยนแปลงแบบ optimistic เป็นชั่วคราว และเก็บบันทึกพอที่จะยืนยันหรือเลิกได้อย่างปลอดภัย
ตัวอย่าง: ผู้ใช้แก้ไขชื่องานในรายการ คุณต้องการให้ชื่ออัปเดททันที แต่ยังต้องจัดการกับข้อผิดพลาดการตรวจสอบและการจัดรูปแบบจากฝั่งเซิร์ฟเวอร์
ใช้การเปลี่ยนแปลงแบบ optimistic ทันทีในสถานะท้องถิ่น เก็บแพตช์เล็ก ๆ (หรือสแน็ปชอต) เพื่อให้ย้อนกลับได้
ส่งคำขอพร้อม request ID (เลขเพิ่มขึ้นหรือตัว ID แบบสุ่ม) วิธีนี้จะจับคู่คำตอบกับการกระทำที่กระตุ้นมัน
ทำเครื่องหมายไอเท็มว่า pending การเป็น pending ไม่จำเป็นต้องบล็อก UI อาจเป็นสปินเนอร์เล็ก ๆ ข้อความจาง หรือ "Saving..." สำคัญคือต้องให้ผู้ใช้เข้าใจว่ายังไม่ยืนยัน
เมื่อสำเร็จ ให้แทนที่ข้อมูลชั่วคราวฝั่งไคลเอนต์ด้วยเวอร์ชันจากเซิร์ฟเวอร์ หากเซิร์ฟเวอร์ปรับอะไร (ตัดช่องว่าง เปลี่ยนตัวพิมพ์ อัปเดต timestamp) ให้ปรับสถานะท้องถิ่นให้ตรง
เมื่อผิดพลาด ให้ย้อนกลับเฉพาะสิ่งที่คำขอนี้เปลี่ยนและแสดงข้อผิดพลาดท้องถิ่น หลีกเลี่ยงการย้อนกลับส่วนที่ไม่เกี่ยวข้องของหน้าจอ
นี่คือรูปแบบเล็ก ๆ ที่ใช้ได้ (ไม่ขึ้นกับไลบรารี):
const requestId = crypto.randomUUID();
applyOptimistic({ id, title: nextTitle, pending: requestId });
try {
const serverItem = await api.updateTask({ id, title: nextTitle, requestId });
confirmSuccess({ id, requestId, serverItem });
} catch (err) {
rollback({ id, requestId });
showError("Could not save. Your change was undone.");
}
สองรายละเอียดที่ป้องกันบั๊กได้มาก: เก็บ request ID ไว้บนไอเท็มขณะที่รอดำเนินการ และยืนยันหรือย้อนกลับเฉพาะเมื่อ ID ตรง นั่นจะหยุดคำตอบเก่ามาเขียนทับการแก้ไขใหม่
Optimistic UI พังเมื่อเครือข่ายตอบไม่เรียงลำดับ ความล้มเหลวแบบคลาสสิก: ผู้ใช้แก้ไขชื่อ แล้วแก้อีกครั้งทันที แต่คำขอแรกเสร็จช้าสุด ถ้าคุณใช้คำตอบนั้นล่าช้า UI จะดีดกลับไปค่าสเก่า
การแก้คือปฏิบัติต่อทุกคำตอบว่าเป็น "อาจเกี่ยวข้อง" และใช้มันเฉพาะเมื่อมันตรงกับเจตนาล่าสุดของผู้ใช้
รูปแบบปฏิบัติได้คือใส่ client request ID (เช่น counter) กับแต่ละการเปลี่ยน เก็บ latest ID ต่อเรคคอร์ด เมื่อคำตอบมาถึงเทียบ ID ถ้าเป็นของเก่าให้ละเว้น
การตรวจสอบเวอร์ชันก็ช่วยได้ ถ้าเซิร์ฟเวอร์คืน updatedAt, version, หรือ etag ให้รับเฉพาะคำตอบที่ใหม่กว่าสถานะที่ UI แสดงอยู่แล้ว
ตัวเลือกอื่น ๆ ที่ผสมได้:
ตัวอย่าง (guard ด้วย request ID):
let nextId = 1;
const latestByItem = new Map();
async function saveTitle(itemId, title) {
const requestId = nextId++;
latestByItem.set(itemId, requestId);
// optimistic update
setItems(prev => prev.map(i => i.id === itemId ? { ...i, title } : i));
const res = await api.updateItem(itemId, { title, requestId });
// ignore stale response
if (latestByItem.get(itemId) !== requestId) return;
// reconcile with server truth
setItems(prev => prev.map(i => i.id === itemId ? { ...i, ...res.item } : i));
}
ถ้าผู้ใช้พิมพ์เร็ว (โน้ต ชื่อ ค้นหา) ให้พิจารณายกเลิกหรือหน่วงการบันทึกจนเขาหยุดพิมพ์ มันลดโหลดเซิร์ฟเวอร์และลดความเสี่ยงที่คำตอบมาช้าแล้วทำให้เกิดการเด้งของ UI
ความล้มเหลวคือที่ที่ Optimistic UI เสียความเชื่อถือ ประสบการณ์ที่แย่ที่สุดคือการเด้งกลับทันทีโดยไม่มีคำอธิบาย
ค่าเริ่มต้นที่ดีสำหรับการแก้ไขคือ: เก็บค่าของผู้ใช้บนหน้าจอ ทำเครื่องหมายว่าไม่ได้บันทึก และแสดงข้อผิดพลาดแบบอินไลน์ตรงที่พวกเขาแก้ไข ถ้าใครเปลี่ยนชื่อโปรเจกต์จาก "Alpha" เป็น "Q1 Launch" อย่าดันมันกลับเป็น "Alpha" เว้นแต่จำเป็น ให้เก็บ "Q1 Launch" และแสดง "Not saved. Name already taken" ให้ผู้ใช้แก้ไข
ฟีดแบ็กแบบอินไลน์ผูกกับฟิลด์หรือแถวที่ล้มเหลว หลีกเลี่ยงช่วงเวลา "เกิดอะไรขึ้น" ที่ประกาศเตือนโผล่แต่ UI เปลี่ยนกลับเงียบ ๆ
สัญญาณที่เชื่อถือได้รวม "Saving..." ขณะรอ, "Not saved" เมื่อผิดพลาด, ไฮไลต์เล็ก ๆ บนแถวที่ได้รับผลกระทบ และข้อความสั้น ๆ บอกผู้ใช้ว่าต้องทำอย่างไรต่อ
Retry ให้ประโยชน์เสมอ Undo เหมาะกับการกระทำเร็วที่ผู้ใช้อาจสำนึกผิด (เช่น archive) แต่สับสนได้สำหรับการแก้ไขที่ผู้ใช้ต้องการค่าใหม่จริง ๆ
เมื่อ mutation ล้มเหลว:
ถ้าจำเป็นต้อง rollback จริง ๆ (เช่น สิทธิ์เปลี่ยนและผู้ใช้ไม่สามารถแก้ได้) อธิบายสาเหตุและคืนค่าความจริงจากเซิร์ฟเวอร์: "Couldn’t save. You no longer have access to edit this."
ถือคำตอบจากเซิร์ฟเวอร์เป็นใบเสร็จ ไม่ใช่แค่ธง success หลังคำขอเสร็จ ให้ประสาน: เก็บสิ่งที่ผู้ใช้ตั้งใจ และยอมรับสิ่งที่เซิร์ฟเวอร์รู้ดีกว่า
การรีเฟตช์ทั้งหมดปลอดภัยที่สุดเมื่อเซิร์ฟเวอร์อาจเปลี่ยนมากกว่าที่คุณเดา และยังง่ายต่อการคิดตาม
รีเฟตช์มักเป็นตัวเลือกที่ดีกว่าเมื่อ mutation กระทบหลายเรคคอร์ด (ย้ายไอเท็มข้ามรายการ) เมื่อสิทธิ์หรือกฎเวิร์กโฟลว์อาจเปลี่ยนผล เมื่อเซิร์ฟเวอร์คืนข้อมูลไม่ครบ หรือเมื่อไคลเอนต์อื่นอัปเดตมุมมองบ่อย
ถ้าเซิร์ฟเวอร์คืนเอนทิตีที่อัปเดต (หรือฟิลด์เพียงพอ) การรวม (merge) อาจให้ประสบการณ์ที่ดีกว่า: UI คงที่แต่รับความจริงจากเซิร์ฟเวอร์
การคลาดเคลื่อนมักมาจากการเขียนทับฟิลด์ที่เซิร์ฟเวอร์เป็นเจ้าของด้วยอ็อบเจกต์ optimistic คิดถึงเคาน์เตอร์ ค่าที่คำนวณได้ timestamps และการจัดรูปแบบ
ตัวอย่าง: คุณตั้ง likedByMe=true และเพิ่ม likeCount แบบ optimistic เซิร์ฟเวอร์อาจลบการกดซ้ำและคืน likeCount ที่ต่างออกไป พร้อมอัปเดต updatedAt
แนวทางรวมง่าย ๆ:
เมื่อเกิดความขัดแย้ง ให้ตัดสินใจล่วงหน้า "Last write wins" ใช้ได้กับสวิตช์ แต่การ merge ระดับฟิลด์ดีกว่าสำหรับฟอร์ม
การติดตามแฟล็ก "dirty since request" ต่อฟิลด์ (หรือเลขเวอร์ชันท้องถิ่น) ช่วยให้คุณละเว้นค่าจากเซิร์ฟเวอร์สำหรับฟิลด์ที่ผู้ใช้แก้หลังจาก mutation เริ่ม ขณะเดียวกันยอมรับความจริงสำหรับฟิลด์อื่น ๆ
ถ้าเซิร์ฟเวอร์ปฏิเสธ mutation ให้แสดงข้อความที่เฉพาะเจาะจงและกระชับแทนการ rollback แบบเซอร์ไพรส์ เก็บข้อมูลผู้ใช้ ไฮไลต์ฟิลด์ และแสดงข้อความ ขั้นตอนการ rollback สำรองไว้เฉพาะกรณีที่การกระทำต้องถูกยกเลิกจริง ๆ (เช่น คุณลบไอเท็มแต่เซิร์ฟเวอร์ปฏิเสธการลบ)
รายการคือที่ที่ Optimistic UI ให้ความรู้สึกดีและแตกได้ง่าย รายการหนึ่งไอเท็มที่เปลี่ยนอาจกระทบการเรียง ยอดรวม ตัวกรอง และหลายหน้า
สำหรับการสร้าง ให้แสดงไอเท็มใหม่ทันทีแต่ทำป้าย pending และไอดีชั่วคราว เก็บตำแหน่งให้คงที่เพื่อไม่ให้มันกระโดด
สำหรับการลบ รูปแบบปลอดภัยคือซ่อนไอเท็มทันทีแต่เก็บ "ghost" สั้น ๆ ในหน่วยความจำจนกว่าเซิร์ฟเวอร์จะยืนยัน แบบนี้รองรับ Undo และทำให้จัดการความล้มเหลวง่ายขึ้น
การจัดเรียงซับซ้อนเพราะแตะหลายไอเท็ม ถ้า you reorder แบบ optimistic ให้เก็บ order ก่อนหน้าเพื่อคืนถ้าจำเป็น
กับ pagination หรือ infinite scroll ให้ตัดสินใจว่าแทรกแบบ optimistic จะอยู่ที่ไหน ใน feed ไอเท็มใหม่มักไปบนสุด ในแคตตาล็อกที่จัดอันดับโดยเซิร์ฟเวอร์ การแทรกท้องถิ่นอาจทำให้ผู้ใช้เข้าใจผิด ทางสายกลางคือแทรกในรายการที่มองเห็นพร้อมป้าย pending แล้วพร้อมย้ายหลังคำตอบถ้าคีย์การเรียงสุดท้ายต่างกัน
เมื่อไอดีชั่วคราวกลายเป็นไอดีจริง ให้ dedupe ด้วยคีย์ที่คงที่ ถ้าจับคู่ด้วยไอดีอย่างเดียว คุณอาจเห็นไอเท็มซ้ำ (temp และ confirmed) เก็บแมป tempId-to-realId และแทนที่ในที่เดิมเพื่อไม่ให้ตำแหน่งสกรอลล์หรือการเลือกรีเซ็ต
เคาน์เตอร์และตัวกรองก็เป็นสถานะของรายการ อัปเดตเคาน์เตอร์แบบ optimistic เมื่อคุณมั่นใจว่าเซิร์ฟเวอร์จะเห็นด้วย มิฉะนั้นให้ทำเครื่องหมายว่ากำลังรีเฟรชและประสานหลังคำตอบ
บั๊กส่วนใหญ่จาก optimistic-update ไม่ได้เกี่ยวกับ React โดยตรง แต่เกิดจากการปฏิบัติต่อการเปลี่ยนแปลงแบบ optimistic เป็น "ความจริงใหม่" แทนที่จะเป็นการเดาชั่วคราว
การอัปเดตแบบ optimistic ทั้งอ็อบเจกต์หรือทั้งหน้าจอเมื่อมีแค่ฟิลด์เดียวเปลี่ยนขยายรัศมีผลกระทบ แก้ไขจากเซิร์ฟเวอร์ทีหลังก็อาจเขียนทับการแก้ไขที่ไม่เกี่ยวข้องได้ง่าย
ตัวอย่าง: ฟอร์มโปรไฟล์แทนที่ user ทั้งหมดเมื่อคุณสลับการตั้งค่า ระหว่างที่คำขอกำลังทำ ผู้ใช้แก้ชื่อ เมื่อคำตอบมาถึง การแทนที่อาจดันชื่อเก่ากลับมา
เก็บแพตช์ optimistic เล็กและเฉพาะเจาะจง
แหล่งคลาดเคลื่อนอีกอย่างคือลืมเคลียร์แฟล็ก pending หลังสำเร็จหรือผิดพลาด UI จะค้างครึ่งโหลด และตรรกะต่อมาจะถือว่ามันยังเป็น optimistic
ถ้าคุณติดตาม pending ต่อไอเท็ม ให้เคลียร์ด้วยคีย์เดียวกับที่ใช้ตั้งค่า ไอดีชั่วคราวมักทำให้เกิดไอเท็ม pending ผีเมื่อไอดีจริงไม่ได้แมปทุกที่
บั๊ก rollback เกิดเมื่อสแน็ปชอตเก็บช้าเกินไปหรือครอบคลุมกว้างเกินไป ถ้าผู้ใช้แก้สองครั้งเร็ว ๆ คุณอาจย้อนกลับการแก้ครั้งที่ 2 โดยใช้สแน็ปชอตก่อนการแก้ครั้งที่ 1 UI จะเด้งไปสถานะที่ผู้ใช้ไม่เคยเห็น
แก้: สแน็ปชอตเฉพาะช่วงที่คุณจะคืนและขอบเขตให้เป็นของ mutation นั้น ๆ (มักใช้ request ID)
การบันทึกจริงมักเป็นหลายขั้นตอน ถ้าขั้นตอนที่ 2 ล้มเหลว (เช่น อัปโหลดรูป) อย่าลบเงียบ ๆ ขั้นตอนที่ 1 ที่สำเร็จ แสดงสิ่งที่บันทึกแล้ว สิ่งที่ไม่บันทึก และสิ่งที่ผู้ใช้ต้องทำต่อ
นอกจากนี้อย่าคาดว่าเซิร์ฟเวอร์จะสะท้อนกลับสิ่งที่คุณส่งตรง ๆ เสมอ เซิร์ฟเวอร์จัดรูปแบบข้อความ ใช้สิทธิ์ ตั้ง timestamps กำหนด IDs และทิ้งฟิลด์เสมอ ประสานจากคำตอบ (หรือรีเฟตช์) แทนการเชื่อแพตช์ optimistic ตลอดไป
Optimistic UI ใช้งานได้เมื่อคาดเดาได้ ปฏิบัติต่อแต่ละการเปลี่ยนแบบ optimistic เหมือนธุรกรรมย่อย: มี ID แสดงสถานะ pending ชัดเจน มีการสลับเมื่อสำเร็จ และมีเส้นทางเมื่อผิดพลาดที่ไม่ทำให้ผู้ใช้งง
เช็คลิสต์ก่อนส่งขึ้นโปรดักชัน:
ถ้าคุณกำลังทำโปรโตไทป์เร็ว ๆ ให้เก็บเวอร์ชันแรกเล็ก: หน้าจอเดียว หนึ่ง mutation การอัปเดตรายการหนึ่งครั้ง เครื่องมืออย่าง Koder.ai (koder.ai) สามารถช่วยสเก็ตช์ UI และ API ได้เร็วขึ้น แต่กฎเดิมยังใช้: ออกแบบ pending vs confirmed ให้ชัดเจนเพื่อให้ไคลเอนต์ไม่หลงทางในสิ่งที่เซิร์ฟเวอร์ยอมรับจริง
Optimistic UI อัปเดตหน้าจอทันที ก่อนที่เซิร์ฟเวอร์จะยืนยันการเปลี่ยนแปลง ทำให้แอปดูรวดเร็ว แต่ต้องมีการประสานกับคำตอบจากเซิร์ฟเวอร์เพื่อไม่ให้ UI เบี้ยวจากสถานะที่ถูกบันทึกจริง
Data drift เกิดเมื่อ UI ถือค่าที่คาดเดาไว้เป็นค่าที่ยืนยันแล้ว แต่เซิร์ฟเวอร์บันทึกค่าแตกต่างหรือปฏิเสธ มักเห็นหลังรีเฟรช ในแท็บอื่น หรือเมื่อเครือข่ายช้าจนคำตอบมาถึงไม่เรียงตามลำดับ
หลีกเลี่ยงหรือระมัดระวังการใช้ optimistic updates กับเรื่องเงิน การเรียกเก็บเงิน การกระทำที่ไม่สามารถย้อนกลับได้ การเปลี่ยนแปลงสิทธิ์ และเวิร์กโฟลว์ที่มีกฎเข้มงวดจากเซิร์ฟเวอร์ ในกรณีเหล่านี้ควรแสดงสถานะรอดำเนินการและรอการยืนยันก่อนเปลี่ยนค่าที่ส่งผลต่อยอดหรือการเข้าถึง
ถือว่า backend เป็นแหล่งความจริงสำหรับข้อมูลที่มีความหมายทางธุรกิจนอกหน้าจอนั้น เช่น ราคา สิทธิ์ ฟิลด์ที่คำนวณได้ และเคาน์เตอร์ที่แชร์ ส่วนสถานะ UI ท้องถิ่นให้เก็บไว้สำหรับร่าง ขณะกำลังแก้ไข focus และตัวกรองที่เป็นการนำเสนอเท่านั้น
แสดงสัญญาณเล็ก ๆ และสม่ำเสมอที่จุดที่มีการเปลี่ยน เช่น “Saving…”, ข้อความจาง หรือสปินเนอร์เล็กๆ เป้าหมายคือให้ชัดว่าค่าชั่วคราว ไม่ใช่การบล็อกทั้งหน้า
ใช้ไอดีชั่วคราวฝั่งไคลเอนต์ (เช่น UUID หรือ temp_...) เมื่อสร้างรายการ แล้วแทนที่ด้วยไอดีจริงจากเซิร์ฟเวอร์เมื่อสำเร็จ เพื่อให้ key ของรายการ การเลือก และสถานะการแก้ไขคงที่ ไม่กระพริบหรือซ้ำกัน
อย่าใช้แฟล็กโหลดหน้าเดียวแบบรวม; ติดตามสถานะ pending ต่อตัวรายการ (หรือแม้แต่ต่อฟิลด์) เก็บแพตช์เชิงบวกขนาดเล็กและสแน็ปชอตสำหรับ rollback เพื่อยืนยันหรือย้อนกลับเฉพาะการเปลี่ยนแปลงนั้นโดยไม่กระทบ UI อื่น
แนบ request ID ให้กับแต่ละการเปลี่ยนและเก็บ latest request ID ต่อไอเท็ม เมื่อคำตอบมาถึงให้ใช้เฉพาะเมื่อ ID ตรงกับล่าสุด มิฉะนั้นให้ละเว้น เพื่อป้องกันคำตอบเก่าดึง UI กลับไปค่าสเก่าที่ไม่ต้องการ
สำหรับการแก้ไขทั่วไป ให้เก็บค่าของผู้ใช้ไว้บนหน้าจอ ทำเครื่องหมายว่าไม่ได้บันทึก และแสดงข้อผิดพลาดแบบอินไลน์พร้อมปุ่ม Retry การ rollback จริง ๆ ควรทำเมื่อการเปลี่ยนแปลงนั้นไม่สามารถยืนอยู่ได้ (เช่น สิทธิ์ถูกยกเลิก) และต้องอธิบายสาเหตุ
รีเฟตช์เมื่อการเปลี่ยนแปลงอาจส่งผลหลายที่ เช่น ยอดรวม การจัดเรียง สิทธิ์ หรือฟิลด์ที่คำนวณได้ เพราะการแพตช์ให้ถูกต้องทุกที่มักพลาดได้ง่าย หากเซิร์ฟเวอร์คืนเอนทิตีที่อัปเดตมาพร้อมหรือฟิลด์เพียงพอ การแมจ์จะให้ประสบการณ์ที่นิ่งกว่า