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>(handleOrDid: string | undefined, collection: string): LatestRecordState<T> { 30 const { did, error: didError, loading: resolvingDid } = useDidResolution(handleOrDid); 31 const { endpoint, error: endpointError, loading: resolvingEndpoint } = usePdsEndpoint(did); 32 const [state, setState] = useState<LatestRecordState<T>>({ loading: !!handleOrDid, empty: false }); 33 34 useEffect(() => { 35 let cancelled = false; 36 37 const assign = (next: Partial<LatestRecordState<T>>) => { 38 if (cancelled) return; 39 setState(prev => ({ ...prev, ...next })); 40 }; 41 42 if (!handleOrDid) { 43 assign({ loading: false, record: undefined, rkey: undefined, error: undefined, empty: false }); 44 return () => { cancelled = true; }; 45 } 46 47 if (didError) { 48 assign({ loading: false, error: didError, empty: false }); 49 return () => { cancelled = true; }; 50 } 51 52 if (endpointError) { 53 assign({ loading: false, error: endpointError, empty: false }); 54 return () => { cancelled = true; }; 55 } 56 57 if (resolvingDid || resolvingEndpoint || !did || !endpoint) { 58 assign({ loading: true, error: undefined }); 59 return () => { cancelled = true; }; 60 } 61 62 assign({ loading: true, error: undefined, empty: false }); 63 64 (async () => { 65 try { 66 const { rpc } = await createAtprotoClient({ service: endpoint }); 67 const res = await (rpc as unknown as { 68 get: ( 69 nsid: string, 70 opts: { params: Record<string, string | number | boolean> } 71 ) => Promise<{ ok: boolean; data: { records: Array<{ uri: string; rkey?: string; value: T }> } }>; 72 }).get('com.atproto.repo.listRecords', { 73 params: { repo: did, collection, limit: 1, reverse: false } 74 }); 75 if (!res.ok) throw new Error('Failed to list records'); 76 const list = res.data.records; 77 if (list.length === 0) { 78 assign({ loading: false, empty: true, record: undefined, rkey: undefined }); 79 return; 80 } 81 const first = list[0]; 82 const derivedRkey = first.rkey ?? extractRkey(first.uri); 83 assign({ record: first.value, rkey: derivedRkey, loading: false, empty: false }); 84 } catch (e) { 85 assign({ error: e as Error, loading: false, empty: false }); 86 } 87 })(); 88 89 return () => { 90 cancelled = true; 91 }; 92 }, [handleOrDid, did, endpoint, collection, resolvingDid, resolvingEndpoint, didError, endpointError]); 93 94 return state; 95} 96 97function extractRkey(uri: string): string | undefined { 98 if (!uri) return undefined; 99 const parts = uri.split('/'); 100 return parts[parts.length - 1]; 101}