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}