คู่มือฉบับสมบูรณ์เกี่ยวกับ useSyncExternalStore ของ React การใช้งาน การนำไปใช้ ประโยชน์ และกรณีการใช้งานขั้นสูงสำหรับการจัดการสถานะภายนอก
React useSyncExternalStore: การจัดการการซิงโครไนซ์สถานะภายนอกขั้นสูง
useSyncExternalStore
เป็น React hook ที่เปิดตัวใน React 18 ซึ่งช่วยให้คุณสามารถสมัครรับข้อมูลและอ่านจากแหล่งข้อมูลภายนอกในลักษณะที่เข้ากันได้กับการแสดงผลพร้อมกัน (concurrent rendering) hook นี้จะเชื่อมช่องว่างระหว่างสถานะที่จัดการโดย React และสถานะภายนอก เช่น ข้อมูลจากไลบรารีของบุคคลที่สาม, Browser API หรือ UI framework อื่นๆ เราจะมาเจาะลึกทำความเข้าใจถึงวัตถุประสงค์, การนำไปใช้ และประโยชน์ของมัน
ทำความเข้าใจความจำเป็นของ useSyncExternalStore
การจัดการสถานะในตัวของ React (useState
, useReducer
, Context API) ทำงานได้ดีเยี่ยมสำหรับข้อมูลที่ผูกติดกับ component tree ของ React อย่างแน่นหนา อย่างไรก็ตาม หลายๆ แอปพลิเคชันจำเป็นต้องรวมเข้ากับแหล่งข้อมูลที่ *นอกเหนือ* การควบคุมของ React แหล่งภายนอกเหล่านี้อาจรวมถึง:
- ไลบรารีการจัดการสถานะของบุคคลที่สาม: การรวมเข้ากับไลบรารีเช่น Zustand, Jotai หรือ Valtio
- Browser APIs: การเข้าถึงข้อมูลจาก
localStorage
,IndexedDB
หรือ Network Information API - ข้อมูลที่ดึงมาจากเซิร์ฟเวอร์: แม้ว่าไลบรารีเช่น React Query และ SWR มักจะถูกเลือกใช้ แต่บางครั้งคุณอาจต้องการการควบคุมโดยตรง
- UI framework อื่นๆ: ในแอปพลิเคชันแบบผสม (hybrid) ที่ React ทำงานร่วมกับเทคโนโลยี UI อื่นๆ
การอ่านและเขียนโดยตรงจากแหล่งภายนอกเหล่านี้ภายใน React component อาจนำไปสู่ปัญหา โดยเฉพาะอย่างยิ่งกับการแสดงผลพร้อมกัน React อาจแสดง component ที่มีข้อมูลเก่า (stale data) หากแหล่งภายนอกมีการเปลี่ยนแปลงในขณะที่ React กำลังเตรียมหน้าจอใหม่ useSyncExternalStore
แก้ปัญหานี้โดยการจัดหากลไกให้ React ซิงโครไนซ์กับสถานะภายนอกได้อย่างปลอดภัย
useSyncExternalStore ทำงานอย่างไร
useSyncExternalStore
hook รับอาร์กิวเมนต์สามตัว:
subscribe
: ฟังก์ชันที่รับ callback ฟังก์ชันนี้จะถูกเรียกเมื่อใดก็ตามที่ external store มีการเปลี่ยนแปลง ฟังก์ชันควรคืนค่าฟังก์ชันที่เมื่อถูกเรียก จะยกเลิกการสมัครรับข้อมูลจาก external storegetSnapshot
: ฟังก์ชันที่คืนค่าปัจจุบันของ external store React ใช้ฟังก์ชันนี้เพื่ออ่านค่าของ store ในระหว่างการแสดงผลgetServerSnapshot
(optional): ฟังก์ชันที่คืนค่าเริ่มต้นของ external store บนเซิร์ฟเวอร์ ฟังก์ชันนี้จำเป็นสำหรับ server-side rendering (SSR) เท่านั้น หากไม่ได้ระบุ React จะใช้getSnapshot
บนเซิร์ฟเวอร์
hook นี้จะคืนค่าปัจจุบันของ external store ซึ่งได้มาจาก getSnapshot
ฟังก์ชัน React จะตรวจสอบให้แน่ใจว่า component จะถูก re-render ใหม่ทุกครั้งที่ค่าที่คืนโดย getSnapshot
เปลี่ยนแปลง โดยพิจารณาจากการเปรียบเทียบ Object.is
ตัวอย่างพื้นฐาน: การซิงโครไนซ์กับ localStorage
เรามาสร้างตัวอย่างง่ายๆ ที่ใช้ useSyncExternalStore
เพื่อซิงโครไนซ์ค่ากับ localStorage
Value from localStorage: {localValue}
ในตัวอย่างนี้:
subscribe
: ฟังเหตุการณ์storage
บนwindow
object เหตุการณ์นี้จะเกิดขึ้นทุกครั้งที่localStorage
ถูกแก้ไขโดยแท็บหรือหน้าต่างอื่นgetSnapshot
: ดึงค่าmyValue
จากlocalStorage
getServerSnapshot
: คืนค่าเริ่มต้นสำหรับ server-side rendering นี่อาจถูกดึงมาจาก cookie หากผู้ใช้เคยตั้งค่าไว้ก่อนหน้าMyComponent
: ใช้useSyncExternalStore
เพื่อสมัครรับข้อมูลการเปลี่ยนแปลงในlocalStorage
และแสดงค่าปัจจุบัน
กรณีการใช้งานขั้นสูงและข้อควรพิจารณา
1. การรวมเข้ากับไลบรารีการจัดการสถานะของบุคคลที่สาม
useSyncExternalStore
มีประโยชน์อย่างยิ่งเมื่อรวม React components เข้ากับไลบรารีการจัดการสถานะภายนอก มาดูตัวอย่างการใช้ Zustand:
Count: {count}
ในตัวอย่างนี้ useSyncExternalStore
ถูกใช้เพื่อสมัครรับข้อมูลการเปลี่ยนแปลงใน Zustand store โปรดสังเกตว่าเราส่ง useStore.subscribe
และ useStore.getState
โดยตรงไปยัง hook ทำให้การรวมเข้าเป็นไปอย่างราบรื่น
2. การปรับปรุงประสิทธิภาพด้วย Memoization
เนื่องจาก getSnapshot
จะถูกเรียกในการแสดงผลทุกครั้ง จึงเป็นสิ่งสำคัญเพื่อให้แน่ใจว่ามีประสิทธิภาพ หลีกเลี่ยงการคำนวณที่ซับซ้อนภายใน getSnapshot
หากจำเป็น ให้ memoize ผลลัพธ์ของ getSnapshot
โดยใช้ useMemo
หรือเทคนิคที่คล้ายคลึงกัน
พิจารณาตัวอย่างนี้ (ที่อาจมีปัญหา):
```javascript import { useSyncExternalStore, useMemo } from 'react'; const externalStore = { data: [...Array(10000).keys()], // Array ขนาดใหญ่ listeners: [], subscribe(listener) { this.listeners.push(listener); return () => { this.listeners = this.listeners.filter((l) => l !== listener); }; }, setState(newData) { this.data = newData; this.listeners.forEach((listener) => listener()); }, getState() { return this.data; }, }; function ExpensiveComponent() { const data = useSyncExternalStore( externalStore.subscribe, () => externalStore.getState().map(x => x * 2) // การดำเนินการที่ซับซ้อน ); return (-
{data.slice(0, 10).map((item) => (
- {item} ))}
ในตัวอย่างนี้ getSnapshot
(ฟังก์ชัน inline ที่ส่งเป็นอาร์กิวเมนต์ที่สองไปยัง useSyncExternalStore
) ทำการดำเนินการ map
ที่ซับซ้อนกับ array ขนาดใหญ่ การดำเนินการนี้จะถูกดำเนินการในการแสดงผล *ทุกครั้ง* แม้ว่าข้อมูลพื้นฐานจะไม่ได้เปลี่ยนแปลง หากต้องการปรับปรุงสิ่งนี้ เราสามารถ memoize ผลลัพธ์ได้:
-
{data.slice(0, 10).map((item) => (
- {item} ))}
ตอนนี้ การดำเนินการ map
จะถูกดำเนินการเฉพาะเมื่อ externalStore.getState()
เปลี่ยนแปลงเท่านั้น โปรดทราบ: คุณจะต้องทำการเปรียบเทียบแบบ deep กับ `externalStore.getState()` หรือใช้กลยุทธ์อื่นหาก store มีการ mutate object เดียวกัน ตัวอย่างนี้ถูกทำให้ง่ายขึ้นเพื่อการสาธิต
3. การจัดการ Concurrent Rendering
ประโยชน์หลักของ useSyncExternalStore
คือความเข้ากันได้กับคุณสมบัติ concurrent rendering ของ React Concurrent rendering ช่วยให้ React สามารถเตรียม UI ได้หลายเวอร์ชันพร้อมกัน เมื่อ external store เปลี่ยนแปลงระหว่าง concurrent render useSyncExternalStore
จะรับประกันว่า React จะใช้ข้อมูลที่ทันสมัยที่สุดเสมอเมื่อทำการ commit การเปลี่ยนแปลงไปยัง DOM
หากไม่มี useSyncExternalStore
component อาจแสดงผลด้วยข้อมูลเก่า ซึ่งนำไปสู่ความไม่สอดคล้องกันทางภาพและพฤติกรรมที่ไม่คาดคิด getSnapshot
method ของ useSyncExternalStore
ถูกออกแบบมาให้เป็น synchronous และรวดเร็ว ช่วยให้ React สามารถระบุได้อย่างรวดเร็วว่า external store มีการเปลี่ยนแปลงระหว่างการแสดงผลหรือไม่
4. ข้อควรพิจารณาเกี่ยวกับ Server-Side Rendering (SSR)
เมื่อใช้ useSyncExternalStore
กับ server-side rendering สิ่งสำคัญคือต้องจัดเตรียม getServerSnapshot
function ฟังก์ชันนี้ใช้เพื่อดึงค่าเริ่มต้นของ external store บนเซิร์ฟเวอร์ หากไม่มี React จะพยายามใช้ getSnapshot
บนเซิร์ฟเวอร์ ซึ่งอาจเป็นไปไม่ได้หาก external store พึ่งพา API เฉพาะของเบราว์เซอร์ (เช่น localStorage
)
getServerSnapshot
function ควรคืนค่าเริ่มต้นหรือดึงข้อมูลจากแหล่งฝั่งเซิร์ฟเวอร์ (เช่น cookies, database) สิ่งนี้จะรับประกันว่า HTML เริ่มต้นที่แสดงผลบนเซิร์ฟเวอร์มีข้อมูลที่ถูกต้อง
5. การจัดการข้อผิดพลาด
การจัดการข้อผิดพลาดที่แข็งแกร่งเป็นสิ่งสำคัญ โดยเฉพาะอย่างยิ่งเมื่อจัดการกับแหล่งข้อมูลภายนอก ครอบคลุม getSnapshot
และ getServerSnapshot
functions ด้วยบล็อก try...catch
เพื่อจัดการข้อผิดพลาดที่อาจเกิดขึ้น บันทึกข้อผิดพลาดอย่างเหมาะสมและจัดเตรียมค่าสำรองเพื่อป้องกันไม่ให้แอปพลิเคชันล่ม
6. Custom Hooks สำหรับการนำกลับมาใช้ใหม่
เพื่อส่งเสริมการนำโค้ดกลับมาใช้ใหม่ ให้ห่อหุ้ม logic ของ useSyncExternalStore
ไว้ใน custom hook ซึ่งจะทำให้ง่ายต่อการแชร์ logic ในหลายๆ components
ตัวอย่างเช่น มาสร้าง custom hook สำหรับการเข้าถึง key เฉพาะใน localStorage
:
ตอนนี้ คุณสามารถใช้ hook นี้ได้อย่างง่ายดายใน component ใดก็ได้:
```javascript import useLocalStorage from './useLocalStorage'; function MyComponent() { const [name, setName] = useLocalStorage('userName', 'Guest'); return (Hello, {name}!
setName(e.target.value)} />แนวทางปฏิบัติที่ดีที่สุด
- ทำให้
getSnapshot
รวดเร็ว: หลีกเลี่ยงการคำนวณที่ซับซ้อนภายในgetSnapshot
function ใช้ memoize ผลลัพธ์หากจำเป็น - จัดเตรียม
getServerSnapshot
สำหรับ SSR: ตรวจสอบให้แน่ใจว่า HTML เริ่มต้นที่แสดงผลบนเซิร์ฟเวอร์มีข้อมูลที่ถูกต้อง - ใช้ Custom Hooks: ห่อหุ้ม logic ของ
useSyncExternalStore
ไว้ใน custom hooks เพื่อการนำกลับมาใช้ใหม่และการบำรุงรักษาที่ดีขึ้น - จัดการข้อผิดพลาดอย่างสง่างาม: ครอบคลุม
getSnapshot
และgetServerSnapshot
ด้วยบล็อกtry...catch
- ลดการสมัครสมาชิก: สมัครรับข้อมูลเฉพาะส่วนของ external store ที่ component ต้องการจริงๆ สิ่งนี้จะช่วยลดการ re-renders ที่ไม่จำเป็น
- พิจารณาทางเลือก: ประเมินว่า
useSyncExternalStore
จำเป็นจริงๆ หรือไม่ สำหรับกรณีที่ง่าย เทคนิคการจัดการสถานะอื่นๆ อาจเหมาะสมกว่า
ทางเลือกของ useSyncExternalStore
แม้ว่า useSyncExternalStore
จะเป็นเครื่องมือที่มีประสิทธิภาพ แต่ก็ไม่ใช่ทางออกที่ดีที่สุดเสมอไป พิจารณาทางเลือกเหล่านี้:
- การจัดการสถานะในตัว (
useState
,useReducer
, Context API): หากข้อมูลถูกผูกติดกับ React component tree อย่างแน่นหนา ตัวเลือกในตัวเหล่านี้มักจะเพียงพอ - React Query/SWR: สำหรับการดึงข้อมูล ไลบรารีเหล่านี้มีความสามารถในการแคช, การลบล้าง และการจัดการข้อผิดพลาดที่ยอดเยี่ยม
- Zustand/Jotai/Valtio: ไลบรารีการจัดการสถานะแบบมินิมอลลิสต์เหล่านี้มีวิธีที่ง่ายและมีประสิทธิภาพในการจัดการสถานะของแอปพลิเคชัน
- Redux/MobX: สำหรับแอปพลิเคชันที่ซับซ้อนที่มีสถานะส่วนกลาง Redux หรือ MobX อาจเป็นตัวเลือกที่ดีกว่า (แม้ว่าจะเพิ่ม boilerplate มากขึ้นก็ตาม)
การเลือกขึ้นอยู่กับข้อกำหนดเฉพาะของแอปพลิเคชันของคุณ
สรุป
useSyncExternalStore
เป็นส่วนเสริมที่มีคุณค่าสำหรับชุดเครื่องมือของ React ช่วยให้สามารถรวมเข้ากับแหล่งสถานะภายนอกได้อย่างราบรื่น ในขณะที่ยังคงความเข้ากันได้กับ concurrent rendering ด้วยการทำความเข้าใจวัตถุประสงค์, การนำไปใช้ และกรณีการใช้งานขั้นสูง คุณสามารถใช้ประโยชน์จาก hook นี้เพื่อสร้าง React application ที่แข็งแกร่งและมีประสิทธิภาพซึ่งโต้ตอบกับข้อมูลจากแหล่งต่างๆ ได้อย่างมีประสิทธิภาพ
โปรดจำไว้ว่าให้จัดลำดับความสำคัญของประสิทธิภาพ, จัดการข้อผิดพลาดอย่างสง่างาม และพิจารณาทางเลือกอื่นก่อนที่จะเลือกใช้ useSyncExternalStore
ด้วยการวางแผนและการนำไปใช้อย่างรอบคอบ hook นี้สามารถเพิ่มความยืดหยุ่นและพลังของ React application ของคุณได้อย่างมาก
การสำรวจเพิ่มเติม
- เอกสาร React สำหรับ useSyncExternalStore
- ตัวอย่างกับไลบรารีการจัดการสถานะต่างๆ (Zustand, Jotai, Valtio)
- การเปรียบเทียบประสิทธิภาพของ
useSyncExternalStore
กับแนวทางอื่นๆ