Vì useEffect dùng giá trị trả về của callback làm hàm cleanup, mà hàm async luôn trả về một Promise. React sẽ cầm Promise đó và cố gọi nó như một hàm khi dọn dẹp.
// Sai: React nhận Promise thay vì hàm cleanup
useEffect(async () => {
const res = await fetch(url)
setData(await res.json())
}, [url])
// Đúng: khai báo hàm async bên trong rồi gọi
useEffect(() => {
const ac = new AbortController()
async function load() {
try {
const res = await fetch(url, { signal: ac.signal })
setData(await res.json())
} catch (err) {
if (err.name !== 'AbortError') setError(err)
}
}
load()
return () => ac.abort()
}, [url])Phần AbortController không phải trang trí. Effect có thể chạy lại trước khi request cũ xong — người dùng gõ nhanh vào ô tìm kiếm là đủ. Không huỷ thì hai response về không theo thứ tự, và bản cũ ghi đè bản mới.
Bắt riêng AbortError cũng cần thiết: huỷ request làm fetch reject, và nếu bạn đẩy thẳng vào setError thì giao diện hiện lỗi cho một thao tác hoàn toàn bình thường.
Lưu ý: Với ứng dụng thật, cân nhắc TanStack Query hoặc SWR thay vì tự viết. Huỷ request, chống trùng lặp, cache và retry đều đã có sẵn — và đó cũng là mấy thứ tự viết dễ sót nhất.