เจาะลึก React's experimental_useContextSelector hook: เพิ่มประสิทธิภาพและจัดการสถานะแอปที่ซับซ้อน เลือกข้อมูลที่คอมโพเนนต์ต้องการจาก Context ป้องกัน re-render เกินจำเป็น
React experimental_useContextSelector: การใช้ Context แบบละเอียด (Fine-Grained Context Consumption)
Context API ของ React เป็นกลไกที่มีประสิทธิภาพสำหรับการแชร์สถานะและพร็อพทั่วทั้งแอปพลิเคชันของคุณโดยไม่จำเป็นต้องมีการส่งพร็อพต่อกัน (prop drilling) อย่างชัดเจน อย่างไรก็ตาม การใช้งาน Context API เริ่มต้นบางครั้งอาจนำไปสู่ปัญหาด้านประสิทธิภาพได้ โดยเฉพาะอย่างยิ่งในแอปพลิเคชันขนาดใหญ่และซับซ้อนที่ค่าของ context มีการเปลี่ยนแปลงบ่อยครั้ง แม้ว่าคอมโพเนนต์จะขึ้นอยู่กับเพียงส่วนเล็กๆ ของ context เท่านั้น การเปลี่ยนแปลงใดๆ ในค่าของ context จะทำให้คอมโพเนนต์ทั้งหมดที่ใช้ context นั้นถูก re-render ซึ่งอาจนำไปสู่การ re-render ที่ไม่จำเป็นและปัญหาคอขวดด้านประสิทธิภาพได้
เพื่อแก้ไขข้อจำกัดนี้ React ได้เปิดตัว hook ที่ชื่อว่า experimental_useContextSelector
(ปัจจุบันยังอยู่ในสถานะทดลอง ตามชื่อของมัน) hook นี้ช่วยให้คอมโพเนนต์สามารถสมัครรับข้อมูล (subscribe) เฉพาะส่วนของ context ที่ต้องการเท่านั้น ซึ่งช่วยป้องกันการ re-render เมื่อส่วนอื่นๆ ของ context เปลี่ยนแปลง วิธีการนี้ช่วยเพิ่มประสิทธิภาพได้อย่างมากโดยลดจำนวนการอัปเดตคอมโพเนนต์ที่ไม่จำเป็น
ทำความเข้าใจปัญหา: Context API แบบคลาสสิกกับการ Re-render
ก่อนที่จะลงรายละเอียดเกี่ยวกับ experimental_useContextSelector
เรามาแสดงให้เห็นถึงปัญหาด้านประสิทธิภาพที่อาจเกิดขึ้นกับ Context API มาตรฐานกัน พิจารณา user context ทั่วโลกที่จัดเก็บข้อมูลผู้ใช้ การตั้งค่า และสถานะการยืนยันตัวตน:
const UserContext = React.createContext({
userInfo: {
name: 'John Doe',
email: 'john.doe@example.com',
country: 'USA'
},
preferences: {
theme: 'light',
language: 'en-US',
notificationsEnabled: true
},
isAuthenticated: false
});
function App() {
const [user, setUser] = React.useState({
userInfo: {
name: 'John Doe',
email: 'john.doe@example.com',
country: 'USA'
},
preferences: {
theme: 'light',
language: 'en-US',
notificationsEnabled: true
},
isAuthenticated: false
});
const updateUser = (newUser) => {
setUser(newUser);
};
return (
);
}
function Profile() {
const { userInfo } = React.useContext(UserContext);
return (
{userInfo.name}
Email: {userInfo.email}
Country: {userInfo.country}
);
}
function Settings() {
const { preferences, updateUser } = React.useContext(UserContext);
const toggleTheme = () => {
updateUser({
...user,
preferences: { ...preferences, theme: preferences.theme === 'light' ? 'dark' : 'light' },
});
};
return (
Theme: {preferences.theme}
);
}
ในสถานการณ์นี้ คอมโพเนนต์ Profile
ใช้เพียงพร็อพเพอร์ตี้ userInfo
เท่านั้น ในขณะที่คอมโพเนนต์ Settings
ใช้พร็อพเพอร์ตี้ preferences
และ updateUser
หากคอมโพเนนต์ Settings
อัปเดตธีม ทำให้เกิดการเปลี่ยนแปลงในออบเจกต์ preferences
คอมโพเนนต์ Profile
ก็จะถูก re-render ด้วยเช่นกัน แม้ว่าจะไม่ได้ขึ้นอยู่กับ preferences
เลยก็ตาม นี่เป็นเพราะว่า React.useContext
จะสมัครรับข้อมูล (subscribe) คอมโพเนนต์ทั้งหมดของค่า context การ re-render ที่ไม่จำเป็นนี้อาจกลายเป็นปัญหาคอขวดด้านประสิทธิภาพที่สำคัญในแอปพลิเคชันที่ซับซ้อนมากขึ้นซึ่งมีผู้ใช้ context จำนวนมาก
ขอแนะนำ experimental_useContextSelector: การใช้ Context แบบเลือกสรร
hook experimental_useContextSelector
มอบวิธีแก้ปัญหานี้โดยอนุญาตให้คอมโพเนนต์เลือกเฉพาะส่วนที่ต้องการจาก context เท่านั้น hook นี้รับสองอาร์กิวเมนต์:
- ออบเจกต์ context (ที่สร้างด้วย
React.createContext
) - ฟังก์ชัน selector ที่รับค่า context ทั้งหมดเป็นอาร์กิวเมนต์ และส่งคืนค่าเฉพาะที่คอมโพเนนต์ต้องการ
คอมโพเนนต์จะ re-render เฉพาะเมื่อค่าที่เลือกเปลี่ยนแปลง (โดยใช้การเปรียบเทียบแบบเข้มงวด ===
) ซึ่งช่วยให้เราสามารถปรับปรุงตัวอย่างก่อนหน้านี้และป้องกันการ re-render ที่ไม่จำเป็นของคอมโพเนนต์ Profile
ได้
การปรับปรุงตัวอย่างด้วย experimental_useContextSelector
นี่คือวิธีที่เราสามารถปรับปรุงตัวอย่างก่อนหน้านี้โดยใช้ experimental_useContextSelector
:
import { unstable_useContextSelector as useContextSelector } from 'use-context-selector';
const UserContext = React.createContext({
userInfo: {
name: 'John Doe',
email: 'john.doe@example.com',
country: 'USA'
},
preferences: {
theme: 'light',
language: 'en-US',
notificationsEnabled: true
},
isAuthenticated: false
});
function App() {
const [user, setUser] = React.useState({
userInfo: {
name: 'John Doe',
email: 'john.doe@example.com',
country: 'USA'
},
preferences: {
theme: 'light',
language: 'en-US',
notificationsEnabled: true
},
isAuthenticated: false
});
const updateUser = (newUser) => {
setUser(newUser);
};
return (
);
}
function Profile() {
const userInfo = useContextSelector(UserContext, (context) => context.userInfo);
return (
{userInfo.name}
Email: {userInfo.email}
Country: {userInfo.country}
);
}
function Settings() {
const preferences = useContextSelector(UserContext, (context) => context.preferences);
const updateUser = useContextSelector(UserContext, (context) => context.updateUser);
const toggleTheme = () => {
updateUser({
...user,
preferences: { ...preferences, theme: preferences.theme === 'light' ? 'dark' : 'light' },
});
};
return (
Theme: {preferences.theme}
);
}
ในตัวอย่างที่ปรับปรุงใหม่นี้ คอมโพเนนต์ Profile
ใช้ useContextSelector
เพื่อเลือกเฉพาะพร็อพเพอร์ตี้ userInfo
จาก context ดังนั้น เมื่อคอมโพเนนต์ Settings
อัปเดตธีม คอมโพเนนต์ Profile
จะไม่ถูก re-render อีกต่อไป เนื่องจากพร็อพเพอร์ตี้ userInfo
ยังคงไม่เปลี่ยนแปลง ในทำนองเดียวกัน คอมโพเนนต์ Settings
จะเลือกเฉพาะพร็อพเพอร์ตี้ preferences
และ updateUser
ที่ต้องการ ซึ่งช่วยเพิ่มประสิทธิภาพให้ดียิ่งขึ้น
ข้อควรทราบที่สำคัญ: อย่าลืมนำเข้า (import) unstable_useContextSelector
จากแพ็กเกจ use-context-selector
ตามชื่อที่แนะนำ hook นี้ยังอยู่ในสถานะทดลอง และอาจมีการเปลี่ยนแปลงใน React เวอร์ชันอนาคต แพ็กเกจ use-context-selector
เป็นทางเลือกที่ดีในการเริ่มต้น แต่โปรดระวังการเปลี่ยนแปลง API ที่อาจเกิดขึ้นในอนาคตจากทีม React เมื่อฟีเจอร์นี้กลายเป็นเวอร์ชันที่เสถียร
ประโยชน์ของการใช้ experimental_useContextSelector
- ประสิทธิภาพที่ดีขึ้น: ลดการ re-render ที่ไม่จำเป็นโดยการอัปเดตคอมโพเนนต์เฉพาะเมื่อค่า context ที่เลือกเปลี่ยนแปลงเท่านั้น สิ่งนี้มีประโยชน์อย่างยิ่งสำหรับแอปพลิเคชันที่ซับซ้อนซึ่งมีข้อมูล context เปลี่ยนแปลงบ่อยครั้ง
- การควบคุมที่ละเอียด: ให้การควบคุมที่แม่นยำว่าคอมโพเนนต์จะสมัครรับข้อมูล (subscribe) ส่วนใดของ context
- ตรรกะคอมโพเนนต์ที่เรียบง่ายขึ้น: ทำให้การทำความเข้าใจเกี่ยวกับการอัปเดตคอมโพเนนต์ง่ายขึ้น เนื่องจากคอมโพเนนต์จะ re-render เมื่อการพึ่งพาเฉพาะของพวกมันเปลี่ยนแปลงเท่านั้น
ข้อควรพิจารณาและแนวปฏิบัติที่ดีที่สุด
- ประสิทธิภาพของฟังก์ชัน Selector: ตรวจสอบให้แน่ใจว่าฟังก์ชัน selector ของคุณมีประสิทธิภาพและหลีกเลี่ยงการคำนวณที่ซับซ้อนหรือการดำเนินการที่สิ้นเปลืองทรัพยากรภายในฟังก์ชันเหล่านั้น ฟังก์ชัน selector จะถูกเรียกทุกครั้งที่ context เปลี่ยนแปลง ดังนั้นการเพิ่มประสิทธิภาพจึงเป็นสิ่งสำคัญ
- Memoization: หากฟังก์ชัน selector ของคุณส่งคืนออบเจกต์หรืออาร์เรย์ใหม่ทุกครั้งที่ถูกเรียก แม้ว่าข้อมูลพื้นฐานจะไม่ได้เปลี่ยนแปลง คอมโพเนนต์ก็จะยังคงถูก re-render พิจารณาใช้เทคนิค memoization (เช่น
React.useMemo
หรือไลบรารีอย่าง Reselect) เพื่อให้แน่ใจว่าฟังก์ชัน selector จะส่งคืนค่าใหม่เฉพาะเมื่อข้อมูลที่เกี่ยวข้องมีการเปลี่ยนแปลงจริงเท่านั้น - โครงสร้างค่า Context: พิจารณาจัดโครงสร้างค่า context ของคุณในลักษณะที่ลดโอกาสที่ข้อมูลที่ไม่เกี่ยวข้องกันจะเปลี่ยนแปลงพร้อมกัน ตัวอย่างเช่น คุณอาจแยกแง่มุมต่างๆ ของสถานะแอปพลิเคชันของคุณออกเป็น context แยกกัน
- ทางเลือกอื่น: สำรวจโซลูชันการจัดการสถานะทางเลือกอื่นๆ เช่น Redux, Zustand หรือ Jotai หากความซับซ้อนของแอปพลิเคชันของคุณต้องการ ไลบรารีเหล่านี้มีคุณสมบัติขั้นสูงเพิ่มเติมสำหรับการจัดการสถานะส่วนกลางและเพิ่มประสิทธิภาพ
- สถานะทดลอง: โปรดทราบว่า
experimental_useContextSelector
ยังคงอยู่ในสถานะทดลอง API อาจเปลี่ยนแปลงได้ใน React เวอร์ชันอนาคต แพ็กเกจuse-context-selector
มีการใช้งานที่เสถียรและเชื่อถือได้ แต่ควรตรวจสอบการอัปเดตของ React เสมอสำหรับการเปลี่ยนแปลงที่อาจเกิดขึ้นกับ API หลัก
ตัวอย่างและการใช้งานจริง
- การจัดการธีม (Theme Management): ในแอปพลิเคชันที่มีธีมที่ปรับแต่งได้ คุณสามารถใช้
experimental_useContextSelector
เพื่อให้คอมโพเนนต์สามารถสมัครรับข้อมูลเฉพาะการตั้งค่าธีมปัจจุบันเท่านั้น ป้องกันการ re-render เมื่อการตั้งค่าแอปพลิเคชันอื่นเปลี่ยนไป ตัวอย่างเช่น พิจารณาเว็บไซต์อีคอมเมิร์ซที่นำเสนอธีมสีที่แตกต่างกันสำหรับผู้ใช้ทั่วโลก คอมโพเนนต์ที่แสดงเฉพาะสี (ปุ่ม, พื้นหลัง ฯลฯ) จะสมัครรับข้อมูลเฉพาะพร็อพเพอร์ตี้theme
ภายใน context เท่านั้น หลีกเลี่ยงการ re-render ที่ไม่จำเป็นเมื่อตัวอย่างเช่น การตั้งค่าสกุลเงินของผู้ใช้เปลี่ยนไป - การทำให้เป็นสากล (Internationalization - i18n): เมื่อจัดการการแปลในแอปพลิเคชันหลายภาษา คุณสามารถใช้
experimental_useContextSelector
เพื่อให้คอมโพเนนต์สามารถสมัครรับข้อมูลเฉพาะภาษาปัจจุบันหรือการแปลที่เฉพาะเจาะจง ตัวอย่างเช่น ลองนึกภาพแพลตฟอร์มโซเชียลมีเดียระดับโลก การแปลโพสต์เดียว (เช่น จากภาษาอังกฤษเป็นสเปน) ไม่ควรทำให้เกิดการ re-render ของฟีดข่าวทั้งหมดหากการแปลของโพสต์นั้นเปลี่ยนแปลงไปเท่านั้นuseContextSelector
ช่วยให้แน่ใจว่าเฉพาะคอมโพเนนต์ที่เกี่ยวข้องเท่านั้นที่ได้รับการอัปเดต - การยืนยันตัวตนผู้ใช้ (User Authentication): ในแอปพลิเคชันที่ต้องการการยืนยันตัวตนผู้ใช้ คุณสามารถใช้
experimental_useContextSelector
เพื่อให้คอมโพเนนต์สามารถสมัครรับข้อมูลเฉพาะสถานะการยืนยันตัวตนของผู้ใช้เท่านั้น ป้องกันการ re-render เมื่อข้อมูลโปรไฟล์ผู้ใช้อื่นๆ เปลี่ยนไป ตัวอย่างเช่น คอมโพเนนต์สรุปบัญชีของแพลตฟอร์มธนาคารออนไลน์อาจขึ้นอยู่กับuserId
จาก context เท่านั้น หากผู้ใช้อัปเดตที่อยู่ในการตั้งค่าโปรไฟล์ คอมโพเนนต์สรุปบัญชีไม่จำเป็นต้อง re-render ซึ่งนำไปสู่ประสบการณ์ผู้ใช้ที่ราบรื่นยิ่งขึ้น - การจัดการฟอร์ม (Form Management): เมื่อจัดการฟอร์มที่ซับซ้อนซึ่งมีหลายช่อง คุณสามารถใช้
experimental_useContextSelector
เพื่อให้แต่ละช่องฟอร์มสามารถสมัครรับข้อมูลเฉพาะค่าของตนเองเท่านั้น ป้องกันการ re-render เมื่อช่องอื่นเปลี่ยนไป ลองนึกภาพแบบฟอร์มใบสมัครหลายขั้นตอนสำหรับวีซ่า แต่ละขั้นตอน (ชื่อ, ที่อยู่, รายละเอียดหนังสือเดินทาง) สามารถแยกออกจากกันได้ และจะ re-render เฉพาะเมื่อข้อมูลภายในขั้นตอนนั้นเปลี่ยนแปลงเท่านั้น แทนที่จะเป็นการ re-render ทั้งฟอร์มหลังจากอัปเดตแต่ละช่อง
experimental_useContextSelector
เป็นเครื่องมืออันทรงคุณค่าสำหรับการปรับปรุงประสิทธิภาพของแอปพลิเคชัน React ที่ใช้ Context API ด้วยการอนุญาตให้คอมโพเนนต์เลือกเฉพาะส่วนที่ต้องการจาก context มันช่วยป้องกันการ re-render ที่ไม่จำเป็นและปรับปรุงการตอบสนองโดยรวมของแอปพลิเคชัน แม้จะยังอยู่ในสถานะทดลอง แต่มันเป็นส่วนเสริมที่มีแนวโน้มที่ดีสำหรับระบบนิเวศของ React และคุ้มค่าที่จะสำรวจสำหรับแอปพลิเคชันที่เน้นประสิทธิภาพ โปรดจำไว้เสมอว่าต้องทดสอบอย่างละเอียดและตระหนักถึงการเปลี่ยนแปลง API ที่อาจเกิดขึ้นเมื่อ hook นี้เติบโตเต็มที่ พิจารณาให้เป็นส่วนเสริมที่ทรงพลังในชุดเครื่องมือ React ของคุณเมื่อต้องรับมือกับการจัดการสถานะที่ซับซ้อนและปัญหาคอขวดด้านประสิทธิภาพที่เกิดจากการอัปเดต context บ่อยครั้ง ด้วยการวิเคราะห์การใช้งาน context ของแอปพลิเคชันของคุณอย่างรอบคอบและนำ experimental_useContextSelector
ไปใช้อย่างมีกลยุทธ์ คุณจะสามารถปรับปรุงประสบการณ์ผู้ใช้อย่างมีนัยสำคัญและสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพและปรับขนาดได้ดียิ่งขึ้น