A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.

AtReact Hooks Deep Dive#

Overview#

The AtReact hooks system provides a robust, cache-optimized layer for fetching AT Protocol data. All hooks follow React best practices with proper cleanup, cancellation, and stable references.


Core Architecture Principles#

1. Three-Tier Caching Strategy#

All data flows through three cache layers:

  • DidCache - DID documents, handle mappings, PDS endpoints
  • BlobCache - Media/image blobs with reference counting
  • RecordCache - AT Protocol records with deduplication

2. Concurrent Request Deduplication#

When multiple components request the same data, only one network request is made. Uses reference counting to manage in-flight requests.

3. Stable Reference Pattern#

Caches use memoized snapshots to prevent unnecessary re-renders:

// Only creates new snapshot if data actually changed
if (existing && existing.did === did && existing.handle === handle) {
    return toSnapshot(existing); // Reuse existing
}

4. Three-Tier Fallback for Bluesky#

For app.bsky.* collections:

  1. Try Bluesky appview API (fastest, public)
  2. Fall back to Slingshot (microcosm service)
  3. Finally query PDS directly

Hook Catalog#

1. useDidResolution#

Purpose: Resolves handles to DIDs or fetches DID documents

Key Features:#

  • Bidirectional: Works with handles OR DIDs
  • Smart Caching: Only fetches if not in cache
  • Dual Resolution Paths:
    • Handle → DID: Uses Slingshot first, then appview
    • DID → Document: Fetches full DID document for handle extraction

State Flow:#

Input: "alice.bsky.social" or "did:plc:xxx"
  
Check didCache
  
If handle: ensureHandle(resolver, handle)  DID
If DID: ensureDidDoc(resolver, did)  DID doc + handle from alsoKnownAs
  
Return: { did, handle, loading, error }

Critical Implementation Details:#

  • Normalizes input to lowercase for handles
  • Memoizes input to prevent effect re-runs
  • Stabilizes error references - only updates if message changes
  • Cleanup: Cancellation token prevents stale updates

2. usePdsEndpoint#

Purpose: Discovers the PDS endpoint for a DID

Key Features:#

  • Depends on DID resolution (implicit dependency)
  • Extracts from DID document if already cached
  • Lazy fetching - only when endpoint not in cache

State Flow:#

Input: DID
  
Check didCache.getByDid(did).pdsEndpoint
  
If missing: ensurePdsEndpoint(resolver, did)
  ├─ Tries to get from existing DID doc
  └─ Falls back to resolver.pdsEndpointForDid()
  
Return: { endpoint, loading, error }

Service Discovery:#

Looks for AtprotoPersonalDataServer service in DID document:

{
  "service": [{
    "type": "AtprotoPersonalDataServer",
    "serviceEndpoint": "https://pds.example.com"
  }]
}

3. useAtProtoRecord#

Purpose: Fetches a single AT Protocol record with smart routing

Key Features:#

  • Collection-aware routing: Bluesky vs other protocols
  • RecordCache deduplication: Multiple components = one fetch
  • Cleanup with reference counting

State Flow:#

Input: { did, collection, rkey }
  
If collection.startsWith("app.bsky."):
  └─ useBlueskyAppview()  Three-tier fallback
Else:
  ├─ useDidResolution(did)
  ├─ usePdsEndpoint(resolved.did)
  └─ recordCache.ensure()  Fetch from PDS
  
Return: { record, loading, error }

RecordCache Deduplication:#

// First component calling this
const { promise, release } = recordCache.ensure(did, collection, rkey, loader)
// refCount = 1

// Second component calling same record
const { promise, release } = recordCache.ensure(...) // Same promise!
// refCount = 2

// On cleanup, both call release()
// Only aborts when refCount reaches 0

4. useBlueskyAppview#

Purpose: Fetches Bluesky records with appview optimization

Key Features:#

  • Collection-aware endpoints:
    • app.bsky.actor.profileapp.bsky.actor.getProfile
    • app.bsky.feed.postapp.bsky.feed.getPostThread
  • CDN URL extraction: Parses CDN URLs to extract CIDs
  • Atomic state updates: Uses reducer for complex state

Three-Tier Fallback with Source Tracking:#

async function fetchWithFallback() {
  // Tier 1: Appview (if endpoint mapped)
  try {
    const result = await fetchFromAppview(did, collection, rkey);
    return { record: result, source: "appview" };
  } catch {}

  // Tier 2: Slingshot
  try {
    const result = await fetchFromSlingshot(did, collection, rkey);
    return { record: result, source: "slingshot" };
  } catch {}

  // Tier 3: PDS
  try {
    const result = await fetchFromPds(did, collection, rkey);
    return { record: result, source: "pds" };
  } catch {}

  // All tiers failed - provide helpful error for banned Bluesky accounts
  if (pdsEndpoint.includes('.bsky.network')) {
    throw new Error('Record unavailable. The Bluesky PDS may be unreachable or the account may be banned.');
  }

  throw new Error('Failed to fetch record from all sources');
}

