A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1import { useEffect, useState } from 'react'; 2import { useDidResolution } from './useDidResolution'; 3import { usePdsEndpoint } from './usePdsEndpoint'; 4import { createAtprotoClient } from '../utils/atproto-client'; 5 6/** 7 * Identifier trio required to address an AT Protocol record. 8 */ 9export interface AtProtoRecordKey { 10 /** Repository DID (or handle prior to resolution) containing the record. */ 11 did?: string; 12 /** NSID collection in which the record resides. */ 13 collection?: string; 14 /** Record key string uniquely identifying the record within the collection. */ 15 rkey?: string; 16} 17 18/** 19 * Loading state returned by {@link useAtProtoRecord}. 20 */ 21export interface AtProtoRecordState<T = unknown> { 22 /** Resolved record value when fetch succeeds. */ 23 record?: T; 24 /** Error thrown while loading, if any. */ 25 error?: Error; 26 /** Indicates whether the hook is in a loading state. */ 27 loading: boolean; 28} 29 30/** 31 * React hook that fetches a single AT Protocol record and tracks loading/error state. 32 * 33 * @param did - DID (or handle before resolution) that owns the record. 34 * @param collection - NSID collection from which to fetch the record. 35 * @param rkey - Record key identifying the record within the collection. 36 * @returns {AtProtoRecordState<T>} Object containing the resolved record, any error, and a loading flag. 37 */ 38export function useAtProtoRecord<T = unknown>({ did: handleOrDid, collection, rkey }: AtProtoRecordKey): AtProtoRecordState<T> { 39 const { did, error: didError, loading: resolvingDid } = useDidResolution(handleOrDid); 40 const { endpoint, error: endpointError, loading: resolvingEndpoint } = usePdsEndpoint(did); 41 const [state, setState] = useState<AtProtoRecordState<T>>({ loading: !!(handleOrDid && collection && rkey) }); 42 43 useEffect(() => { 44 let cancelled = false; 45 46 const assignState = (next: Partial<AtProtoRecordState<T>>) => { 47 if (cancelled) return; 48 setState(prev => ({ ...prev, ...next })); 49 }; 50 51 if (!handleOrDid || !collection || !rkey) { 52 assignState({ loading: false, record: undefined, error: undefined }); 53 return () => { cancelled = true; }; 54 } 55 56 if (didError) { 57 assignState({ loading: false, error: didError }); 58 return () => { cancelled = true; }; 59 } 60 61 if (endpointError) { 62 assignState({ loading: false, error: endpointError }); 63 return () => { cancelled = true; }; 64 } 65 66 if (resolvingDid || resolvingEndpoint || !did || !endpoint) { 67 assignState({ loading: true, error: undefined }); 68 return () => { cancelled = true; }; 69 } 70 71 assignState({ loading: true, error: undefined, record: undefined }); 72 73 (async () => { 74 try { 75 const { rpc } = await createAtprotoClient({ service: endpoint }); 76 const res = await (rpc as unknown as { 77 get: ( 78 nsid: string, 79 opts: { params: { repo: string; collection: string; rkey: string } } 80 ) => Promise<{ ok: boolean; data: { value: T } }>; 81 }).get('com.atproto.repo.getRecord', { 82 params: { repo: did, collection, rkey } 83 }); 84 if (!res.ok) throw new Error('Failed to load record'); 85 const record = (res.data as { value: T }).value; 86 assignState({ record, loading: false }); 87 } catch (e) { 88 const err = e instanceof Error ? e : new Error(String(e)); 89 assignState({ error: err, loading: false }); 90 } 91 })(); 92 93 return () => { 94 cancelled = true; 95 }; 96 }, [handleOrDid, did, endpoint, collection, rkey, resolvingDid, resolvingEndpoint, didError, endpointError]); 97 98 return state; 99}