useSyncExternalStore cho phép một component đọc dữ liệu nằm ngoài React mà không gặp phải các lỗi hiển thị (visual glitches) mà quá trình render đồng thời (concurrent rendering) có thể gây ra. Nó thay thế mô hình cũ là đưa các giá trị bên ngoài vào state cục bộ bằng useEffect, một mô hình có thể gây ra hiện tượng "tearing" – khi các phần của UI hiển thị các giá trị khác nhau trong cùng một lần render.
Tại sao mô hình cũ bị lỗi khi chạy đồng thời
Trước React 18, công thức phổ biến là:
const [value, setValue] = useState(initial);
useEffect(() => {
const unsubscribe = externalSource.subscribe(v => setValue(v));
return unsubscribe;
}, []);
Effect chạy sau khi component đã render. Khi bộ render đồng thời của React quyết định tạm dừng, hủy bỏ hoặc chạy lại một lần render, effect có thể kích hoạt tại một thời điểm khác với UI đang đọc state. Kết quả là sự không khớp: một component có thể đã đọc giá trị cũ, component khác lại đọc giá trị mới, và màn hình hiển thị dữ liệu không nhất quán trong chốc lát. Hiện tượng "tearing" này vô hại trong thế giới đồng bộ thuần túy nhưng sẽ trở thành một lỗi hiển thị rõ rệt khi React render nhiều phiên bản của UI song song.
Cách useSyncExternalStore giải quyết vấn đề
Hook này yêu cầu hai callback:
- subscribe – đăng ký một listener và trả về một hàm để hủy đăng ký.
- getSnapshot – trả về giá trị hiện tại của nguồn bên ngoài.
React gọi subscribe khi component mount và hủy nó khi unmount. Trong mỗi lần render, nó gọi getSnapshot và đảm bảo rằng tất cả các component sử dụng cùng một external store đều đọc được cùng một snapshot chính xác. Vì việc đọc diễn ra trong giai đoạn render, bộ render có thể điều phối các cập nhật trên toàn bộ cây component, loại bỏ hiện tượng tearing.
Các thư viện như Zustand và Redux đã bao bọc các store của họ bằng hook này, đó là lý do tại sao chúng hoạt động ngay lập tức với các tính năng concurrent của React 18.
Các quy tắc để tránh hook hoạt động sai
- Chỉ trả về những gì component cần – một object lớn sẽ buộc React phải so sánh các cấu trúc sâu trong mỗi lần cập nhật. Một snapshot nhỏ gọn sẽ giúp việc phát hiện thay đổi trở nên rẻ (chi phí thấp).
- Giữ nguyên định danh tham chiếu (referential identity) – nếu giá trị bên ngoài không thay đổi, getSnapshot phải trả về cùng một tham chiếu object. Việc trả về một object mới trong mỗi lần gọi sẽ kích hoạt vòng lặp render vô hạn vì React thấy có sự thay đổi trong mỗi lần render.
- Chỉ giới hạn sử dụng cho các nguồn bên ngoài – state cục bộ của component nên thuộc về
useStatehoặcuseReducer. Sử dụng hook external-store cho các giá trị thuần túy nội bộ sẽ làm tăng sự gián tiếp không cần thiết.
Khi nào nên dùng useSyncExternalStore
- Bao bọc các Browser APIs (kích thước window, trạng thái mạng, media queries).
- Kết nối với một JavaScript store thuần túy mà không biết về React.
- Thay thế prop-drilling bằng một nguồn chỉ đọc (read-only) toàn cục mà nhiều component có thể subscribe.
Lập luận ngược lại
Mô hình useState + useEffect vẫn hoạt động tốt cho các dữ liệu đơn giản, ít thay đổi, đặc biệt là khi ứng dụng không bật các tính năng concurrent. Đây là một cách tiếp cận cấp thấp hơn mà một số nhà phát triển thấy dễ hiểu hơn vì logic đăng ký (subscription) nằm rõ ràng trong effect. Tuy nhiên, sự đánh đổi là rủi ro bị tearing khi chế độ render đồng thời được bật. Đối với các dự án đã sử dụng các khả năng mới của React 18, sự an toàn bổ sung của useSyncExternalStore xứng đáng với lượng boilerplate tăng thêm không đáng kể.
Những điều cần lưu ý tiếp theo
- Xác minh rằng getSnapshot của store trả về một tham chiếu ổn định; việc
console.lognhanh tham chiếu trong quá trình phát triển có thể giúp phát hiện việc vô tình tạo lại object. - Giữ hàm subscription nhẹ nhàng – các tác vụ nặng nên diễn ra bên trong nguồn bên ngoài, không phải trong callback được truyền vào React.
- Kiểm tra các component dưới chế độ concurrent của React (ví dụ: sử dụng
actvớiReactDOM.createRoot) để xác nhận rằng không có hiện tượng tearing nào xuất hiện.
Một bản demo trực tiếp cho thấy hook hoạt động như thế nào, đồng bộ hóa một bộ đếm đơn giản được lưu trữ bên ngoài React và cập nhật nhiều component mà không gặp lỗi hiển thị. Bản demo có tại https://usesyncexternalstore.vercel.app/. Mã nguồn có trên GitHub, và một bài viết dài hơn sẽ giải thích chi tiết về cách triển khai.
Takeaway: Trong một thế giới mà React có thể render các phần của UI không theo thứ tự, useSyncExternalStore là cầu nối đáng tin cậy giữa dữ liệu bên ngoài và việc render component. Nó duy trì sự nhất quán về mặt hình ảnh, có khả năng mở rộng cho các store toàn cục và đã được tích hợp sẵn trong các thư viện quản lý state phổ biến. Hãy áp dụng nó bất cứ khi nào bạn cần đọc state không thuộc React, và hãy giữ các snapshot nhỏ gọn và ổn định để đạt được toàn bộ lợi ích của nó.
