React ต้องการให้ component ของคุณคาดเดาได้ (predictable). หากให้ state และ props แบบเดิม มันควรจะวาด UI แบบเดิมทุกครั้ง. แต่แอปพลิเคชันจริงส่วนใหญ่ไม่สามารถอยู่แค่ในโลกสมมตินั้นได้. พวกมันต้องเชื่อมต่อกับโลกภายนอก. Dashboard ต้องการตัวเลขใหม่จาก server. Chat widget ต้องคอยฟังข้อความ. Timer ต้องเดิน. การทำงานเหล่านี้คือ side effects ซึ่งอยู่นอก render cycle ของ React. useEffect hook คือที่ที่คุณจะเอาการทำงานที่ยุ่งเหยิงและคาดเดาไม่ได้เหล่านี้ไปไว้ เพื่อให้ตัว component เองยังคงความบริสุทธิ์ (pure) และคาดเดาได้

Side Effects: อะไรบ้างที่ควรอยู่ใน useEffect

Side effect คืออะไรก็ตามที่ไปแตะต้องโลกภายนอกนอกเหนือจากการ return JSX. Render phase ของ React ควรจะเป็น pure. เมื่อคุณเริ่มดึงข้อมูล (fetching data), เขียนค่าลงใน global variables, หรือแนบ listener เข้ากับ DOM คุณได้ก้าวออกจากเขตแดนของ pure แล้ว

ตัวอย่างที่พบบ่อย ได้แก่:

  • การดึงข้อมูลจาก API
  • การตั้งค่า timer หรือ interval
  • การเพิ่ม event listener ให้กับ window หรือ document
  • การอัปเดตชื่อแท็บของเบราว์เซอร์ (browser tab title)
  • การเชื่อมต่อกับ WebSockets

งานเหล่านี้มีลักษณะร่วมกันอย่างหนึ่งคือ: พวกมันไม่ควรอยู่ใน return statement ของ component หรืออยู่ใน logic การ render หลัก การพยายามเรียก browser API อย่าง setInterval โดยตรงภายใน render body จะทำให้มันทำงานทุกครั้งที่มีการ render ซึ่งจะสร้าง timer ซ้ำซ้อนและทำให้เกิดพฤติกรรมที่สับสน useEffect ถูกสร้างขึ้นมาเพื่อแยกงานเหล่านี้ออกมาและรันในจังหวะที่ถูกต้อง

Dependency Array ควบคุมจังหวะเวลาอย่างไร

อาร์กิวเมนต์ตัวที่สองของ useEffect คือ dependency array และมันคือสาเหตุหลักที่ทำให้เหล่านักพัฒนาที่ย้ายมาจาก class components เกิดความสับสน ให้คิดซะว่ามันคือชุดของตัวแปรที่ React คอยเฝ้าดู เพื่อตัดสินใจว่าจะข้ามหรือจะรัน effect ของคุณหลังจาก render รอบปัจจุบัน

มี 3 รูปแบบที่คุณจะได้ใช้ซ้ำๆ

ไม่มี dependency array เลย หากคุณละเว้น array ไปเลย React จะถือว่าคุณต้องการให้ effect ทำงานหลังจากการ render ทุกครั้ง รวมถึงครั้งแรกด้วย ซึ่งนี่ไม่ใช่สิ่งที่คุณต้องการบ่อยนัก หาก effect ของคุณมีการทำ network request หรือการจัดการ DOM ที่หนักหน่วง การรันมันทุกครั้งที่มีการกดปุ่มหรือเปลี่ยน state จะทำให้ประสิทธิภาพ (performance) ตกลงอย่างมาก ใช้รูปแบบนี้เฉพาะเมื่อคุณต้องการให้รันซ้ำจริงๆ เพราะ prop หรือ state ใดๆ อาจเปลี่ยนไปและคุณไม่สามารถระบุเจาะจงได้ว่าตัวไหน

Array ว่าง [] สิ่งนี้บอกให้ React รัน effect เพียงครั้งเดียว ทันทีหลังจากที่ component mount และ DOM พร้อมใช้งาน นี่คือที่ที่เหมาะสมสำหรับการดึงข้อมูลเริ่มต้น (initial data fetches) ตัวอย่างเช่น หาก component ของคุณโหลดข้อมูลโปรไฟล์ผู้ใช้ คุณต้องการให้ request นั้นทำงานเพียงครั้งเดียวเมื่อหน้าโปรไฟล์ปรากฏขึ้น ไม่ใช่ทุกครั้งที่ผู้ใช้โต้ตอบกับฟอร์มที่อยู่ด้านล่างของหน้า

Array ที่มีตัวแปรเฉพาะเจาะจง [count] นี่คือเครื่องมือที่แม่นยำ React จะเปรียบเทียบค่าปัจจุบันของ dependencies เหล่านี้กับค่าของพวกมันระหว่างการ render ครั้งล่าสุด หากมีตัวใดตัวหนึ่งเปลี่ยนไป effect จะทำงาน หากไม่มีอะไรในรายการเปลี่ยนไป React จะข้าม effect นั้นไปเลย

