One Space per page.Space.fromUri fetches the ref + the Manifest; cache that in state, don't re-open per render.
Pagination is client-side.samples() returns the whole materialized list — the SDK's eager fan-out. For >100K samples switch to Dataset.iterStream server-side or space.itemsInRange for windowed reads.
Each thumb has its own useEffect. The cleanup function revokes the URL when the sample changes or the component unmounts. Without that you leak bytes.
revoked flag. Handles the late-arriving fetch (resolved after unmount) so you don't try to revoke a URL that was never published.
Going further
The full demo app at dreamdb-workspace/demo/ shows:
IntersectionObserver-based lazy loading
React Query for content-addressed caching (staleTime: Infinity on Manifest hashes; finite on mutable refs)
Zustand for the bucket-list state with localStorage persistence