React 18 ได้เพิ่ม useSyncExternalStore ซึ่งเป็น hook ที่ช่วยให้ component สามารถอ่านข้อมูลจากแหล่งใดก็ได้ที่อยู่นอก React tree เช่น browser APIs, WebSockets หรือ third-party stores โดยไม่เกิดปัญหา visual glitches ที่มักพบในรูปแบบเดิมที่ต้องทำ mirror ข้อมูลด้วย useState และ useEffect หากคุณเคยเห็น UI กระพริบ (flicker) เพราะบางส่วนแสดงขนาดหน้าต่างแบบเก่า ในขณะที่อีกส่วนแสดงขนาดใหม่ไปแล้ว hook ตัวนี้คือทางออกที่คุณรอคอย

ทำไม external state ถึงต้องการ hook ใหม่

React component สามารถ subscribe สิ่งต่างๆ ที่อยู่นอกขอบเขตของตัวเองได้เสมอ ไม่ว่าจะเป็น object window, Redux store หรือ WebSocket connection โดยวิธีแบบดั้งเดิมคือการตั้งค่า subscription ไว้ภายใน useEffect จากนั้นจึงส่งค่าที่ได้รับเข้ามาไปยัง local state ผ่าน setState และปล่อยให้ React ทำการ re-render ซึ่งวิธีนี้ใช้งานได้ดีเมื่อ React ทำการ render แบบ synchronous แต่ React 18 ได้นำ concurrent rendering เข้ามา ซึ่งทำให้ component อาจถูก render หลายครั้งก่อนที่ browser จะทำการ paint จริงๆ ในโหมดนั้น รูปแบบการ “mirror-into-local-state” อาจทำให้เกิด tearing ซึ่งก็คือการที่ส่วนต่างๆ ของ UI อ่าน snapshot ของค่าภายนอกเดียวกันที่แตกต่างกันในช่วง render cycle เดียวกัน

useSyncExternalStore ถูกสร้างขึ้นเพื่อป้องกันการเกิด tearing โดยจะขอ snapshot (ค่าปัจจุบัน) จากแหล่งข้อมูลภายนอก และขอฟังก์ชัน subscribe (วิธีการแจ้งเตือนเมื่อมีการเปลี่ยนแปลง) React จะเรียกใช้ subscribe เมื่อ component mount และจะทำการ unsubscribe ให้โดยอัตโนมัติเมื่อ unmount เมื่อใดก็ตามที่ snapshot ที่ส่งกลับมาจาก getSnapshot เปลี่ยนแปลง React จะกำหนดตารางการ render ที่จะอ่าน snapshot ตัวเดียวกัน สำหรับทั้ง component tree เพื่อรับประกันความสอดคล้องกัน (consistency)

ฟังก์ชันสองตัวที่จำเป็นต้องมี

Function หน้าที่
subscribe รับ callback ที่ต้องถูกเรียกทุกครั้งเมื่อค่าภายนอกเปลี่ยนแปลง โดยจะคืนค่าเป็นฟังก์ชัน unsubscribe ซึ่ง React จะเรียกใช้เมื่อทำการ cleanup
getSnapshot ส่งคืนค่าปัจจุบันจากแหล่งข้อมูลภายนอก โดยต้องมีความ referentially stable (คือเป็น function object ตัวเดิมในทุกการ render) เพื่อให้ React สามารถเปรียบเทียบ snapshot ได้อย่างถูกต้อง

ตัวอย่างการใช้งานขั้นพื้นฐานสำหรับตรวจสอบสถานะ online ของ browser มีดังนี้:

function useOnlineStatus() {
  return useSyncExternalStore(
    (onChange) => {
      window.addEventListener('online', onChange);
      window.addEventListener('offline', onChange);
      return () => {
        window.removeEventListener('online', onChange);
        window.removeEventListener('offline', onChange);
      };
    },
    () => navigator.onLine
  );
}

เมื่อ component ใดๆ เรียกใช้ useOnlineStatus() React จะทำการ re-render เฉพาะเมื่อ navigator.onLine เปลี่ยนแปลงจริงๆ เท่านั้น และทุกการ render จะเห็นค่าที่เหมือนกันเสมอ