หากคุณกำลังซิงค์ชื่อแท็บเบราว์เซอร์กับตัวแปร state คุณควรใส่ตัวแปรนั้นไว้ใน dependency array จากนั้น React จะอัปเดตชื่อแท็บเฉพาะเมื่อค่านั้นเปลี่ยนไปเท่านั้น หากคุณไม่ใส่ไว้ ชื่อแท็บก็จะไม่อัปเดต (stale) แต่ถ้าคุณใส่ตัวแปร state ที่ไม่เกี่ยวข้องลงไป คุณก็จะเสียรอบการทำงาน (cycles) ไปกับการอัปเดตชื่อแท็บสำหรับการเปลี่ยนแปลงที่ไม่ได้สำคัญอะไร

การ Cleanup ไม่ใช่ทางเลือก (แต่เป็นสิ่งที่ต้องทำ)

effect บางอย่างทิ้งร่องรอยเอาไว้ เช่น timer ที่ยังคงนับต่อไป, event listener ที่ยังคงทำงานอยู่, หรือ WebSocket ที่ยังเปิดค้างไว้ เมื่อ component ของคุณ unmount หรือแม้แต่เมื่อ effect ทำงานซ้ำเพราะ dependencies เปลี่ยนไป React จะไม่ทำความสะอาดสิ่งที่ค้างอยู่จาก effect ก่อนหน้าให้โดยอัตโนมัติ นั่นคือหน้าที่ของคุณ

คุณสามารถสร้าง cleanup function ได้โดยการ return function ออกมาจากภายใน useEffect React จะเรียกใช้ cleanup นี้ก่อนที่จะเริ่มใช้ effect ถัดไป และเรียกอีกครั้งเมื่อ component ถูกถอดออกจากหน้าจอ

คุณควรใช้ cleanup สำหรับ:

  • การล้าง intervals หรือ timeouts ด้วย clearInterval หรือ clearTimeout
  • การลบ event listeners ที่เพิ่มเข้าไปใน window, document, หรือ external nodes
  • การยกเลิกการติดตาม (unsubscribing) จาก data streams หรือ services

หากละเลยสิ่งนี้ คุณจะเจอกับปัญหา memory leaks เช่น component mount ขึ้นมา, แนบ scroll listener, แล้ว unmount ออกไป แต่ listener ยังคงอยู่ เบราว์เซอร์จะยังคงถือ callback และ DOM nodes ที่อ้างถึงไว้ เมื่อเวลาผ่านไป โดยเฉพาะใน single-page applications ที่มีการเปลี่ยนหน้าไปมาบ่อยๆ "ผี" เหล่านี้จะสะสมมากขึ้นและทำให้แท็บช้าลง วิธีแก้ไขมักจะมีเพียงไม่กี่บรรทัด นั่นคือการ return function ที่ทำหน้าที่ลบสิ่งที่คุณเพิ่มเข้าไปนั่นเอง

ข้อผิดพลาดที่พบบ่อยซึ่งมักหลุดไปถึง Production

แม้แต่นักพัฒนาที่มีประสบการณ์ก็ยังเลือกใช้ useEffect ทั้งที่มีทางเลือกที่ง่ายกว่า นี่คือ 3 รูปแบบที่ควรเป็นสัญญาณเตือนในระหว่างการทำ code review

Infinite loops. อย่าอัปเดต state variable ภายใน useEffect หากตัวแปรนั้นอยู่ใน dependency array ของคุณ เว้นแต่ว่าคุณจะมีเงื่อนไข (gate condition) ที่ช่วยหยุดวงจรนั้น หากคุณอ่านค่า count แล้วทำการเพิ่มค่า (increment) จากนั้นระบุ count เป็น dependency ตัว React จะตรวจพบการเปลี่ยนแปลง แล้วทำการ re-render จากนั้นก็รัน effect อีกครั้ง เพิ่มค่าอีกครั้ง และทำให้เบราว์เซอร์ค้าง

Unnecessary effects. อย่าใช้ useEffect เพื่อคำนวณค่าจาก props หรือ state ที่มีอยู่แล้ว หากคุณสามารถคำนวณค่า (derive) ได้โดยตรงในระหว่างการ render ก็ให้ทำแบบนั้นเลย ค่าที่ได้จากการคำนวณ (Derived values) ควรอยู่ในส่วนของ component body หรือใช้การคำนวณแบบ memoized ด้วย useMemo การย้ายค่าเหล่านี้ไปไว้ใน effect จะทำให้ logic ของคุณถูกแยกออกจากกันระหว่างช่วง render และช่วง effect โดยไม่เกิดประโยชน์ใดๆ และยังทำให้โค้ดอ่านเข้าใจยากขึ้นด้วย

The wrong tool for user actions. `use