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>({ 39 did: handleOrDid, 40 collection, 41 rkey, 42}: AtProtoRecordKey): AtProtoRecordState<T> { 43 const { 44 did, 45 error: didError, 46 loading: resolvingDid, 47 } = useDidResolution(handleOrDid); 48 const { 49 endpoint, 50 error: endpointError, 51 loading: resolvingEndpoint, 52 } = usePdsEndpoint(did); 53 const [state, setState] = useState<AtProtoRecordState<T>>({ 54 loading: !!(handleOrDid && collection && rkey), 55 }); 56 57 useEffect(() => { 58 let cancelled = false; 59 60 const assignState = (next: Partial<AtProtoRecordState<T>>) => { 61 if (cancelled) return; 62 setState((prev) => ({ ...prev, ...next })); 63 }; 64 65 if (!handleOrDid || !collection || !rkey) { 66 assignState({ 67 loading: false, 68 record: undefined, 69 error: undefined, 70 }); 71 return () => { 72 cancelled = true; 73 }; 74 } 75 76 if (didError) { 77 assignState({ loading: false, error: didError }); 78 return () => { 79 cancelled = true; 80 }; 81 } 82 83 if (endpointError) { 84 assignState({ loading: false, error: endpointError }); 85 return () => { 86 cancelled = true; 87 }; 88 } 89 90 if (resolvingDid || resolvingEndpoint || !did || !endpoint) { 91 assignState({ loading: true, error: undefined }); 92 return () => { 93 cancelled = true; 94 }; 95 } 96 97 assignState({ loading: true, error: undefined, record: undefined }); 98 99 (async () => { 100 try { 101 const { rpc } = await createAtprotoClient({ 102 service: endpoint, 103 }); 104 const res = await ( 105 rpc as unknown as { 106 get: ( 107 nsid: string, 108 opts: { 109 params: { 110 repo: string; 111 collection: string; 112 rkey: string; 113 }; 114 }, 115 ) => Promise<{ ok: boolean; data: { value: T } }>; 116 } 117 ).get("com.atproto.repo.getRecord", { 118 params: { repo: did, collection, rkey }, 119 }); 120 if (!res.ok) throw new Error("Failed to load record"); 121 const record = (res.data as { value: T }).value; 122 assignState({ record, loading: false }); 123 } catch (e) { 124 const err = e instanceof Error ? e : new Error(String(e)); 125 assignState({ error: err, loading: false }); 126 } 127 })(); 128 129 return () => { 130 cancelled = true; 131 }; 132 }, [ 133 handleOrDid, 134 did, 135 endpoint, 136 collection, 137 rkey, 138 resolvingDid, 139 resolvingEndpoint, 140 didError, 141 endpointError, 142 ]); 143 144 return state; 145}