The source field in the result accurately indicates which tier successfully fetched the data, enabling debugging and analytics.

CDN URL Handling:#

Appview returns CDN URLs like:

https://cdn.bsky.app/img/avatar/plain/did:plc:xxx/bafkreixxx@jpeg

Hook extracts CID (bafkreixxx) and creates standard Blob object:

{
  $type: "blob",
  ref: { $link: "bafkreixxx" },
  mimeType: "image/jpeg",
  size: 0,
  cdnUrl: "https://cdn.bsky.app/..." // Preserved for fast rendering
}

Reducer Pattern:#

type Action =
  | { type: "SET_LOADING"; loading: boolean }
  | { type: "SET_SUCCESS"; record: T; source: "appview" | "slingshot" | "pds" }
  | { type: "SET_ERROR"; error: Error }
  | { type: "RESET" };

// Atomic state updates, no race conditions
dispatch({ type: "SET_SUCCESS", record, source });

5. useLatestRecord#

Purpose: Fetches the most recent record from a collection

Key Features:#

  • Timestamp validation: Skips records before 2023 (pre-ATProto)
  • PDS-only: Slingshot doesn't support listRecords
  • Smart fetching: Gets 3 records to handle invalid timestamps

State Flow:#

Input: { did, collection }
  
useDidResolution(did)
usePdsEndpoint(did)
  
callListRecords(endpoint, did, collection, limit: 3)
  
Filter: isValidTimestamp(record)  year >= 2023
  
Return first valid record: { record, rkey, loading, error, empty }

Timestamp Validation:#

function isValidTimestamp(record: unknown): boolean {
  const timestamp = record.createdAt || record.indexedAt;
  if (!timestamp) return true; // No timestamp, assume valid
  
  const date = new Date(timestamp);
  return date.getFullYear() >= 2023; // ATProto created in 2023
}

6. usePaginatedRecords#

Purpose: Cursor-based pagination with prefetching

Key Features:#

  • Dual fetching modes:
    • Author feed (appview) - for Bluesky posts with filters
    • Direct PDS - for all other collections
  • Smart prefetching: Loads next page in background
  • Invalid timestamp filtering: Same as useLatestRecord
  • Request sequencing: Prevents race conditions with requestSeq

State Management:#

// Pages stored as array
pages: [
  { records: [...], cursor: "abc" },  // page 0
  { records: [...], cursor: "def" },  // page 1
  { records: [...], cursor: undefined } // page 2 (last)
]
pageIndex: 1 // Currently viewing page 1

Prefetch Logic:#

useEffect(() => {
  const cursor = pages[pageIndex]?.cursor;
  if (!cursor || pages[pageIndex + 1]) return; // No cursor or already loaded
  
  // Prefetch next page in background
  fetchPage(identity, cursor, pageIndex + 1, "prefetch");
}, [pageIndex, pages]);

Author Feed vs PDS:#

if (preferAuthorFeed && collection === "app.bsky.feed.post") {
  // Use app.bsky.feed.getAuthorFeed
  const res = await callAppviewRpc("app.bsky.feed.getAuthorFeed", {
    actor: handle || did,
    filter: "posts_with_media", // Optional filter
    includePins: true
  });
} else {
  // Use com.atproto.repo.listRecords
  const res = await callListRecords(pdsEndpoint, did, collection, limit);
}

Race Condition Prevention:#

const requestSeq = useRef(0);

// On identity change
resetState();
requestSeq.current += 1; // Invalidate in-flight requests

// In fetch callback
const token = requestSeq.current;
// ... do async work ...
if (token !== requestSeq.current) return; // Stale request, abort

7. useBlob#

Purpose: Fetches and caches media blobs with object URL management

Key Features:#

  • Automatic cleanup: Revokes object URLs on unmount
  • BlobCache deduplication: Same blob = one fetch
  • Reference counting: Safe concurrent access

State Flow:#

Input: { did, cid }
  
useDidResolution(did)
usePdsEndpoint(did)
  
Check blobCache.get(did, cid)
  
If missing: blobCache.ensure()  Fetch from PDS
  ├─ GET /xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}
  └─ Store in cache
  
Create object URL: URL.createObjectURL(blob)
  
Return: { url, loading, error }
  
Cleanup: URL.revokeObjectURL(url)

Object URL Management:#

const objectUrlRef = useRef<string>();

