1. Đặt vấn đề
có bao giờ các bạn tự hỏi rằng khi bạn có 1 list user như hình bên dưới, mỗi khi click vào 1 user thì gọi sẽ call tới api tương ứng để lấy dữ liệu của user đó hiển thị ra selected user. Và khi tôi liên tục nhấn liên tiếp vào các user khác nhau trong môi trường mạng chậm thì các lần gọi api trước sẽ trôi về đâu?

code demo mình để ở đây
import { useEffect, useState } from "react"; import "./App.css"; import { User } from "./types"; const URL = "https://jsonplaceholder.typicode.com"; function App() { const [users, setUsers] = useState<User[]>([]); const [selectedUser, setSelectedUser] = useState<User>(); const [userId, setUserId] = useState<number>(); const handleClick = (userId: number) => { setUserId(userId); }; useEffect(() => { const fetchUsers = async () => { try { const response = await fetch(`${URL}/users`); const data = await response.json(); setUsers(data); } catch (err) { console.log(err); } }; fetchUsers(); }, []); useEffect(() => { if (userId) { const fetchUser = async () => { try { const response = await fetch(`${URL}/users/${userId}`); const data = await response.json(); setSelectedUser(data); } catch (err) { console.log(err); } }; fetchUser(); } }, [userId]); return ( <> <h1>List Users</h1> <p style={{ color: "red" }}> <strong>selected user: {selectedUser ? selectedUser.name : ""}</strong> </p> <ul> {users.map((user) => ( <li key={user.id} onClick={() => handleClick(user.id)} style={{ cursor: "pointer" }} > {user.name} </li> ))} </ul> </> ); } export default App;
link demo repo mình để ở đây
https://github.com/longnguyendev/call-api-demo
2. vấn đề đầu tiên

👀 Câu hỏi đặt ra? Ta nhận thấy rằng với lần render đầu tiên tại sao lại có tận 2 lần gọi cùng một api get users. Tại sao điều đó lại xảy ra?.
- giải thích: từ phiên bản react18 đã bổ sung một tính năng mới là
StrictMode, chế độ này lúc mới ra đã gây ra rất nhiều tranh cãi, nhiều người hiểu lầm rằng nó gây ra lỗi call api lặp lại không cần thiết. Nhưng thực tế đây chỉ là một chế độ trong môi trường phát triển (development) và hoàn toàn không ảnh hưởng gì đến môi trường sản phẩm (production), nó giúp chúng ta phát triển ứng dụng react một cách nghiêm ngặt hơn bằng cách tự động mount và unmount các component ngay lần đầu tiên render để các lập trình viên sớm phát hiện các lỗi liên quan đến hiệu năng nhưfetchapi, các lắng nghe sự kiện nhưsetTimeOut(),setInterval(), đảm bảo chúng phải được remove trước khi bị unmount khỏiDOM.
3. Giải quyết vấn đề đầu tiên
Để giải quyết vấn đề đầu tiên, chúng ta có thể cải thiện
fetchUsers bằng cách sử dụng AbortController , AbortController là một đối tượng trong JavaScript được sử dụng để hủy bỏ các hoạt động bất đồng bộ, chẳng hạn như các yêu cầu mạng (fetch) hoặc các tác vụ khác. Nó cung cấp một cách để gửi tín hiệu hủy bỏ (abort) đến các hoạt động đang diễn ra, giúp quản lý tài nguyên và tránh các hành động không cần thiết khi chúng không còn được yêu cầu.useEffect(() => { const controller = new AbortController(); const { signal } = controller; const fetchUsers = async () => { try { const response = await fetch(`${URL}/users`, { signal, }); const data = await response.json(); setUsers(data); } catch (err) { console.log(err); } }; fetchUsers(); return () => controller.abort(); }, []);
Sử dụng cleanup function trong
useEffect để gọi lệnh hủy bỏ controller.abort() ngay trước khi component bị unmount khỏi DOM💰Kết quả đạt được Kết quả là lần fetch dữ liệu đầu tiên đã bị hủy (canceled) như hình bên dưới

4. Vấn đề thứ 2
Giả sử chúng ta đặt ra một tình huống mạng chậm như hình bên dưới

Khi mà các lần fetch dữ liệu xảy ra rất chậm dẫn đến việc hiển thị selected user diễn ra không đồng thời và phải mất một khoảng thời gian để hiển thị trên giao diện. Vậy khi lần fetch dữ liệu trước chưa hoàn tất mà ta lại click vào một vài user khác liên tiếp thì chuyện gì sẽ xảy ra

Nhưng chúng ta có thể thấy một loạt các lần fetch dữ liệu khác được thêm vào hàng đợi và vẫn đang chờ để được xử lý, điều này dẫn đến việc hiển thị selected user bị nhấp nháy bởi những lần gọi dữ liệu trước rồi mới hiển thị tên của user mà ta chọn cuối cùng. Vậy đó có phải là điều mà ta mong đợi?. Rõ ràng là không rồi vì khi ta đã chọn một user khác thì ta sẽ không quan tâm việc gọi dữ liệu của user trước để hiển thị nữa, và nếu việc gọi dữ liệu trước vẫn xử lý trong khi người dùng có một yêu cầu xử lý khác dẫn đến việc không tối ưu sử dụng tài nguyên internet. Điều ta mong đợi là lần gọi dữ liệu trước đó nên bị hủy bỏ và chỉ xử lý lần gọi dữ liệu cuối cùng mà thôi
5. Giải quyết vấn đề thứ 2
Tương tự như cách giải quyết trước ta làm theo như demo bên dưới
useEffect(() => { const controller = new AbortController(); const { signal } = controller; if (userId) { const fetchUser = async () => { try { const response = await fetch(`${URL}/users/${userId}`, { signal, }); const data = await response.json(); setSelectedUser(data); } catch (err) { console.log(err); } }; fetchUser(); } return () => controller.abort(); }, [userId]);
💰Kết quả đạt được Kết quả đạt được là các câu gọi dữ liệu trước đó bị hủy bỏ ngay khi có câu gọi dữ liệu mới

6. Tổng kết
Trong bài viết này, chúng ta đã tìm hiểu về một số vấn đề khi gọi dữ liệu từ phía client, biết được
AbortController là gì và cách sử dụng của nó trong việc cải thiện việc gọi dữ liệu. Nếu bạn thấy bài viết này hữu ích, hãy chia sẻ bài viết này để nhiều người biết đến AbortController hơn nhé.