นักพัฒนา React ทุกคนต้องเผชิญกับคำถามเดียวกันในที่สุด: ควรจะใช้ Context หรือนี่เป็นปัญหาที่ต้องใช้ Redux? หากคุณเพิ่งเริ่มเขียนมาได้เพียงไม่กี่เดือน ข้อมูลที่สับสนวุ่นวายบนโลกออนไลน์อาจทำให้คุณรู้สึกว่ามันเป็นการตัดสินใจแบบ "อย่างใดอย่างหนึ่ง" บางบทช่วยสอนมองว่า Redux เป็นเพียงภาระที่ล้าสมัย ในขณะที่บางคนเตือนว่า Context ไม่สามารถขยายขนาด (scale) เกินกว่าแอป To-do list ได้ ซึ่งทั้งสองขั้วนี้ไม่ได้ช่วยอะไรเลย ความจริงก็คือ เครื่องมือเหล่านี้ถูกสร้างมาเพื่อแก้ปัญหาที่แตกต่างกัน และการเลือกใช้อย่างชาญฉลาดนั้นขึ้นอยู่กับว่าแอปพลิเคชันของคุณทำอะไรจริงๆ

ปัญหา Prop Drilling

ก่อนที่คุณจะเลือกกลยุทธ์การจัดการ state การทำความเข้าใจ "โรค" ที่เครื่องมือทั้งสองพยายามจะรักษาจะช่วยได้มาก ลองจินตนาการว่าคุณกำลังสร้างเว็บไซต์อีคอมเมิร์ซ คุณดึงข้อมูลโปรไฟล์ของผู้ใช้ที่คอมโพเนนต์ App ระดับบนสุด แต่ในส่วน footer มีคอมโพเนนต์เล็กๆ อย่าง AccountLink ที่ต้องการรูปโปรไฟล์นั้น หากไม่มี global store วัตถุ user จะต้องเดินทางผ่าน Home, จากนั้นไป Header, ต่อด้วย NavContainer, เข้าสู่ UserDropdown และสุดท้ายจึงไปถึง AccountLink ทุกเลเยอร์ที่อยู่ตรงกลางต้องสัมผัสกับข้อมูลที่ตัวเองไม่ได้ใช้งาน นั่นคือสิ่งที่เรียกว่า prop drilling

Prop drilling ทำให้คอมโพเนนต์มีความเปราะบาง การทำ Refactoring จะกลายเป็นเรื่องเสี่ยงเพราะการดึงตัวกลางตัวใดตัวหนึ่งออกจะทำให้สายโซ่ขาดสะบั้น ความสามารถในการนำกลับมาใช้ใหม่ (Reusability) ก็ลดลงเพราะคอมโพเนนต์ต้องรับ props ทั้งที่มันแค่ส่งต่อลงไปข้างล่างเท่านั้น ทั้ง Context และ Redux ต่างช่วยกำจัดปัญหานี้โดยการอนุญาตให้คอมโพเนนต์ที่อยู่ห่างกันสามารถ subscribe ข้อมูลที่ใช้ร่วมกันได้โดยตรง แต่ด้วยวิธีการส่งข้อมูลและต้นทุนในการทำนั้น ทั้งสองอย่างจะเริ่มแตกต่างกันอย่างรวดเร็ว

เมื่อ React Context API คือตัวเลือกที่เหมาะสม

React Context ถูกสร้างมาพร้อมกับตัว library เอง ไม่ต้องติดตั้ง npm เพิ่ม ไม่ต้องตั้งค่า build และไม่มีไฟล์ boilerplate คุณเพียงแค่สร้าง context object, หุ้มส่วนหนึ่งของ tree ด้วย Provider และเรียกใช้ค่าด้วย useContext ในคอมโพเนนต์ที่อยู่ภายใน ด้วยความเรียบง่ายนี้ Context จึงโดดเด่นมากในโปรเจกต์ขนาดเล็กถึงขนาดกลางที่การเปลี่ยนแปลงของ state เกิดขึ้นไม่บ่อยนัก และโครงสร้างของ state นั้นค่อนข้างแบน (flat)

