หลังจากที่ได้ย้ายจาก 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.
สาเหตุ
ปัญหาข้างบนมาจากที่ว่าเวลาเราทำการ unmouted component ไปแล้วแต่ http request มันยังคงทำงานต่อเพราะมันไม่ได้หยุด request ตัวนั้นแล้วเมื่อมันทำงานเสร็จก็จะไปเซ็ต state อันเก่า
มาลองดูตัวอย่างคร่าวๆกันดีกว่า
useEffect(() => {
(
async () => {
const rawData = await fetch('/some/api')
const data = rawData.json()
setSomeState(data)
}
)()
}, [])
จาก code ข้างบนก็ดูปกติไม่มีอะไรแต่เราเราลองเพิ่ม delay ให้มันสักหน่อยก่อนที่จะให้่ยิง http request
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 ลงไป
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 ในการ 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();
}
}, [])
จาก 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();
const data = await axios.get(
"https://jsonplaceholder.typicode.com/todos",
{ signal: controller.signal }
);
- ky(ตัวที่เป็นปัญหาถ้าผมไม่ได้ทำอะไรผิด)
const data = await ky
.get("https://jsonplaceholder.typicode.com/todos", {
signal: controller.signal,
}).json();
ตัวอย่างทั้งหมดนี้อยู่ใน github -> here
ถ้าเป็น vue ล่ะ
เนื่องจาก vue มี lifecycle ที่คล้ายกับตัว react แต่สุดท้ายการอัพเดต state
แตกต่างกันแต่เราสามารถใช้วิธียกเลิก http request ได้เหมือนกัน
ตัวอย่างของ vue -> here
ถ้าเกิดผมผิดพลาดตรงไหนไปก็ขอภัยด้วยครับสามารถ comment ที่ผมผิดไปหรือจะเสนอแนะอะไรได้เลยครับเนื่องจากเป็น post แรกของผมใน dev.to เลยครับขอบคุณครับ
Top comments (0)