Cancel http request when component is unmounted
Tags: react, vue, thai
Published: 2021-11-07 08:42:57

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

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

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

```log
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.
```

## สาเหตุ

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

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

```js
useEffect(() => {
 (
   async () => {
     const rawData = await fetch('/some/api')
     const data = rawData.json()
     setSomeState(data) 
   }
 )()
}, [])
```

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

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

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

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

## วิธีแก้
- เช็คว่า component unmounted ไปยังถ้า unmounted ไปแล้วไม่ให้เซ็ต state ลงไป

```js
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
 }
}, [])
```

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

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

ใช้ตัว [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) ในการ cancel http request ครับ

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

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

```js
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();
 }
}, [])
```

จาก 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 

- [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)

```js
 const rawData = await fetch(
  "https://jsonplaceholder.typicode.com/todos/",
  { signal: controller.signal }
 );
 const data = await rawData.json();
```

- [axios](https://github.com/axios/axios)

```js
 const data = await axios.get(
   "https://jsonplaceholder.typicode.com/todos",
   { signal: controller.signal }
 );
```

- [ky](https://github.com/sindresorhus/ky)(ตัวที่เป็นปัญหาถ้าผมไม่ได้ทำอะไรผิด)

```js
const data = await ky
.get("https://jsonplaceholder.typicode.com/todos", {
   signal: controller.signal,
 }).json();
```

ตัวอย่างทั้งหมดนี้อยู่ใน github -> [here](https://github.com/jungai/cancel-request-in-react)

**ถ้าเป็น vue ล่ะ**

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

ตัวอย่างของ vue -> [here](https://github.com/jungai/cancel-request-in-vue)

---

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