// On successful fetch
const nextUrl = URL.createObjectURL(blob);
const prevUrl = objectUrlRef.current;
objectUrlRef.current = nextUrl;
if (prevUrl) URL.revokeObjectURL(prevUrl); // Clean up old URL

// On unmount
useEffect(() => () => {
  if (objectUrlRef.current) {
    URL.revokeObjectURL(objectUrlRef.current);
  }
}, []);

8. useBlueskyProfile#

Purpose: Wrapper around useBlueskyAppview for profile records

Key Features:#

  • Simplified interface: Just pass DID
  • Type conversion: Converts ProfileRecord to BlueskyProfileData
  • CID extraction: Extracts avatar/banner CIDs from blobs

Implementation:#

export function useBlueskyProfile(did: string | undefined) {
  const { record, loading, error } = useBlueskyAppview<ProfileRecord>({
    did,
    collection: "app.bsky.actor.profile",
    rkey: "self",
  });

  const data = record ? {
    did: did || "",
    handle: "", // Populated by caller
    displayName: record.displayName,
    description: record.description,
    avatar: extractCidFromBlob(record.avatar),
    banner: extractCidFromBlob(record.banner),
    createdAt: record.createdAt,
  } : undefined;

  return { data, loading, error };
}

Purpose: Fetches backlinks from Microcosm Constellation API

Key Features:#

  • Specialized use case: Tangled stars, etc.
  • Abort controller: Cancels in-flight requests
  • Refetch support: Manual refresh capability

State Flow:#

Input: { subject: "at://did:plc:xxx/sh.tangled.repo/yyy", source: "sh.tangled.feed.star:subject" }
  
GET https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinks
  ?subject={subject}&source={source}&limit={limit}
  
Return: { backlinks: [...], total, loading, error, refetch }

10. useRepoLanguages#

Purpose: Fetches language statistics from Tangled knot server

Key Features:#

  • Branch fallback: Tries "main", then "master"
  • Knot server query: For repository analysis

State Flow:#

Input: { knot: "knot.gaze.systems", did, repoName, branch }
  
GET https://{knot}/xrpc/sh.tangled.repo.languages
  ?repo={did}/{repoName}&ref={branch}
  
If 404: Try fallback branch
  
Return: { data: { languages: {...} }, loading, error }

Cache Implementation Deep Dive#

DidCache#

Purpose: Cache DID documents, handle mappings, PDS endpoints

class DidCache {
  private byHandle = new Map<string, DidCacheEntry>();
  private byDid = new Map<string, DidCacheEntry>();
  private handlePromises = new Map<string, Promise<...>>();
  private docPromises = new Map<string, Promise<...>>();
  private pdsPromises = new Map<string, Promise<...>>();
  
  // Memoized snapshots prevent re-renders
  private toSnapshot(entry): DidCacheSnapshot {
    if (entry.snapshot) return entry.snapshot; // Reuse
    entry.snapshot = { did, handle, doc, pdsEndpoint };
    return entry.snapshot;
  }
}

Key methods:

  • getByHandle(handle) - Instant cache lookup
  • getByDid(did) - Instant cache lookup
  • ensureHandle(resolver, handle) - Deduplicated resolution
  • ensureDidDoc(resolver, did) - Deduplicated doc fetch
  • ensurePdsEndpoint(resolver, did) - Deduplicated PDS discovery

Snapshot stability:

memoize(entry) {
  const existing = this.byDid.get(did);
  
  // Data unchanged? Reuse snapshot (same reference)
  if (existing && existing.did === did && 
      existing.handle === handle && ...) {
    return toSnapshot(existing); // Prevents re-render!
  }
  
  // Data changed, create new entry
  const merged = { did, handle, doc, pdsEndpoint, snapshot: undefined };
  this.byDid.set(did, merged);
  return toSnapshot(merged);
}

BlobCache#

Purpose: Cache media blobs with reference counting

class BlobCache {
  private store = new Map<string, BlobCacheEntry>();
  private inFlight = new Map<string, InFlightBlobEntry>();
  
  ensure(did, cid, loader) {
    // Already cached?
    const cached = this.get(did, cid);
    if (cached) return { promise: Promise.resolve(cached), release: noop };
    
    // In-flight request?
    const existing = this.inFlight.get(key);
    if (existing) {
      existing.refCount++; // Multiple consumers
      return { promise: existing.promise, release: () => this.release(key) };
    }
    
    // New request
    const { promise, abort } = loader();
    this.inFlight.set(key, { promise, abort, refCount: 1 });
    return { promise, release: () => this.release(key) };
  }
  