ลองนึกถึง UI themes ผู้ใช้อาจจะสลับระหว่าง light และ dark mode เพียงครั้งเดียวต่อการใช้งานหนึ่งเซสชัน ค่านี้จะถูกส่งต่อไปยังทุก styled component แต่เนื่องจากมันเปลี่ยนน้อยมาก ปัญหาเรื่องประสิทธิภาพจึงแทบไม่ปรากฏ สถานะการยืนยันตัวตน (Authentication status) ก็เป็นอีกหนึ่งกรณีที่เหมาะสม เมื่อผู้ใช้ล็อกอินแล้ว flag isAuthenticated และวัตถุ user จะคงที่ตลอดการเปลี่ยนหน้าหลายสิบครั้ง การตั้งค่าภาษาหรือ localization ก็ทำงานในลักษณะเดียวกัน สิ่งเหล่านี้คือสัญญาณที่ส่งออกไปในวงกว้างและเคลื่อนที่ช้า ซึ่งคอมโพเนนต์จำนวนมากต้องการใช้ แต่มีคอมโพเนนต์เพียงไม่กี่ตัวที่ทำการเปลี่ยนแปลง (mutate) มัน

ข้อควรระวังคือวิธีที่ Context จัดการกับการอัปเดต เมื่อค่าของ Context Provider เปลี่ยนแปลง React จะทำการ re-render ทุกคอมโพเนนต์ที่ใช้งาน context นั้น ในแอปพลิเคชันขนาดเล็กคุณจะไม่รู้สึกถึงความแตกต่าง แต่ในแอปขนาดใหญ่ หากคุณนำข้อมูลที่มีการเปลี่ยนแปลงอย่างรวดเร็วไปไว้ใน Context ที่ถูกใช้งานอย่างแพร่หลาย คุณจะทำให้เกิดการ re-render ต่อเนื่องกันอย่างสิ้นเปลือง (cascade of wasted renders) คุณสามารถแยก context เพื่อแยกส่วนที่มีความผันผวนได้ แต่เมื่อถึงจุดนั้น คุณกำลังต้องมานั่งออกแบบวิธีแก้ปัญหาเรื่องประสิทธิภาพด้วยตัวเอง ทั้งที่เครื่องมืออื่นสามารถจัดการเรื่องนี้ได้อยู่แล้ว

เมื่อ Redux Toolkit คู่ควรกับงานของคุณ

Redux Toolkit ถูกออกแบบมาสำหรับแอปพลิเคชันที่ state มีความซับซ้อน มีการอัปเดตบ่อยครั้ง และมีฟีเจอร์ที่อยู่ห่างกันหลายส่วนที่ต้องอ่านและเขียนข้อมูลชุดเดียวกันโดยไม่ให้เกิดการชนกัน ลองพิจารณาตะกร้าสินค้า ผู้ใช้เพิ่มสินค้าจาก product card ไอคอนตะกร้าใน header ต้องอัปเดตจำนวน badge แถบ sidebar เลื่อนออกมาเพื่อแสดงรายการสินค้า ช่องใส่รหัสส่วนลดต้องมีการตรวจสอบความถูกต้อง (validation) และหน้า checkout จะต้องอ่านข้อมูลในตะกร้าในภายหลัง State ดังกล่าวถูกสัมผัสโดยคอมโพเนนต์ที่ไม่เกี่ยวข้องกันทั่วทั้ง tree และมีการเปลี่ยนแปลง (mutate) อยู่บ่อยครั้ง

Redux Toolkit แก้ปัญหานี้ผ่าน store ส่วนกลางและ slices ของ state ที่ชัดเจน คอมโพเนนต์จะ subscribe เฉพาะส่วนของข้อมูลที่จำเป็นต้องใช้เท่านั้นผ่าน useSelector หากราคาหุ้นมีการอัปเดตใน dashboard แบบ real-time คอมโพเนนต์ที่แสดงการตั้งค่าโปรไฟล์ผู้ใช้ก็จะไม่ถูกปลุกขึ้นมาทำงาน Redux ใช้การตรวจสอบ reference equality อยู่เบื้องหลังเพื่อให้การ subscription มีความละเอียด (granular) ซึ่งสิ่งนี้จะกลายเป็นเรื่องสำคัญมากเมื่อจำนวนคอมโพเนนต์ของคุณเพิ่มขึ้นเป็นหลักร้อย

