DEV Community

Woraphol Wananiyakul
Woraphol Wananiyakul

Posted on

Cancel http request when component is unmounted

หลังจากที่ได้ย้ายจาก vue มา react ได้ประมาณ6เดือน บางปัญหาที่ไม่เคยเจอใน vue เลยมาเจอจากตัว react และบทความนี้ก็เป็น1ในปัญหาที่ผมได้เจอตอนเขียน react จริงๆจัง

ที่มาของปัญหา

เวลาเราต้องสร้าง component บางตัวแล้วเราต้องการให้มันยิง http request เพื่อไปเอาข้อมูลบางอย่างมาเราจะทำตอนที่ component นั้น mount ขึ้นมาแล้วยิงไปจังหวะนั้น ดูรวมๆแล้วก็ปกติไม่มีอะไรแต่ถ้า component นั้นถูก unmount ไปในตอนที่มันยังยิง http request ไม่เสร็จมันจะเกิด error ขึ้น

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Enter fullscreen mode Exit fullscreen mode

สาเหตุ

ปัญหาข้างบนมาจากที่ว่าเวลาเราทำการ unmouted component ไปแล้วแต่ http request มันยังคงทำงานต่อเพราะมันไม่ได้หยุด request ตัวนั้นแล้วเมื่อมันทำงานเสร็จก็จะไปเซ็ต state อันเก่า

มาลองดูตัวอย่างคร่าวๆกันดีกว่า

useEffect(() => {
 (
   async () => {
     const rawData = await fetch('/some/api')
     const data = rawData.json()
     setSomeState(data) 
   }
 )()
}, [])
Enter fullscreen mode Exit fullscreen mode

จาก code ข้างบนก็ดูปกติไม่มีอะไรแต่เราเราลองเพิ่ม delay ให้มันสักหน่อยก่อนที่จะให้่ยิง http request

useEffect(() => {
 (
   async () => {
     setTimeout(async () => {
       const rawData = await fetch('/some/api')
       const data = rawData.json()
       setSomeState(data) 
     }, 2000)
   }
 )()
}, [])
Enter fullscreen mode Exit fullscreen mode

ก็ยังดูปกติเมื่อ component ถูก mount ขึ้นมาจะรอ 2 วินาทีหลังจากนั้นก็ยิงเพื่อไปเอาข้อมูล แต่ช้าก่อนถ้าเกิดระหว่างนี้ component ถูก unmount ล่ะ ??

flow ข้างบนก็ยังคงดำเนินต่อไปจนจบแม้ว่า component ถูก unmounted ไปแล้ว ทีนี้ react มันไม่ให้เรา update state ตอนเมื่อ component ถูก unmounted ไปแล้วนั่นเอง

วิธีแก้

  • เช็คว่า component unmounted ไปยังถ้า unmounted ไปแล้วไม่ให้เซ็ต state ลงไป
useEffect(() => {
 let isUnmounted = false
 (
   async () => {
     setTimeout(async () => {
       const rawData = await fetch('/some/api')
       const data = rawData.json()

       if (isUnmounted) return;

       setSomeState(data)

     }, 2000)
   }
 )()

 () => {
   isUnmounted = true
 }
}, [])
Enter fullscreen mode Exit fullscreen mode

code ข้างบนก็เป็นวิธีแก้อย่างหนึ่ง แต่การแก้ข้างบนจะแก้แค่ปัญหาที่ว่า react ไม่ให้อัพเดต state ตอน unmounted ส่วนปัญหาที่ http request ยังคงยิงไปไม่หยุดยังคงอยู่

  • แก้แบบยั่งยืน

ใช้ตัว AbortController ในการ cancel http request ครับ

ข้อดีของมันคือ มันสามารถที่จะยกเลิก request และก้ browser support (อมก)

ทีนี้ก็เอาตัว abortController มา implement

useEffect(() => {
 const controller = new AbortController();
 (
   async () => {
     setTimeout(async () => {
       const rawData = await fetch('/some/api', {
        signal: controller.signal
       })
       const data = rawData.json()
       setSomeState(data)

     }, 2000)
   }
 )()

 () => {
    controller.abort();
 }
}, [])
Enter fullscreen mode Exit fullscreen mode

จาก code ข้างบน เราจะใช้ new AbortController() ที่นี้เมื่อ unmounted เราจะให้ abort() แล้วตัวตอน fetch ก็จะใส่ options signal ไป

signal ตัวนี้จะเปลี่ยนเมื่อเรา call abort() ทำให้ตอนที่เรายิง htttp request ไปแล้ว signal เป็น false มันจะ cancel http request นั้นทันที (เช็คได้ใน devtools ขึ้้น error หรือดูในแถบ network จะขึ้น cancel) สิ่งที่ต้องทำเพิ่มก็คือไปเขียน trycatch เพื่อดักerror ว่ามันเป็น abort

เพิ่มเติ่ม

ตัว AbortController รองรับกับ http client หลายเจ้าเพียงแค่เราใส่ตัว options ให้มันแต่จะมีบางตัวที่ใช้เราแล้วเหมือนจะเป็น bug

 const rawData = await fetch(
  "https://jsonplaceholder.typicode.com/todos/",
  { signal: controller.signal }
 );
 const data = await rawData.json();
Enter fullscreen mode Exit fullscreen mode
 const data = await axios.get(
   "https://jsonplaceholder.typicode.com/todos",
   { signal: controller.signal }
 );
Enter fullscreen mode Exit fullscreen mode
  • ky(ตัวที่เป็นปัญหาถ้าผมไม่ได้ทำอะไรผิด)
const data = await ky
.get("https://jsonplaceholder.typicode.com/todos", {
   signal: controller.signal,
 }).json();
Enter fullscreen mode Exit fullscreen mode

ตัวอย่างทั้งหมดนี้อยู่ใน github -> here

ถ้าเป็น vue ล่ะ

เนื่องจาก vue มี lifecycle ที่คล้ายกับตัว react แต่สุดท้ายการอัพเดต state
แตกต่างกันแต่เราสามารถใช้วิธียกเลิก http request ได้เหมือนกัน

ตัวอย่างของ vue -> here


ถ้าเกิดผมผิดพลาดตรงไหนไปก็ขอภัยด้วยครับสามารถ comment ที่ผมผิดไปหรือจะเสนอแนะอะไรได้เลยครับเนื่องจากเป็น post แรกของผมใน dev.to เลยครับขอบคุณครับ

Top comments (0)