  private release(key) {
    const entry = this.inFlight.get(key);
    entry.refCount--;
    if (entry.refCount <= 0) {
      this.inFlight.delete(key);
      entry.abort(); // Cancel fetch
    }
  }
}

RecordCache#

Purpose: Cache AT Protocol records with deduplication

Identical structure to BlobCache but for record data.


Common Patterns#

1. Cancellation Pattern#

useEffect(() => {
  let cancelled = false;
  
  const assignState = (next) => {
    if (cancelled) return; // Don't update unmounted component
    setState(prev => ({ ...prev, ...next }));
  };
  
  // ... async work ...
  
  return () => {
    cancelled = true; // Mark as cancelled
    release?.(); // Decrement refCount
  };
}, [deps]);

2. Error Stabilization Pattern#

setError(prevError => 
  prevError?.message === newError.message 
    ? prevError  // Reuse same reference
    : newError   // New error
);

3. Identity Tracking Pattern#

const identityRef = useRef<string>();
const identity = did && endpoint ? `${did}::${endpoint}` : undefined;

useEffect(() => {
  if (identityRef.current !== identity) {
    identityRef.current = identity;
    resetState(); // Clear stale data
  }
  // ...
}, [identity]);

4. Dual-Mode Resolution#

const isDid = input.startsWith("did:");
const normalizedHandle = !isDid ? input.toLowerCase() : undefined;

// Different code paths
if (isDid) {
  snapshot = await didCache.ensureDidDoc(resolver, input);
} else {
  snapshot = await didCache.ensureHandle(resolver, normalizedHandle);
}

Performance Optimizations#

1. Memoized Snapshots#

Caches return stable references when data unchanged → prevents re-renders

2. Reference Counting#

Multiple components requesting same data share one fetch

3. Prefetching#

usePaginatedRecords loads next page in background

4. CDN URLs#

Bluesky appview returns CDN URLs → skip blob fetching for images

5. Smart Routing#

Bluesky collections use fast appview → non-Bluesky goes direct to PDS

6. Request Deduplication#

In-flight request maps prevent duplicate fetches

7. Timestamp Validation#

Skip invalid records early (before 2023) → fewer wasted cycles


Error Handling Strategy#

1. Fallback Chains#

Never fail on first attempt → try multiple sources

2. Graceful Degradation#

// Slingshot failed? Try appview
try {
  return await fetchFromSlingshot();
} catch (slingshotError) {
  try {
    return await fetchFromAppview();
  } catch (appviewError) {
    // Combine errors for better debugging
    throw new Error(`${appviewError.message}; Slingshot: ${slingshotError.message}`);
  }
}

3. Component Isolation#

Errors in one component don't crash others (via error boundaries recommended)

4. Abort Handling#

try {
  await fetch(url, { signal });
} catch (err) {
  if (err.name === "AbortError") return; // Expected, ignore
  throw err;
}

5. Banned Bluesky Account Detection#

When all three tiers fail and the PDS is a .bsky.network endpoint, provide a helpful error:

// All tiers failed - check if it's a banned Bluesky account
if (pdsEndpoint.includes('.bsky.network')) {
  throw new Error(
    'Record unavailable. The Bluesky PDS may be unreachable or the account may be banned.'
  );
}

This helps users understand why data is unavailable instead of showing generic fetch errors. Applies to both useBlueskyAppview and useAtProtoRecord hooks.


Testing Considerations#

Key scenarios to test:#

  1. Concurrent requests: Multiple components requesting same data
  2. Race conditions: Component unmounting mid-fetch
  3. Cache invalidation: Identity changes during fetch
  4. Error fallbacks: Slingshot down → appview works
  5. Timestamp filtering: Records before 2023 skipped
  6. Reference counting: Proper cleanup on unmount
  7. Prefetching: Background loads don't interfere with active loads

Common Gotchas#

1. React Rules of Hooks#

All hooks called unconditionally, even if results not used:

// Always call, conditionally use results
const blueskyResult = useBlueskyAppview({
  did: isBlueskyCollection ? handleOrDid : undefined, // Pass undefined to skip
  collection: isBlueskyCollection ? collection : undefined,
  rkey: isBlueskyCollection ? rkey : undefined,
});

2. Cleanup Order Matters#

return () => {
  cancelled = true;      // 1. Prevent state updates
  release?.();           // 2. Decrement refCount
  revokeObjectURL(...);  // 3. Free resources
};

3. Snapshot Reuse#

Don't modify cached snapshots! They're shared across components.

4. CDN URL Extraction#

Bluesky CDN URLs must be parsed carefully:

https://cdn.bsky.app/img/avatar/plain/did:plc:xxx/bafkreixxx@jpeg
                                        ^^^^^^^^^^^^       ^^^^^^
                                           DID              CID