Redux ยังให้การไหลของข้อมูล (data flow) ที่คาดเดาได้ การเปลี่ยนแปลงของ state เกิดขึ้นผ่านการ dispatch actions ที่จัดการโดย reducers ฟังดูเหมือนศัพท์เทคนิค แต่ในทางปฏิบัติหมายความว่าคุณสามารถใช้ grep ค้นหาใน codebase เพื่อหา addToCart และจะพบทุกเส้นทางของโค้ดที่มีการแก้ไขตะกร้าสินค้า ในทีมขนาดใหญ่ ข้อตกลงนี้จะช่วยป้องกันบั๊ก ในทางตรงกันข้าม Context เป็นเพียงแค่ค่า (value) และตัวตั้งค่า (setter) ผู้ใช้งานคนใดก็ได้สามารถเรียก setState และการตามหาต้นตอของค่าที่ผิดพลาดหมายถึงการต้องไล่เช็ก breakpoint ในคอมโพเนนต์หลายๆ ตัว

จุดที่ทั้งสองแตกต่างกันอย่างแท้จริง

คุณลักษณะด้านประสิทธิภาพ (Performance characteristics) คือสิ่งที่แยกเครื่องมือเหล่านี้ออกจากกันมากกว่าสิ่งอื่นใด Context จะส่งค่าใหม่ไปยังผู้บริโภค (consumers) ทั้งหมดโดยไม่มีเงื่อนไข ในขณะที่ Redux จะแจ้งเตือนเฉพาะผู้ติดตาม (subscribers) ที่ข้อมูลในส่วน (slice) ที่เลือกไว้มีการเปลี่ยนแปลงเท่านั้น หากคุณกำลังสร้างแดชบอร์ดหุ้นแบบเรียลไทม์ที่ราคาอัปเดตทุกวินาที Context จะทำให้เกิดการ re-render ทั้งระบบอย่างรุนแรง (re-render storm) แต่ Redux จะอนุญาตให้เฉพาะเซลล์แสดงราคา (ticker cell) และกราฟเส้น (sparkline chart) เท่านั้นที่คำนวณใหม่

การดีบั๊ก (Debugging) เป็นอีกด้านหนึ่งที่ Redux นำหน้าในแอปพลิเคชันที่มีความซับซ้อน Redux DevTools ช่วยให้คุณทำ time-travel debugging ได้ คุณสามารถย้อนกลับไปดูแต่ละ action ที่ถูก dispatch และดูการย้อนกลับของ state ได้ ในขั้นตอนการชำระเงินที่มีหลายขั้นตอน เช่น การคำนวณค่าขนส่ง การตรวจสอบการชำระเงิน และการกู้คืนข้อผิดพลาด การสามารถเล่นซ้ำลำดับเหตุการณ์ที่นำไปสู่บั๊กได้อย่างแม่นยำนั้นมีค่าอย่างยิ่ง ส่วน Context ต้องพึ่งพา React DevTools มาตรฐาน คุณสามารถตรวจสอบค่า context ปัจจุบันได้ แต่ไม่มี log ของ action หรือตัวดูความแตกต่างของ state (state diff viewer) ในตัว สุดท้ายคุณก็ต้องกลับไปใช้วิธีการใส่ console logs กระจัดกระจายเหมือนเดิม

Middleware และ side effects เป็นส่วนหนึ่งของ DNA ของ Redux โดย Redux Toolkit มาพร้อมกับ createAsyncThunk และทำงานร่วมกับไลบรารีการดึงข้อมูล (data-fetching libraries) ได้อย่างราบรื่น คุณสามารถจัดการการเรียก API, แสดงตัวหมุนโหลด (loading spinner), จัดการเมื่อเครือข่ายล้มเหลว และเก็บแคชผลลัพธ์ ทั้งหมดนี้ทำได้ภายใน data flow ของ Redux ส่วน Context ไม่มีรูปแบบ (pattern) ในตัวสำหรับการจัดการ logic แบบ asynchronous คุณต้องเลือกระหว่างการดึงข้อมูลภายใน component แล้วค่อยส่งผลลัพธ์เข้าไปใน Context หรือไม่ก็ต้องห่อหุ้ม provider ด้วย utility สำหรับ async ที่เขียนขึ้นเอง ซึ่งมันใช้งานได้ แต่มันเป็นการแก้ปัญหาเฉพาะหน้า (ad hoc)