กฎที่ช่วยป้องกันไม่ให้ hook ทำงานผิดพลาด

  1. คืนค่าข้อมูลชิ้นที่เล็กที่สุดที่ component จำเป็นต้องใช้
    หาก external store เก็บ object ขนาดใหญ่ แต่ component สนใจเพียงแค่ฟิลด์เดียว ให้คืนค่าเฉพาะฟิลด์นั้น การคืนค่าที่ใหญ่เกินความจำเป็นจะทำให้เกิดการ re-render โดยไม่จำเป็น

  2. ทำ Memoise ให้กับ getSnapshot
    หากคุณสร้างฟังก์ชันขึ้นมาใหม่ในทุกการ render React จะมองว่าเป็นแหล่งข้อมูลใหม่และอาจทำให้เกิด loop ไม่สิ้นสุด ควรใช้ useCallback หุ้มไว้ หรือนิยามฟังก์ชันไว้ภายนอก component

  3. หลีกเลี่ยงการคืนค่าเป็น object ใหม่ในทุกครั้งที่เรียก
    การคืนค่าเป็น object ใหม่ (เช่น { count: store.getCount() }) จะสร้าง reference ใหม่ในทุกการ render ทำให้ React เข้าใจว่า snapshot เปลี่ยนแปลงและจะกระตุ้นให้เกิด infinite render loop ควรใช้ค่าประเภท primitives, strings, numbers หรือ memoised objects

  4. อย่าใช้กับ internal component state
    useState ยังคงเป็นเครื่องมือที่เหมาะสมสำหรับข้อมูลที่อยู่ เฉพาะ ภายใน component เท่านั้น การใช้ useSyncExternalStore จะเป็นการเพิ่ม overhead ที่ไม่จำเป็นสำหรับ local state

เมื่อไหร่ที่ควรเลือกใช้

  • Browser APIs – ขนาดหน้าต่าง (window size), media queries, navigator.onLine, สถานะแบตเตอรี่
  • Framework-agnostic stores – Zustand, Redux, MobX หรือ custom store ใดๆ ที่มี subscription API ให้ใช้งาน
  • Mutable sources ที่อัปเดตอยู่นอก React – ข้อความจาก WebSocket, event การเปลี่ยนแปลงของ IndexedDB, การแจ้งเตือนจาก Service Worker

หากแหล่งข้อมูลของคุณอยู่ใน React อยู่แล้ว (เช่น state ของ parent component) ให้ใช้ useState หรือ context ต่อไป

สิ่งที่ชุมชนนักพัฒนาพูดถึง

ผู้ที่เริ่มใช้งานกลุ่มแรกๆ รายงานว่า useSyncExternalStore ช่วยกำจัดอาการหน้าจอกระพริบ (flicker) ที่พบเมื่อมีการปรับขนาดหน้าต่างในระบบ concurrent-rendering ไลบรารีบางตัวได้เปลี่ยน internal hooks มาใช้ API นี้แล้ว ซึ่งช่วยให้พฤติกรรมของไลบรารีคาดเดาได้ง่ายขึ้นใน React เวอร์ชันต่างๆ สิ่งที่ต้องแลกมาคือภาระทางความคิด (mental load) ที่เพิ่มขึ้นเล็กน้อย เนื่องจากนักพัฒนาต้องคำนึงถึงความเสถียร (stability) ของฟังก์ชันทั้งสองและหลีกเลี่ยงการคืนค่าเป็น object ใหม่ในทุกครั้งที่เรียก

สิ่งที่ควรติดตามต่อไป

การอัปเดตของ React ที่กำลังจะมาถึงอาจมีการปรับปรุงข้อกำหนด (contract) รอบๆ useSyncExternalStore ให้รัดกุมยิ่งขึ้น โดยอาจมีการเพิ่มตัวช่วย (helpers) แบบ built-in สำหรับ browser APIs ทั่วไป คอยติดตามบล็อกอย่างเป็นทางการของ React สำหรับการประกาศยกเลิก (deprecation) รูปแบบการเขียนแบบเดิมที่เลียนแบบ useEffect ในระหว่างนี้ hook ดังกล่าวยังมีความเสถียรและเป็นส่วนหนึ่งของ public API ดังนั้นคุณจึงสามารถ refactor โค้ดจัดการ external-state ที่มีอยู่ได้อย่างปลอดภัย

ทดลองใช้งานอย่างรวดเร็ว

คุณสามารถดูเดโมที่แสดงการซิงค์ขนาดหน้าต่าง (window-size syncing), การตรวจจับสถานะออนไลน์/ออฟไลน์ และการใช้งาน Zustand store แบบง่ายๆ ได้ที่ https://usesyncexternalstore.vercel.app/ ส่วนซอร์สโค้ดที่มีคอมเมนต์ประกอบอย่างครบถ้วน สามารถดูได้ที่ https://github.com/dev48v/usesyncexternalstore

สรุปประเด็นสำคัญ: useSyncExternalStore ช่วยให้ React มีสะพานเชื่อมที่เชื่อถือได้ไปยังข้อมูลที่เปลี่ยนแปลงได้ (mutable data) ใดๆ ที่อยู่นอก render cycle ซึ่งช่วยป้องกันปัญหา tearing และการ render ที่ไม่จำเป็น นำไปใช้กับแหล่งข้อมูลภายนอก (external sources) โดยรักษา snapshot ให้มีขนาดเล็กที่สุดและมีความเสถียร แล้วปล่อยให้ React จัดการงานหนักในเรื่องความสอดคล้องของข้อมูล (consistency)