React 18 đã bổ sung useSyncExternalStore, một hook cho phép các component đọc dữ liệu từ bất kỳ nguồn nào nằm ngoài React tree—như browser API, WebSockets, hoặc các store của bên thứ ba—mà không gặp phải các lỗi hiển thị (visual glitches) thường thấy ở pattern cũ là sao chép dữ liệu đó bằng useStateuseEffect. Nếu bạn từng thấy giao diện (UI) bị nhấp nháy vì một phần hiển thị kích thước cửa sổ cũ trong khi phần khác đã cập nhật kích thước mới, thì hook này chính là giải pháp mà bạn đang mong đợi.

Tại sao state bên ngoài cần một hook mới

Các component trong React luôn có khả năng đăng ký (subscribe) vào những thứ tồn tại bên ngoài phạm vi của chúng: đối tượng window, một Redux store, hoặc một kết nối WebSocket. Cách tiếp cận truyền thống là thiết lập một subscription bên trong useEffect, đẩy các giá trị nhận được vào local state thông qua setState, và để React re-render. Cách này hoạt động tốt khi React render đồng bộ, nhưng React 18 đã giới thiệu concurrent rendering, nơi một component có thể được render nhiều lần trước khi trình duyệt thực sự vẽ (paint) lên màn hình. Ở chế độ đó, pattern "sao chép vào local state" có thể gây ra hiện tượng tearing—các phần khác nhau của UI đọc các snapshot khác nhau của cùng một giá trị bên ngoài trong cùng một chu kỳ render.

useSyncExternalStore được xây dựng để ngăn chặn hiện tượng tearing. Nó yêu cầu nguồn bên ngoài cung cấp một snapshot (giá trị hiện tại) và một hàm subscribe (cách để nhận thông báo khi có thay đổi). React sẽ gọi subscribe khi component mount và tự động unsubscribe khi unmount. Bất cứ khi nào snapshot được trả về bởi getSnapshot thay đổi, React sẽ lập lịch một lượt render để đọc cùng một snapshot cho toàn bộ component tree, đảm bảo tính nhất quán.

Hai hàm bắt buộc

Hàm Chức năng
subscribe Nhận một callback phải được gọi bất cứ khi nào giá trị bên ngoài thay đổi. Nó trả về một hàm unsubscribe mà React sẽ gọi khi dọn dẹp (cleanup).
getSnapshot Trả về giá trị hiện tại từ nguồn bên ngoài. Nó phải ổn định về tham chiếu (referentially stable)—tức là cùng một đối tượng hàm trong mỗi lần render—để React có thể so sánh các snapshot một cách chính xác.

Một bản triển khai tối giản cho trạng thái online của trình duyệt trông như sau:

function useOnlineStatus() {
  return useSyncExternalStore(
    (onChange) => {
      window.addEventListener('online', onChange);
      window.addEventListener('offline', onChange);
      return () => {
        window.removeEventListener('online', onChange);
        window.removeEventListener('offline', onChange);
      };
    },
    () => navigator.onLine
  );
}

Khi bất kỳ component nào gọi useOnlineStatus(), React sẽ chỉ re-render nó khi navigator.onLine thực sự thay đổi, và mọi lần render đều sẽ thấy cùng một giá trị.

Các quy tắc để tránh làm hỏng hook

  1. Chỉ trả về phần dữ liệu nhỏ nhất mà component cần.
    Nếu external store chứa một object lớn nhưng component chỉ quan tâm đến một field, hãy chỉ trả về field đó. Việc trả về dữ liệu lớn hơn sẽ gây ra các lần re-render không cần thiết.

  2. Memoize getSnapshot.
    Nếu bạn tạo lại hàm này trong mỗi lần render, React sẽ coi đó là một nguồn mới và có thể gây ra vòng lặp vô tận. Hãy bọc nó trong useCallback hoặc định nghĩa nó bên ngoài component.

  3. Tránh trả về các object mới trong mỗi lần gọi.
    Việc trả về một object mới (ví dụ: { count: store.getCount() }) sẽ tạo ra một tham chiếu mới trong mỗi lần render, khiến React nghĩ rằng snapshot đã thay đổi và kích hoạt vòng lặp render vô tận. Hãy sử dụng các kiểu dữ liệu nguyên thủy (primitives), chuỗi, số, hoặc các object đã được memoize.

  4. Đừng dùng nó cho state nội bộ của component.
    useState vẫn là công cụ phù hợp cho dữ liệu chỉ tồn tại duy nhất bên trong component. useSyncExternalStore sẽ tạo thêm overhead không cần thiết cho local state.

Khi nào nên sử dụng

  • Browser APIs – kích thước cửa sổ, media queries, navigator.onLine, trạng thái pin.
  • Các store không phụ thuộc framework (Framework-agnostic stores) – Zustand, Redux, MobX, hoặc bất kỳ custom store nào có cung cấp subscription API.
  • Các nguồn có thể thay đổi (mutable sources) cập nhật bên ngoài React – tin nhắn WebSocket, các sự kiện thay đổi IndexedDB, thông báo từ Service Worker.

Nếu nguồn dữ liệu của bạn đã nằm trong React (ví dụ: state của một component cha), hãy tiếp tục sử dụng useState hoặc context.

Cộng đồng nói gì

Những người dùng đầu tiên báo cáo rằng useSyncExternalStore đã loại bỏ hiện tượng nhấp nháy mà họ gặp phải khi thay đổi kích thước cửa sổ trong thiết lập concurrent-rendering. Một số thư viện đã chuyển đổi các hook nội bộ của họ sang API này, hứa hẹn hành vi dễ dự đoán hơn trên các phiên bản React khác nhau. Sự đánh đổi là áp lực tư duy (mental load) cao hơn một chút: lập trình viên phải chú ý đến tính ổn định của hai hàm và tránh trả về các object mới trong mỗi lần gọi.

Những gì cần theo dõi tiếp theo

Các bản phát hành sắp tới của React có thể thắt chặt các quy tắc xung quanh useSyncExternalStore, có khả năng bổ sung các trình hỗ trợ (helpers) tích hợp sẵn cho các API trình duyệt phổ biến. Hãy theo dõi blog chính thức của React để cập nhật về bất kỳ sự thay đổi nào liên quan đến việc loại bỏ mô hình mô phỏng useEffect cũ. Trong thời gian chờ đợi, hook này đã ổn định và là một phần của public API, vì vậy bạn có thể yên tâm refactor mã nguồn quản lý trạng thái bên ngoài (external-state) hiện có.

Trải nghiệm nhanh

Một bản demo trực tiếp trình diễn việc đồng bộ kích thước cửa sổ (window-size syncing), phát hiện trạng thái trực tuyến/ngoại tuyến (online/offline detection) và một Zustand store đơn giản có tại https://usesyncexternalstore.vercel.app/. Mã nguồn, đi kèm với các chú thích đầy đủ, có sẵn tại https://github.com/dev48v/usesyncexternalstore.

Điểm mấu chốt: useSyncExternalStore cung cấp cho React một cầu nối đáng tin cậy tới bất kỳ dữ liệu có thể thay đổi (mutable data) nào nằm ngoài chu kỳ render của nó, giúp ngăn chặn hiện tượng tearing và các lần render không cần thiết. Hãy áp dụng nó cho các nguồn bên ngoài, giữ cho snapshot ở mức tối giản và ổn định, và để React đảm nhận phần việc nặng nhọc nhất là duy trì tính nhất quán.