ค่าใช้จ่ายในการตั้งค่า (Setup cost) คือจุดที่ Context ชนะอย่างขาดลอย การสร้าง theme provider ใช้เวลาเพียงประมาณห้านาที แต่ Redux Toolkit ต้องมีการสร้างไฟล์ store, กำหนด slices และห่อหุ้มแอปพลิเคชันของคุณด้วย Provider แม้จะไม่ใช่พิธีกรรมที่ต้องใช้เวลาเป็นสัปดาห์เหมือน Redux สมัยก่อนที่เต็มไปด้วย boilerplate มหาศาล แต่มันก็ยังต้องตั้งค่ามากกว่า Context สำหรับโปรเจกต์เสริมช่วงสุดสัปดาห์หรือแดชบอร์ดที่มีเพียงสามเส้นทาง (routes) ค่าใช้จ่ายส่วนเกิน (overhead) นี้อาจไม่คุ้มค่า

การใช้ทั้งสองอย่างในแอปพลิเคชันเดียวกัน

คุณไม่จำเป็นต้องเลือกข้างใดข้างหนึ่ง แอปพลิเคชันที่ใช้งานจริงจำนวนมากใช้ Context สำหรับเรื่อง UI shell ส่วนกลาง และใช้ Redux สำหรับข้อมูลทางธุรกิจที่มีความซับซ้อน (domain-heavy business data) รูปแบบที่พบบ่อยคือการเก็บ theme, locale และอาจรวมถึง auth flag แบบเบาๆ ไว้ใน Context เพราะทุก route จำเป็นต้องใช้และข้อมูลเหล่านี้เปลี่ยนแปลงไม่บ่อยนัก ในขณะเดียวกัน ระบบจัดการคำสั่งซื้อ, ศูนย์แจ้งเตือน และตารางข้อมูล จะอยู่ใน Redux ซึ่งต้องการการควบคุมที่แม่นยำเนื่องจากการอัปเดตที่เกิดขึ้นบ่อยและการจัดการ logic ข้าม component

แนวทางแบบไฮบริดนี้ช่วยให้เรื่องง่ายๆ ยังคงง่ายอยู่ โดยไม่ต้องบังคับใช้ Redux store เต็มรูปแบบกับ object ของ theme ที่อยู่นิ่งๆ นอกจากนี้ยังช่วยป้องกันไม่ให้ Redux slices ของคุณเต็มไปด้วย UI chrome ที่ไม่จำเป็นต้องใช้การจัดการ state ระดับอุตสาหกรรมตั้งแต่แรก

บทสรุปที่แท้จริง

การเลือกใช้เครื่องมือที่หนักกว่าไม่ใช่เรื่องน่าภาคภูมิใจ เริ่มต้นด้วยการดูว่า state ของคุณเปลี่ยนแปลงบ่อยแค่ไหน มี component มากน้อยเพียงใดที่เข้าถึงมัน และคุณจำเป็นต้องติดตามการเปลี่ยนแปลง (mutations) ข้ามขอบเขตของทีมหรือไม่ หากคุณกำลังจัดการกับค่าที่เปลี่ยนแปลงช้าและใช้ร่วมกันอย่างกว้างขวางในแอปพลิเคชันขนาดปานกลาง Context ก็น่าจะเพียงพอแล้ว แต่ถ้า state ของคุณเปลี่ยนแปลงบ่อย ครอบคลุมฟีเจอร์ที่ไม่เกี่ยวข้องกัน และต้องการประวัติการตรวจสอบ (audit trail) ที่ชัดเจน Redux Toolkit จะช่วยลดความยุ่งยากให้คุณได้

เลือกตามลักษณะของโปรเจกต์ ไม่ใช่ตามการพูดในงานสัมมนาหรือจำนวน GitHub stars ตะกร้าสินค้าที่มีสินค้าถึงห้าสิบชิ้นไม่ได้หมายความว่าต้องใช้ Redux เสมอไป และการสลับ theme ก็ไม่จำเป็นต้องมี global store เลือกเครื่องมือให้เหมาะกับปัญหา แล้ว codebase ของคุณจะยังคงดูแลรักษาได้ง่าย แม้ว่ากระแสความนิยมจะผ่านพ้นไปแล้วก็ตาม