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 * Shape of the state returned by {@link useLatestRecord}. 8 */ 9export interface LatestRecordState<T = unknown> { 10 /** Latest record value if one exists. */ 11 record?: T; 12 /** Record key for the fetched record, when derivable. */ 13 rkey?: string; 14 /** Error encountered while fetching. */ 15 error?: Error; 16 /** Indicates whether a fetch is in progress. */ 17 loading: boolean; 18 /** `true` when the collection has zero records. */ 19 empty: boolean; 20} 21 22/** 23 * Fetches the most recent record from a collection using `listRecords(limit=1)`. 24 * 25 * @param handleOrDid - Handle or DID that owns the collection. 26 * @param collection - NSID of the collection to query. 27 * @returns {LatestRecordState<T>} Object reporting the latest record value, derived rkey, loading status, emptiness, and any error. 28 */ 29export function useLatestRecord<T = unknown>( 30 handleOrDid: string | undefined, 31 collection: string, 32): LatestRecordState<T> { 33 const { 34 did, 35 error: didError, 36 loading: resolvingDid, 37 } = useDidResolution(handleOrDid); 38 const { 39 endpoint, 40 error: endpointError, 41 loading: resolvingEndpoint, 42 } = usePdsEndpoint(did); 43 const [state, setState] = useState<LatestRecordState<T>>({ 44 loading: !!handleOrDid, 45 empty: false, 46 }); 47 48 useEffect(() => { 49 let cancelled = false; 50 51 const assign = (next: Partial<LatestRecordState<T>>) => { 52 if (cancelled) return; 53 setState((prev) => ({ ...prev, ...next })); 54 }; 55 56 if (!handleOrDid) { 57 assign({ 58 loading: false, 59 record: undefined, 60 rkey: undefined, 61 error: undefined, 62 empty: false, 63 }); 64 return () => { 65 cancelled = true; 66 }; 67 } 68 69 if (didError) { 70 assign({ loading: false, error: didError, empty: false }); 71 return () => { 72 cancelled = true; 73 }; 74 } 75 76 if (endpointError) { 77 assign({ loading: false, error: endpointError, empty: false }); 78 return () => { 79 cancelled = true; 80 }; 81 } 82 83 if (resolvingDid || resolvingEndpoint || !did || !endpoint) { 84 assign({ loading: true, error: undefined }); 85 return () => { 86 cancelled = true; 87 }; 88 } 89 90 assign({ loading: true, error: undefined, empty: false }); 91 92 (async () => { 93 try { 94 const { rpc } = await createAtprotoClient({ 95 service: endpoint, 96 }); 97 const res = await ( 98 rpc as unknown as { 99 get: ( 100 nsid: string, 101 opts: { 102 params: Record< 103 string, 104 string | number | boolean 105 >; 106 }, 107 ) => Promise<{ 108 ok: boolean; 109 data: { 110 records: Array<{ 111 uri: string; 112 rkey?: string; 113 value: T; 114 }>; 115 }; 116 }>; 117 } 118 ).get("com.atproto.repo.listRecords", { 119 params: { repo: did, collection, limit: 1, reverse: false }, 120 }); 121 if (!res.ok) throw new Error("Failed to list records"); 122 const list = res.data.records; 123 if (list.length === 0) { 124 assign({ 125 loading: false, 126 empty: true, 127 record: undefined, 128 rkey: undefined, 129 }); 130 return; 131 } 132 const first = list[0]; 133 const derivedRkey = first.rkey ?? extractRkey(first.uri); 134 assign({ 135 record: first.value, 136 rkey: derivedRkey, 137 loading: false, 138 empty: false, 139 }); 140 } catch (e) { 141 assign({ error: e as Error, loading: false, empty: false }); 142 } 143 })(); 144 145 return () => { 146 cancelled = true; 147 }; 148 }, [ 149 handleOrDid, 150 did, 151 endpoint, 152 collection, 153 resolvingDid, 154 resolvingEndpoint, 155 didError, 156 endpointError, 157 ]); 158 159 return state; 160} 161 162function extractRkey(uri: string): string | undefined { 163 if (!uri) return undefined; 164 const parts = uri.split("/"); 165 return parts[parts.length - 1]; 166}