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}