Haliviq
React

useEffect cleanup ทำงาน 2 รอบใน Strict Mode ไม่ใช่บั๊กที่ต้องแก้

ตอนพัฒนา React 18 จะ mount, unmount แล้ว mount ซ้ำให้ตั้งใจ เพื่อจับ effect ที่รันซ้ำแล้วให้ผลไม่เหมือนเดิม ถ้ารันสองรอบแล้วพัง เช่น subscribe ซ้ำหรือยิง request สองครั้ง แสดงว่า effect นั้นไม่ปลอดภัยอยู่แล้ว ให้แก้ cleanup ไม่ใช่ปิด Strict Mode

เกิดอะไรขึ้น

ตอนทำหน้าหนึ่งใน development useEffect ของเรารันสองรอบตั้งแต่โหลดครั้งแรก เปิด subscription ซ้ำสองอัน และยิง request ซ้ำสองครั้ง ความคิดแรกของเราคือปิด Strict Mode เพื่อให้มันหยุด

แต่สุดท้ายพบว่า Strict Mode ไม่ได้ทำให้เกิดบั๊ก มันแค่ทำให้เราเห็นบั๊กที่มีอยู่แล้ว

ทำไมถึงเป็นแบบนั้น

ใน development ตั้งแต่ React 18 Strict Mode จะจำลองว่า component ถูก unmount แล้ว mount ใหม่ คือรัน effect, รัน cleanup, แล้วรัน effect อีกรอบ โดย state ยังอยู่เหมือนเดิม และเกิดเฉพาะตอน development ไม่มีใน production build

จุดประสงค์คือเช็กว่าทุก effect มี cleanup ที่ย้อนสิ่งที่ setup ไว้ได้ครบ effect ที่เขียนถูกต้องจะทำงานเหมือนกันไม่ว่ารันรอบเดียวหรือ setup, cleanup, setup ถ้าเกิด subscription ซ้ำ แปลว่า cleanup ขาดหรือไม่ครบ และบั๊กเดียวกันจะโผล่ใน production ตอน component ถูก remount จริง เช่นผู้ใช้เปลี่ยนหน้าไปแล้วกลับมา

ตอนนี้เราทำอย่างไร

เขียน cleanup ให้ย้อนสิ่งที่ effect ทำให้ครบ เช่น unsubscribe, เคลียร์ timer, ปิด connection, ยกเลิก fetch สำหรับการดึงข้อมูล ให้ใช้ AbortController หรือ flag สำหรับข้าม response ที่หมดอายุ request ซ้ำใน dev ก็จะไม่มีผลเสีย และ library อย่าง React Query หรือ SWR ก็ช่วย dedupe ให้ได้

ส่วนงานที่ควรเกิดครั้งเดียวต่อหนึ่งการกระทำของผู้ใช้ เช่นส่งออเดอร์หรือส่ง event การซื้อ ไม่ควรอยู่ใน effect ให้ย้ายไปไว้ใน event handler

effect ที่ cleanup ครบ รันซ้ำได้อย่างปลอดภัยtsx
useEffect(() => {
  const controller = new AbortController()

  fetch('/api/items', { signal: controller.signal })
    .then(res => res.json())
    .then(setItems)
    .catch(err => {
      if (err.name !== 'AbortError') console.error(err)
    })

  return () => controller.abort()
}, [])

เช็กลิสต์สั้น ๆ

  1. 1อ่าน effect แล้วลิสต์ทุกอย่างที่มัน setup ไว้
  2. 2เขียน cleanup ย้อนทีละรายการ
  3. 3ยกเลิก fetch ที่ยังค้างอยู่ด้วย AbortController
  4. 4ย้ายงานที่เกิดจากการกระทำของผู้ใช้ไปไว้ใน event handler
  5. 5เปิด Strict Mode ทิ้งไว้

จำไว้สั้น ๆ

ถ้า effect พังเมื่อรันซ้ำ ให้แก้ cleanup ไม่ใช่ปิด Strict Mode

เราอยากฟังว่าคุณกำลังสร้างอะไรอยู่

คุยกับเรา