Giới thiệu: Kẻ thù thầm lặng mang tên "Race Condition"
Chào các bạn, là một lập trình viên React, chắc hẳn chúng ta đã quá quen thuộc với hook useEffect. Nó mạnh mẽ, linh hoạt, nhưng đôi khi cũng "ngang trái" không kém, đặc biệt là khi làm việc với các tác vụ bất đồng bộ như fetch dữ liệu. Một trong những "cạm bẫy" mà ít ai để ý đến cho đến khi gặp lỗi, đó chính là Race Condition.
Hãy tưởng tượng bạn đang lướt nhanh qua các trang sản phẩm trên một website thương mại điện tử. Bạn click vào "Áo", rồi ngay lập tức click sang "Quần". Nếu trang "Quần" hiển thị dữ liệu của "Áo" trong một khoảnh khắc, thì đó chính là dấu hiệu của Race Condition. Trong bài viết này, chúng ta sẽ cùng mổ xẻ hiện tượng này trong useEffect và tìm ra các giải pháp "thanh toán" chúng một cách triệt để!
Race Condition là gì và tại sao lại xuất hiện trong useEffect?
Race Condition xảy ra khi nhiều tác vụ bất đồng bộ (async operations) được kích hoạt gần như đồng thời, và kết quả cuối cùng phụ thuộc vào việc tác vụ nào hoàn thành trước. Trong ngữ cảnh của useEffect và fetch dữ liệu, vấn đề thường gặp là:
- Bạn có một
useEffectfetch dữ liệu dựa trên một dependency (ví dụ: ID sản phẩm). - Dependency này thay đổi liên tục (người dùng click nhanh).
- Mỗi lần dependency thay đổi, một yêu cầu fetch mới được gửi đi.
- Nếu yêu cầu cũ hoàn thành sau khi yêu cầu mới đã hoàn thành và cập nhật state, ứng dụng của bạn sẽ hiển thị dữ liệu không chính xác.
- Nghiêm trọng hơn, nếu component bị unmount trong khi một yêu cầu fetch vẫn đang chờ, việc cố gắng cập nhật state trên một component không còn tồn tại có thể gây ra lỗi "Can't perform a React state update on an unmounted component".
Tóm lại, Race Condition là cuộc đua giữa các yêu cầu bất đồng bộ, và không phải lúc nào yêu cầu "chậm" cũng là yêu cầu "đúng".
Giải pháp 1: Sử dụng cờ "mounted" để kiểm soát trạng thái component
Đây là một trong những cách tiếp cận phổ biến và dễ hiểu nhất. Chúng ta sẽ dùng một biến cờ (flag) để theo dõi xem component còn được mount hay không. Biến này sẽ được đặt thành false trong hàm cleanup của useEffect.
Cách thực hiện:
- Khởi tạo một biến boolean, ví dụ
isActive, gán giá trịtrue. - Trong hàm cleanup của
useEffect, đặtisActive = false. - Trước khi cập nhật state sau khi fetch dữ liệu, kiểm tra
if (isActive).
import React, { useState, useEffect } from 'react';
function ProductDetail({ productId }) {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isActive = true; // Cờ theo dõi trạng thái component
const fetchProduct = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (isActive) { // Chỉ cập nhật state nếu component còn được mount
setProduct(data);
}
} catch (err) {
if (isActive) { // Chỉ cập nhật state nếu component còn được mount
setError(err.message);
}
} finally {
if (isActive) { // Chỉ cập nhật state nếu component còn được mount
setLoading(false);
}
}
};
fetchProduct();
return () => {
isActive = false; // Đánh dấu component là không còn mount khi cleanup
};
}, [productId]); // Dependency thay đổi sẽ kích hoạt lại useEffect
if (loading) return Loading product...
;
if (error) return Error: {error}
;
if (!product) return No product found.
;
return (
{product.name}
{product.description}
Price: ${product.price}
);
}
export default ProductDetail;Với cách này, chúng ta đảm bảo rằng dù yêu cầu fetch cũ có hoàn thành muộn hơn yêu cầu mới, hoặc component bị unmount, thì state vẫn sẽ không bị cập nhật sai hoặc gây lỗi.
Giải pháp 2: Hủy bỏ yêu cầu với AbortController (cho Fetch API)
Nếu bạn đang sử dụng Fetch API, AbortController là một công cụ mạnh mẽ và hiện đại để hủy bỏ các yêu cầu HTTP đang chờ xử lý. Điều này không chỉ giúp tránh Race Condition mà còn tối ưu hóa tài nguyên mạng.
Cách thực hiện:
- Tạo một instance mới của
AbortControllertronguseEffect. - Truyền
controller.signalvào tùy chọn của hàmfetch. - Trong hàm cleanup, gọi
controller.abort()để hủy bỏ yêu cầu.
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // Tạo AbortController mới
const signal = controller.signal;
const fetchUser = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/users/${userId}`, { signal }); // Truyền signal vào fetch
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (err) {
if (err.name === 'AbortError') {
console.log('Fetch aborted'); // Yêu cầu đã bị hủy
} else {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchUser();
return () => {
controller.abort(); // Hủy bỏ yêu cầu khi component unmount hoặc dependency thay đổi
};
}, [userId]);
if (loading) return Loading user profile...
;
if (error) return Error: {error}
;
if (!user) return No user found.
;
return (
{user.name}
Email: {user.email}
);
}
export default UserProfile;Khi controller.abort() được gọi, bất kỳ yêu cầu fetch nào đang sử dụng signal đó sẽ bị hủy và một DOMException với name: 'AbortError' sẽ được ném ra. Bạn có thể bắt lỗi này để xử lý riêng biệt.
Lưu ý: Đối với các thư viện HTTP khác như Axios, bạn cũng có các cơ chế tương tự để hủy bỏ yêu cầu (ví dụ: cancel token trong Axios cũ, hoặc tích hợp AbortController trong các phiên bản mới hơn).
Lời kết: Xây dựng ứng dụng React mạnh mẽ hơn
Race Condition có thể là một vấn đề khó chịu, nhưng với các giải pháp phù hợp, chúng ta hoàn toàn có thể kiểm soát và ngăn chặn chúng. Việc sử dụng cờ isActive hoặc AbortController trong useEffect không chỉ giúp ứng dụng của bạn ổn định hơn mà còn mang lại trải nghiệm người dùng mượt mà hơn.
Hãy luôn nhớ rằng, quản lý các tác vụ bất đồng bộ một cách cẩn thận là khóa để xây dựng các ứng dụng React mạnh mẽ và đáng tin cậy. Chúc các bạn code vui vẻ!