A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1import React from "react";
2import { useAtProtoRecord } from "../hooks/useAtProtoRecord";
3
4/**
5 * Common rendering customization props for AT Protocol records.
6 */
7interface AtProtoRecordRenderProps<T> {
8 /** Custom renderer component that receives the fetched record and loading state. */
9 renderer?: React.ComponentType<{
10 record: T;
11 loading: boolean;
12 error?: Error;
13 }>;
14 /** React node displayed when no record is available (after error or before load). */
15 fallback?: React.ReactNode;
16 /** React node shown while the record is being fetched. */
17 loadingIndicator?: React.ReactNode;
18}
19
20/**
21 * Props for fetching an AT Protocol record from the network.
22 */
23type AtProtoRecordFetchProps<T> = AtProtoRecordRenderProps<T> & {
24 /** Repository DID that owns the record. */
25 did: string;
26 /** NSID collection containing the record. */
27 collection: string;
28 /** Record key identifying the specific record. */
29 rkey: string;
30 /** Must be undefined when fetching (discriminates the union type). */
31 record?: undefined;
32};
33
34/**
35 * Props for rendering a prefetched AT Protocol record.
36 */
37type AtProtoRecordProvidedRecordProps<T> = AtProtoRecordRenderProps<T> & {
38 /** Prefetched record value to render (skips network fetch). */
39 record: T;
40 /** Optional DID for context (not used for fetching). */
41 did?: string;
42 /** Optional collection for context (not used for fetching). */
43 collection?: string;
44 /** Optional rkey for context (not used for fetching). */
45 rkey?: string;
46};
47
48/**
49 * Union type for AT Protocol record props - supports both fetching and prefetched records.
50 */
51export type AtProtoRecordProps<T = unknown> =
52 | AtProtoRecordFetchProps<T>
53 | AtProtoRecordProvidedRecordProps<T>;
54
55/**
56 * Core component for fetching and rendering AT Protocol records with customizable presentation.
57 *
58 * Supports two modes:
59 * 1. **Fetch mode**: Provide `did`, `collection`, and `rkey` to fetch the record from the network
60 * 2. **Prefetch mode**: Provide a `record` directly to skip fetching (useful for SSR/caching)
61 *
62 * When no custom renderer is provided, displays the record as formatted JSON.
63 *
64 * @example
65 * ```tsx
66 * // Fetch mode - retrieves record from network
67 * <AtProtoRecord
68 * did="did:plc:example"
69 * collection="app.bsky.feed.post"
70 * rkey="3k2aexample"
71 * renderer={MyCustomRenderer}
72 * />
73 * ```
74 *
75 * @example
76 * ```tsx
77 * // Prefetch mode - uses provided record
78 * <AtProtoRecord
79 * record={myPrefetchedRecord}
80 * renderer={MyCustomRenderer}
81 * />
82 * ```
83 *
84 * @param props - Either fetch props (did/collection/rkey) or prefetch props (record).
85 * @returns A rendered AT Protocol record with loading/error states handled.
86 */
87export function AtProtoRecord<T = unknown>(props: AtProtoRecordProps<T>) {
88 const {
89 renderer: Renderer,
90 fallback = null,
91 loadingIndicator = "Loading…",
92 } = props;
93 const hasProvidedRecord = "record" in props;
94 const providedRecord = hasProvidedRecord ? props.record : undefined;
95
96 const {
97 record: fetchedRecord,
98 error,
99 loading,
100 } = useAtProtoRecord<T>({
101 did: hasProvidedRecord ? undefined : props.did,
102 collection: hasProvidedRecord ? undefined : props.collection,
103 rkey: hasProvidedRecord ? undefined : props.rkey,
104 });
105
106 const record = providedRecord ?? fetchedRecord;
107 const isLoading = loading && !providedRecord;
108
109 if (error && !record) return <>{fallback}</>;
110 if (!record) return <>{isLoading ? loadingIndicator : fallback}</>;
111 if (Renderer)
112 return <Renderer record={record} loading={isLoading} error={error} />;
113 return (
114 <pre
115 style={{
116 fontSize: 12,
117 padding: 8,
118 background: "#f5f5f5",
119 overflow: "auto",
120 }}
121 >
122 {JSON.stringify(record, null, 2)}
123 </pre>
124 );
125}