A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.

easier prefilling by allowing record as a prop in components

+55 -16
README.md
··· 10 10 ## Features 11 11 12 12 - Drop-in components for common record types (`BlueskyPost`, `BlueskyProfile`, `TangledString`, etc.). 13 + - Pass prefetched data directly to components to skip API calls—perfect for server-side rendering, caching, or when you already have the data. 13 14 - Hooks and helpers for composing your own renderers for your own applications, (PRs welcome!) 14 15 - Built on the lightweight [`@atcute/*`](https://github.com/atcute) clients. 15 16 ··· 39 40 } 40 41 ``` 41 42 43 + ## Passing prefetched data to skip API calls 44 + 45 + All components accept a `record` prop. When provided, the component uses your data immediately without making network requests for that record. This is perfect for SSR, caching strategies, or when you've already fetched data through other means. 46 + 47 + ```tsx 48 + import { BlueskyPost, useLatestRecord } from "atproto-ui"; 49 + import type { FeedPostRecord } from "atproto-ui"; 50 + 51 + const MyComponent: React.FC<{ did: string }> = ({ did }) => { 52 + // Fetch the latest post using the hook 53 + const { record, rkey, loading } = useLatestRecord<FeedPostRecord>( 54 + did, 55 + "app.bsky.feed.post" 56 + ); 57 + 58 + if (loading) return <p>Loading…</p>; 59 + if (!record || !rkey) return <p>No posts found.</p>; 60 + 61 + // Pass the fetched record directly—BlueskyPost won't re-fetch it 62 + return <BlueskyPost did={did} rkey={rkey} record={record} />; 63 + }; 64 + ``` 65 + 66 + The same pattern works for all components: 67 + 68 + ```tsx 69 + // BlueskyProfile with prefetched data 70 + <BlueskyProfile did={did} record={profileRecord} /> 71 + 72 + // TangledString with prefetched data 73 + <TangledString did={did} rkey={rkey} record={stringRecord} /> 74 + 75 + // LeafletDocument with prefetched data 76 + <LeafletDocument did={did} rkey={rkey} record={documentRecord} /> 77 + ``` 78 + 42 79 ### Available building blocks 43 80 44 81 | Component / Hook | What it does | 45 82 | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | 46 - | `AtProtoProvider` | Configures PLC directory (defaults to `https://plc.directory`) and shares protocol clients via React context. | 47 - | `BlueskyProfile` | Renders a profile card for a DID/handle. Accepts `fallback`, `loadingIndicator`, `renderer`, and `colorScheme`. | 48 - | `BlueskyPost` / `BlueskyQuotePost` | Shows a single Bluesky post, with quotation support, custom renderer overrides, and the same loading/fallback knobs. | 83 + | `AtProtoProvider` | Configures PLC directory (defaults to `https://plc.directory`) and shares protocol clients via React context. | 84 + | `AtProtoRecord` | Core component that fetches and renders any AT Protocol record. **Accepts a `record` prop to use prefetched data and skip API calls.** | 85 + | `BlueskyProfile` | Renders a profile card for a DID/handle. **Accepts a `record` prop to skip fetching.** Also supports `fallback`, `loadingIndicator`, `renderer`, and `colorScheme`. | 86 + | `BlueskyPost` / `BlueskyQuotePost` | Shows a single Bluesky post with quotation support. **Accepts a `record` prop to skip fetching.** Custom renderer overrides and loading/fallback knobs available. | 49 87 | `BlueskyPostList` | Lists the latest posts with built-in pagination (defaults: 5 per page, pagination controls on). | 50 - | `TangledString` | Renders a Tangled string (gist-like record) with optional renderer overrides. | 51 - | `LeafletDocument` | Displays long-form Leaflet documents with blocks, theme support, and renderer overrides. | 52 - | `useDidResolution`, `useLatestRecord`, `usePaginatedRecords`, … | Hook-level access to records if you want to own the markup or prefill components. | 88 + | `TangledString` | Renders a Tangled string (gist-like record). **Accepts a `record` prop to skip fetching.** Optional renderer overrides available. | 89 + | `LeafletDocument` | Displays long-form Leaflet documents with blocks and theme support. **Accepts a `record` prop to skip fetching.** Renderer overrides available. | 90 + | `useDidResolution`, `useLatestRecord`, `usePaginatedRecords`, … | Hook-level access to records. `useLatestRecord` returns both the `record` and `rkey` so you can pass them directly to components. | 53 91 54 92 All components accept a `colorScheme` of `'light' | 'dark' | 'system'` so they can blend into your design. They also accept `fallback` and `loadingIndicator` props to control what renders before or during network work, and most expose a `renderer` override when you need total control of the final markup. 55 93 56 - ### Prefill components with the latest record 94 + ### Using hooks to fetch data once 57 95 58 - `useLatestRecord` gives you the most recent record for any collection along with its `rkey`. You can use that key to pre-populate components like `BlueskyPost`, `LeafletDocument`, or `TangledString`. 96 + `useLatestRecord` gives you the most recent record for any collection along with its `rkey`. You can pass both to components to skip the fetch: 59 97 60 98 ```tsx 61 99 import { useLatestRecord, BlueskyPost } from "atproto-ui"; 62 100 import type { FeedPostRecord } from "atproto-ui"; 63 101 64 102 const LatestBlueskyPost: React.FC<{ did: string }> = ({ did }) => { 65 - const { rkey, loading, error, empty } = useLatestRecord<FeedPostRecord>( 103 + const { record, rkey, loading, error, empty } = useLatestRecord<FeedPostRecord>( 66 104 did, 67 105 "app.bsky.feed.post", 68 106 ); 69 107 70 108 if (loading) return <p>Fetching latest post…</p>; 71 109 if (error) return <p>Could not load: {error.message}</p>; 72 - if (empty || !rkey) return <p>No posts yet.</p>; 110 + if (empty || !record || !rkey) return <p>No posts yet.</p>; 73 111 74 - return <BlueskyPost did={did} rkey={rkey} colorScheme="system" />; 112 + // Pass both record and rkey—no additional API call needed 113 + return <BlueskyPost did={did} rkey={rkey} record={record} colorScheme="system" />; 75 114 }; 76 115 ``` 77 116 78 - The same pattern works for other components: swap the collection NSID and the component you render once you have an `rkey`. 117 + The same pattern works for other components. Just swap the collection NSID and component: 79 118 80 119 ```tsx 81 120 const LatestLeafletDocument: React.FC<{ did: string }> = ({ did }) => { 82 - const { rkey } = useLatestRecord(did, "pub.leaflet.document"); 83 - return rkey ? ( 84 - <LeafletDocument did={did} rkey={rkey} colorScheme="light" /> 121 + const { record, rkey } = useLatestRecord(did, "pub.leaflet.document"); 122 + return record && rkey ? ( 123 + <LeafletDocument did={did} rkey={rkey} record={record} colorScheme="light" /> 85 124 ) : null; 86 125 }; 87 126 ``` 88 127 89 128 ## Compose your own component 90 129 91 - The helpers let you stitch together custom experiences without reimplementing protocol plumbing. The example below pulls a creator’s latest post and renders a minimal summary: 130 + The helpers let you stitch together custom experiences without reimplementing protocol plumbing. The example below pulls a creator's latest post and renders a minimal summary: 92 131 93 132 ```tsx 94 133 import { useLatestRecord, useColorScheme, AtProtoRecord } from "atproto-ui";
+19
lib/components/BlueskyPost.tsx
··· 22 22 */ 23 23 rkey: string; 24 24 /** 25 + * Prefetched post record. When provided, skips fetching the post from the network. 26 + * Note: Profile and avatar data will still be fetched unless a custom renderer is used. 27 + */ 28 + record?: FeedPostRecord; 29 + /** 25 30 * Custom renderer component that receives resolved post data and status flags. 26 31 */ 27 32 renderer?: React.ComponentType<BlueskyPostRendererInjectedProps>; ··· 119 124 export const BlueskyPost: React.FC<BlueskyPostProps> = ({ 120 125 did: handleOrDid, 121 126 rkey, 127 + record, 122 128 renderer, 123 129 fallback, 124 130 loadingIndicator, ··· 197 203 ); 198 204 } 199 205 206 + // When record is provided, pass it directly to skip fetching 207 + if (record) { 208 + return ( 209 + <AtProtoRecord<FeedPostRecord> 210 + record={record} 211 + renderer={Wrapped} 212 + fallback={fallback} 213 + loadingIndicator={loadingIndicator} 214 + /> 215 + ); 216 + } 217 + 218 + // Otherwise fetch the record using did, collection, and rkey 200 219 return ( 201 220 <AtProtoRecord<FeedPostRecord> 202 221 did={repoIdentifier}
+20
lib/components/BlueskyProfile.tsx
··· 17 17 did: string; 18 18 /** 19 19 * Record key within the profile collection. Typically `'self'`. 20 + * Optional when `record` is provided. 20 21 */ 21 22 rkey?: string; 23 + /** 24 + * Prefetched profile record. When provided, skips fetching the profile from the network. 25 + */ 26 + record?: ProfileRecord; 22 27 /** 23 28 * Optional renderer override for custom presentation. 24 29 */ ··· 94 99 export const BlueskyProfile: React.FC<BlueskyProfileProps> = ({ 95 100 did: handleOrDid, 96 101 rkey = "self", 102 + record, 97 103 renderer, 98 104 fallback, 99 105 loadingIndicator, ··· 128 134 /> 129 135 ); 130 136 }; 137 + 138 + // When record is provided, pass it directly to skip fetching 139 + if (record) { 140 + return ( 141 + <AtProtoRecord<ProfileRecord> 142 + record={record} 143 + renderer={Wrapped} 144 + fallback={fallback} 145 + loadingIndicator={loadingIndicator} 146 + /> 147 + ); 148 + } 149 + 150 + // Otherwise fetch the record using did, collection, and rkey 131 151 return ( 132 152 <AtProtoRecord<ProfileRecord> 133 153 did={repoIdentifier}
+18
lib/components/LeafletDocument.tsx
··· 30 30 */ 31 31 rkey: string; 32 32 /** 33 + * Prefetched Leaflet document record. When provided, skips fetching from the network. 34 + */ 35 + record?: LeafletDocumentRecord; 36 + /** 33 37 * Optional custom renderer for advanced layouts. 34 38 */ 35 39 renderer?: React.ComponentType<LeafletDocumentRendererInjectedProps>; ··· 70 74 export const LeafletDocument: React.FC<LeafletDocumentProps> = ({ 71 75 did, 72 76 rkey, 77 + record, 73 78 renderer, 74 79 fallback, 75 80 loadingIndicator, ··· 116 121 ); 117 122 }; 118 123 124 + // When record is provided, pass it directly to skip fetching 125 + if (record) { 126 + return ( 127 + <AtProtoRecord<LeafletDocumentRecord> 128 + record={record} 129 + renderer={Wrapped} 130 + fallback={fallback} 131 + loadingIndicator={loadingIndicator} 132 + /> 133 + ); 134 + } 135 + 136 + // Otherwise fetch the record using did, collection, and rkey 119 137 return ( 120 138 <AtProtoRecord<LeafletDocumentRecord> 121 139 did={did}
+17
lib/components/TangledString.tsx
··· 11 11 did: string; 12 12 /** Record key within the `sh.tangled.string` collection. */ 13 13 rkey: string; 14 + /** Prefetched Tangled String record. When provided, skips fetching from the network. */ 15 + record?: TangledStringRecord; 14 16 /** Optional renderer override for custom presentation. */ 15 17 renderer?: React.ComponentType<TangledStringRendererInjectedProps>; 16 18 /** Fallback node displayed before loading begins. */ ··· 58 60 export const TangledString: React.FC<TangledStringProps> = ({ 59 61 did, 60 62 rkey, 63 + record, 61 64 renderer, 62 65 fallback, 63 66 loadingIndicator, ··· 78 81 canonicalUrl={`https://tangled.org/strings/${did}/${encodeURIComponent(rkey)}`} 79 82 /> 80 83 ); 84 + 85 + // When record is provided, pass it directly to skip fetching 86 + if (record) { 87 + return ( 88 + <AtProtoRecord<TangledStringRecord> 89 + record={record} 90 + renderer={Wrapped} 91 + fallback={fallback} 92 + loadingIndicator={loadingIndicator} 93 + /> 94 + ); 95 + } 96 + 97 + // Otherwise fetch the record using did, collection, and rkey 81 98 return ( 82 99 <AtProtoRecord<TangledStringRecord> 83 100 did={did}