A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1# atproto-ui 2 3atproto-ui is a component library and set of hooks for rendering records from the AT Protocol (Bluesky, Leaflet, and friends) in React applications. It handles DID resolution, PDS endpoint discovery, and record fetching so you can focus on UI. [Live demo](https://atproto-ui.wisp.place). 4 5## Screenshots 6 7![Bluesky component](readme_img/bluesky.png) 8![Tangled String component](readme_img/tangled.png) 9 10## Features 11 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. 14- Hooks and helpers for composing your own renderers for your own applications, (PRs welcome!) 15- Built on the lightweight [`@atcute/*`](https://github.com/atcute) clients. 16 17## Installation 18 19```bash 20npm install atproto-ui 21``` 22 23## Quick start 24 251. Wrap your app (once) with the `AtProtoProvider`. 262. Drop any of the ready-made components inside that provider. 273. Use the hooks to prefetch handles, blobs, or latest records when you want to control the render flow yourself. 28 29```tsx 30import { AtProtoProvider, BlueskyPost } from "atproto-ui"; 31 32export function App() { 33 return ( 34 <AtProtoProvider> 35 <BlueskyPost did="did:plc:example" rkey="3k2aexample" /> 36 {/* you can use handles in the components as well. */} 37 <LeafletDocument did="nekomimi.pet" rkey="3m2seagm2222c" /> 38 </AtProtoProvider> 39 ); 40} 41``` 42 43## Passing prefetched data to skip API calls 44 45All 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 48import { BlueskyPost, useLatestRecord } from "atproto-ui"; 49import type { FeedPostRecord } from "atproto-ui"; 50 51const 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 66The 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 79### Available building blocks 80 81| Component / Hook | What it does | 82| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | 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. | 87| `BlueskyPostList` | Lists the latest posts with built-in pagination (defaults: 5 per page, pagination controls on). | 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. | 91 92All 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. 93 94### Using hooks to fetch data once 95 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: 97 98```tsx 99import { useLatestRecord, BlueskyPost } from "atproto-ui"; 100import type { FeedPostRecord } from "atproto-ui"; 101 102const LatestBlueskyPost: React.FC<{ did: string }> = ({ did }) => { 103 const { record, rkey, loading, error, empty } = useLatestRecord<FeedPostRecord>( 104 did, 105 "app.bsky.feed.post", 106 ); 107 108 if (loading) return <p>Fetching latest post</p>; 109 if (error) return <p>Could not load: {error.message}</p>; 110 if (empty || !record || !rkey) return <p>No posts yet.</p>; 111 112 // Pass both record and rkey—no additional API call needed 113 return <BlueskyPost did={did} rkey={rkey} record={record} colorScheme="system" />; 114}; 115``` 116 117The same pattern works for other components. Just swap the collection NSID and component: 118 119```tsx 120const LatestLeafletDocument: React.FC<{ did: string }> = ({ did }) => { 121 const { record, rkey } = useLatestRecord(did, "pub.leaflet.document"); 122 return record && rkey ? ( 123 <LeafletDocument did={did} rkey={rkey} record={record} colorScheme="light" /> 124 ) : null; 125}; 126``` 127 128## Compose your own component 129 130The 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: 131 132```tsx 133import { useLatestRecord, useColorScheme, AtProtoRecord } from "atproto-ui"; 134import type { FeedPostRecord } from "atproto-ui"; 135 136const LatestPostSummary: React.FC<{ did: string }> = ({ did }) => { 137 const scheme = useColorScheme("system"); 138 const { rkey, loading, error } = useLatestRecord<FeedPostRecord>( 139 did, 140 "app.bsky.feed.post", 141 ); 142 143 if (loading) return <span>Loading</span>; 144 if (error || !rkey) return <span>No post yet.</span>; 145 146 return ( 147 <AtProtoRecord<FeedPostRecord> 148 did={did} 149 collection="app.bsky.feed.post" 150 rkey={rkey} 151 renderer={({ record }) => ( 152 <article data-color-scheme={scheme}> 153 <strong>{record?.text ?? "Empty post"}</strong> 154 </article> 155 )} 156 /> 157 ); 158}; 159``` 160 161There is a [demo](https://atproto-ui.wisp.place/) where you can see the components in live action. 162 163## Running the demo locally 164 165```bash 166npm install 167npm run dev 168``` 169 170Then open the printed Vite URL and try entering a Bluesky handle to see the components in action. 171 172## Next steps 173 174- Expand renderer coverage (e.g., Grain.social photos). 175- Expand documentation with TypeScript API references and theming guidelines. 176 177Contributions and ideas are welcome—feel free to open an issue or PR!