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}