Bạn có thể đã nghe về useSyncExternalStore()
chưa, đây là một hook mới trong React 18 để đăng ký theo dõi các nguồn dữ liệu bên ngoài. Nó thường được sử dụng nội bộ bởi các thư viện quản lý trạng thái – như Redux – để triển khai hệ thống selector.
Trong bài viết tương tác này, mình muốn đưa ra một vấn đề: các hook React trả về dư thừa gây ra những lần re-render không cần thiết. Chúng ta sẽ cùng xem cách useSyncExternalStore()
có thể là một giải pháp hợp lý.
Hook trả về dư thừa
Hãy cùng minh họa vấn đề với useLocation()
từ React-Router.
Hook này trả về một object với nhiều thuộc tính (pathname, hash, search...), nhưng bạn có thể không sử dụng hết tất cả. Chỉ cần gọi hook này thôi cũng sẽ kích hoạt re-render mỗi khi bất kỳ thuộc tính nào trong số đó được cập nhật.
Hãy xem xét ví dụ ứng dụng sau:
function CurrentPathname() { const { pathname } = useLocation(); return <div>{pathname}</div>;
} function CurrentHash() { const { hash } = useLocation(); return <div>{hash}</div>;
} function Links() { return ( <div> <Link to="#link1">#link1</Link> <Link to="#link2">#link2</Link> <Link to="#link3">#link3</Link> </div> );
} function App() { return ( <div> <CurrentPathname /> <CurrentHash /> <Links /> </div> );
}
Mỗi khi bạn click vào một đường dẫn có hash
, CurrentPathname
sẽ bị re-render, dù nó chẳng hề sử dụng thuộc tính hash
Dùng useSyncExternalStore để cứu nguy?
Tài liệu chính thức nói rằng:
useSyncExternalStore
là một hook được khuyến nghị để đọc và đăng ký theo dõi dữ liệu từ các nguồn bên ngoài theo cách tương thích với các tính năng kết xuất đồng thời như hydration chọn lọc và time slicing. Phương thức này trả về giá trị của store và chấp nhận ba đối số:
- subscribe: hàm đăng ký một callback sẽ được gọi mỗi khi store thay đổi.
- getSnapshot: hàm trả về giá trị hiện tại của store.
- getServerSnapshot: hàm trả về snapshot được sử dụng trong quá trình kết xuất trên server.
function useSyncExternalStore<Snapshot>( subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => Snapshot, getServerSnapshot?: () => Snapshot,
): Snapshot;
Cảm giác hơi trừu tượng phải không? Trang tài liệu beta này đưa ra một ví dụ khá tốt:
function subscribe(callback) { window.addEventListener("online", callback); window.addEventListener("offline", callback); return () => { window.removeEventListener("online", callback); window.removeEventListener("offline", callback); };
} function useOnlineStatus() { return useSyncExternalStore( subscribe, () => navigator.onLine, () => true, );
} function ChatIndicator() { const isOnline = useOnlineStatus(); // ...
}
Hóa ra, lịch sử trình duyệt cũng có thể được coi là một nguồn dữ liệu bên ngoài. Hãy cùng xem cách sử dụng useSyncExternalStore
với React-Router nhé!
UseHistorySelector()
React-Router đã cung cấp tất cả những gì chúng ta cần để kết nối với useSyncExternalStore
:
- Truy cập lịch sử trình duyệt với
useHistory()
: Hook này giúp bạn lấy được đối tượng history để thao tác với lịch sử. - Đăng ký lắng nghe các cập nhật lịch sử với history.listen(callback): Hàm này cho phép bạn đăng ký một callback để nhận thông báo mỗi khi có thay đổi trong lịch sử trình duyệt.
- Truy cập snapshot của vị trí hiện tại với history.location: Thuộc tính này trả về thông tin về URL hiện tại trong trình duyệt.
Cách dùng useHistory()
rất đơn giản:
function useHistorySelector(selector) { const history = useHistory(); return useSyncExternalStore(history.listen, () => selector(history), );
}
Hãy dùng cho app của bạn như sau:
function CurrentPathname() { const pathname = useHistorySelector( (history) => history.location.pathname, ); return <div>{pathname}</div>;
} function CurrentHash() { const hash = useHistorySelector( (history) => history.location.hash, ); return <div>{hash}</div>;
}
Giờ đây, khi bạn nhấp vào một liên kết hash phía trên, component CurrentPathname sẽ không còn bị re-render nữa!
Kết luận
Mình hy vọng bài viết này đã thuyết phục bạn nhìn nhận lại useSyncExternalStore()
. Mình cảm thấy hook này hiện tại chưa được sử dụng nhiều trong hệ sinh thái React, và xứng đáng nhận được sự chú ý hơn nữa. Có rất nhiều nguồn dữ liệu bên ngoài mà bạn có thể đăng ký theo dõi.
Nếu bạn vẫn chưa nâng cấp lên React 18, hiện tại bạn có thể sử dụng một shim use-sync-external-store cho các phiên bản cũ hơn. Ngoài ra, cũng có một export use-sync-external-store/with-selector
trong trường hợp bạn cần trả về một giá trị không phải nguyên thủy và đã memoized.