useSyncExternalStore ช่วยให้คอมโพเนนต์สามารถอ่านข้อมูลที่อยู่นอก React ได้โดยไม่เสี่ยงต่อการเกิด visual glitches ที่อาจเกิดขึ้นจากการทำ concurrent rendering มันเข้ามาแทนที่รูปแบบเดิมที่ใช้ useEffect ในการดึงค่าภายนอกเข้าสู่ local state ซึ่งเป็นรูปแบบที่อาจทำให้เกิด "tearing" หรือการที่ส่วนต่างๆ ของ UI แสดงค่าที่แตกต่างกันในระหว่างการเรนเดอร์ครั้งเดียวกัน
ทำไมรูปแบบเดิมถึงมีปัญหาเมื่อใช้ concurrency
ก่อน React 18 สูตรที่ใช้กันทั่วไปคือ:
const [value, setValue] = useState(initial);
useEffect(() => {
const unsubscribe = externalSource.subscribe(v => setValue(v));
return unsubscribe;
}, []);
Effect จะทำงานหลังจากคอมโพเนนต์เรนเดอร์เสร็จแล้ว เมื่อ concurrent renderer ของ React ตัดสินใจที่จะหยุด (pause), ยกเลิก (abort), หรือเล่นซ้ำ (replay) การเรนเดอร์ ตัว effect อาจทำงานในจุดที่ต่างจาก UI ที่อ่าน state ผลลัพธ์ที่ได้คือความไม่สอดคล้องกัน: คอมโพเนนต์หนึ่งอาจอ่านค่าเก่า แต่อีกคอมโพเนนต์หนึ่งอ่านค่าใหม่ และหน้าจอจะแสดงข้อมูลที่ไม่ตรงกันชั่วขณะ อาการ "tearing" นี้อาจไม่เป็นปัญหาในโลกที่ทำงานแบบ synchronous ล้วนๆ แต่จะกลายเป็นบั๊กที่เห็นได้ชัดเมื่อ React เรนเดอร์ UI หลายเวอร์ชันพร้อมกัน (in parallel)
How useSyncExternalStore แก้ปัญหานี้ได้อย่างไร
Hook นี้ต้องการ callback สองตัว:
- subscribe – ลงทะเบียน listener และคืนค่าฟังก์ชันสำหรับยกเลิกการลงทะเบียน
- getSnapshot – คืนค่าปัจจุบันของแหล่งข้อมูลภายนอก
React จะเรียก subscribe เมื่อคอมโพเนนต์ mount และจะยกเลิก (dispose) เมื่อ unmount ในระหว่างการเรนเดอร์ทุกครั้ง React จะเรียก getSnapshot และรับประกันว่าคอมโพเนนต์ทั้งหมดที่ใช้ external store เดียวกันจะอ่าน snapshot ที่เหมือนกันทุกประการ เนื่องจากการอ่านเกิดขึ้นภายในขั้นตอนการเรนเดอร์ (render phase) ตัว renderer จึงสามารถประสานการอัปเดต (coordinate updates) ไปทั่วทั้ง tree ได้ ซึ่งช่วยกำจัดปัญหา tearing
Library อย่าง Zustand และ Redux ได้ห่อหุ้ม (wrap) store ของตนเองด้วย hook นี้อยู่แล้ว นั่นคือเหตุผลที่พวกมันสามารถใช้งานร่วมกับฟีเจอร์ concurrent ของ React 18 ได้ทันที
กฎที่ช่วยป้องกันไม่ให้ hook ทำงานผิดพลาด
- คืนค่าเฉพาะสิ่งที่คอมโพเนนต์จำเป็นต้องใช้เท่านั้น – การคืนค่า object ขนาดใหญ่จะบังคับให้ React ต้องเปรียบเทียบโครงสร้างที่ซับซ้อน (deep structures) ในทุกๆ การอัปเดต การใช้ snapshot ที่มีขนาดเล็กจะช่วยให้การตรวจจับการเปลี่ยนแปลง (change detection) ทำได้รวดเร็วและประหยัดทรัพยากร
- รักษา referential identity – หากค่าภายนอกไม่มีการเปลี่ยนแปลง getSnapshot ต้องคืนค่า object reference เดิม การคืนค่า object ใหม่ในทุกๆ การเรียกจะทำให้เกิด infinite render loop เพราะ React จะมองเห็นว่ามีการเปลี่ยนแปลงในทุกการเรนเดอร์
- จำกัดการใช้งานเฉพาะแหล่งข้อมูลภายนอก – local component state ควรอยู่ใน
useStateหรือuseReducerการใช้ hook สำหรับ external store กับค่าที่เป็นภายในล้วนๆ จะเป็นการเพิ่มความซับซ้อนโดยไม่จำเป็น
เมื่อไหร่ควรเลือกใช้ useSyncExternalStore
- การห่อหุ้ม (Wrapping) browser APIs (เช่น window size, สถานะ network-online, media queries)
- การเชื่อมต่อกับ plain JavaScript store ที่ไม่รู้จัก React
- การแทนที่ prop-drilling ด้วยแหล่งข้อมูลแบบ read-only ระดับ global ที่มีหลายคอมโพเนนต์คอย subscribe อยู่
ข้อโต้แย้ง
รูปแบบ useState + useEffect ยังคงใช้งานได้กับข้อมูลที่เรียบง่ายและเปลี่ยนแปลงไม่บ่อย โดยเฉพาะเมื่อแอปพลิเคชันไม่ได้เปิดใช้งานฟีเจอร์ concurrent มันเป็นแนวทางระดับต่ำ (lower-level approach) ที่นักพัฒนาบางคนพบว่าเข้าใจได้ง่ายกว่า เพราะตรรกะการ subscription จะอยู่ใน effect อย่างชัดเจน อย่างไรก็ตาม ข้อแลกเปลี่ยนคือความเสี่ยงที่จะเกิด tearing เมื่อเปิดใช้งาน concurrent rendering สำหรับโปรเจกต์ที่ใช้ความสามารถใหม่ๆ ของ React 18 อยู่แล้ว ความปลอดภัยที่เพิ่มขึ้นของ useSyncExternalStore นั้นคุ้มค่ากว่า boilerplate ที่เพิ่มขึ้นเพียงเล็กน้อย
สิ่งที่ควรระวังต่อไป
- ตรวจสอบให้แน่ใจว่า getSnapshot ของ store คืนค่า reference ที่เสถียร (stable reference) การใช้
console.logเพื่อดู reference ระหว่างการพัฒนาสามารถช่วยให้เห็นการสร้าง object ใหม่โดยไม่ตั้งใจได้ - รักษาฟังก์ชัน subscription ให้มีน้ำหนักเบา (lightweight) – งานที่หนักควรเกิดขึ้นภายในแหล่งข้อมูลภายนอก ไม่ใช่ใน callback ที่ส่งให้ React
- ทดสอบคอมโพเนนต์ภายใต้ React’s concurrent mode (เช่น การใช้
actร่วมกับReactDOM.createRoot) เพื่อยืนยันว่าไม่เกิดอาการ tearing
ตัวอย่างการใช้งานจริง (live demo) แสดงให้เห็นการทำงานของ hook นี้ โดยการซิงค์ตัวนับ (counter) ง่ายๆ ที่เก็บไว้นอก React และอัปเดตคอมโพเนนต์หลายตัวโดยไม่มี visual glitches สามารถดู demo ได้ที่ https://usesyncexternalstore.vercel.app/ ส่วน source code อยู่บน GitHub และยังมีบทความฉบับเต็มที่อธิบายรายละเอียดการ implementation
สรุปสาระสำคัญ: ในโลกที่ React สามารถเรนเดอร์ส่วนต่างๆ ของ UI แบบไม่เรียงลำดับได้ useSyncExternalStore คือสะพานที่เชื่อถือได้ระหว่างข้อมูลภายนอกและการเรนเดอร์คอมโพเนนต์ มันช่วยรักษาความสอดคล้องทางภาพ (visual consistency) รองรับการขยายตัวไปยัง global stores และถูกรวมไว้ใน library การจัดการ state ยอดนิยมอยู่แล้ว จงนำไปใช้เมื่อใดก็ตามที่คุณต้องการอ่าน non-React state และพยายามรักษา snapshot ให้มีขนาดเล็กและเสถียรเพื่อให้ได้รับประโยชน์สูงสุด
