···1010## Features
11111212- Drop-in components for common record types (`BlueskyPost`, `BlueskyProfile`, `TangledString`, etc.).
1313+- Pass prefetched data directly to components to skip API calls—perfect for server-side rendering, caching, or when you already have the data.
1314- Hooks and helpers for composing your own renderers for your own applications, (PRs welcome!)
1415- Built on the lightweight [`@atcute/*`](https://github.com/atcute) clients.
1516···3940}
4041```
41424343+## Passing prefetched data to skip API calls
4444+4545+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.
4646+4747+```tsx
4848+import { BlueskyPost, useLatestRecord } from "atproto-ui";
4949+import type { FeedPostRecord } from "atproto-ui";
5050+5151+const MyComponent: React.FC<{ did: string }> = ({ did }) => {
5252+ // Fetch the latest post using the hook
5353+ const { record, rkey, loading } = useLatestRecord<FeedPostRecord>(
5454+ did,
5555+ "app.bsky.feed.post"
5656+ );
5757+5858+ if (loading) return <p>Loading…</p>;
5959+ if (!record || !rkey) return <p>No posts found.</p>;
6060+6161+ // Pass the fetched record directly—BlueskyPost won't re-fetch it
6262+ return <BlueskyPost did={did} rkey={rkey} record={record} />;
6363+};
6464+```
6565+6666+The same pattern works for all components:
6767+6868+```tsx
6969+// BlueskyProfile with prefetched data
7070+<BlueskyProfile did={did} record={profileRecord} />
7171+7272+// TangledString with prefetched data
7373+<TangledString did={did} rkey={rkey} record={stringRecord} />
7474+7575+// LeafletDocument with prefetched data
7676+<LeafletDocument did={did} rkey={rkey} record={documentRecord} />
7777+```
7878+4279### Available building blocks
43804481| Component / Hook | What it does |
4582| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
4646-| `AtProtoProvider` | Configures PLC directory (defaults to `https://plc.directory`) and shares protocol clients via React context. |
4747-| `BlueskyProfile` | Renders a profile card for a DID/handle. Accepts `fallback`, `loadingIndicator`, `renderer`, and `colorScheme`. |
4848-| `BlueskyPost` / `BlueskyQuotePost` | Shows a single Bluesky post, with quotation support, custom renderer overrides, and the same loading/fallback knobs. |
8383+| `AtProtoProvider` | Configures PLC directory (defaults to `https://plc.directory`) and shares protocol clients via React context. |
8484+| `AtProtoRecord` | Core component that fetches and renders any AT Protocol record. **Accepts a `record` prop to use prefetched data and skip API calls.** |
8585+| `BlueskyProfile` | Renders a profile card for a DID/handle. **Accepts a `record` prop to skip fetching.** Also supports `fallback`, `loadingIndicator`, `renderer`, and `colorScheme`. |
8686+| `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. |
4987| `BlueskyPostList` | Lists the latest posts with built-in pagination (defaults: 5 per page, pagination controls on). |
5050-| `TangledString` | Renders a Tangled string (gist-like record) with optional renderer overrides. |
5151-| `LeafletDocument` | Displays long-form Leaflet documents with blocks, theme support, and renderer overrides. |
5252-| `useDidResolution`, `useLatestRecord`, `usePaginatedRecords`, … | Hook-level access to records if you want to own the markup or prefill components. |
8888+| `TangledString` | Renders a Tangled string (gist-like record). **Accepts a `record` prop to skip fetching.** Optional renderer overrides available. |
8989+| `LeafletDocument` | Displays long-form Leaflet documents with blocks and theme support. **Accepts a `record` prop to skip fetching.** Renderer overrides available. |
9090+| `useDidResolution`, `useLatestRecord`, `usePaginatedRecords`, … | Hook-level access to records. `useLatestRecord` returns both the `record` and `rkey` so you can pass them directly to components. |
53915492All 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.
55935656-### Prefill components with the latest record
9494+### Using hooks to fetch data once
57955858-`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`.
9696+`useLatestRecord` gives you the most recent record for any collection along with its `rkey`. You can pass both to components to skip the fetch:
59976098```tsx
6199import { useLatestRecord, BlueskyPost } from "atproto-ui";
62100import type { FeedPostRecord } from "atproto-ui";
6310164102const LatestBlueskyPost: React.FC<{ did: string }> = ({ did }) => {
6565- const { rkey, loading, error, empty } = useLatestRecord<FeedPostRecord>(
103103+ const { record, rkey, loading, error, empty } = useLatestRecord<FeedPostRecord>(
66104 did,
67105 "app.bsky.feed.post",
68106 );
6910770108 if (loading) return <p>Fetching latest post…</p>;
71109 if (error) return <p>Could not load: {error.message}</p>;
7272- if (empty || !rkey) return <p>No posts yet.</p>;
110110+ if (empty || !record || !rkey) return <p>No posts yet.</p>;
731117474- return <BlueskyPost did={did} rkey={rkey} colorScheme="system" />;
112112+ // Pass both record and rkey—no additional API call needed
113113+ return <BlueskyPost did={did} rkey={rkey} record={record} colorScheme="system" />;
75114};
76115```
771167878-The same pattern works for other components: swap the collection NSID and the component you render once you have an `rkey`.
117117+The same pattern works for other components. Just swap the collection NSID and component:
7911880119```tsx
81120const LatestLeafletDocument: React.FC<{ did: string }> = ({ did }) => {
8282- const { rkey } = useLatestRecord(did, "pub.leaflet.document");
8383- return rkey ? (
8484- <LeafletDocument did={did} rkey={rkey} colorScheme="light" />
121121+ const { record, rkey } = useLatestRecord(did, "pub.leaflet.document");
122122+ return record && rkey ? (
123123+ <LeafletDocument did={did} rkey={rkey} record={record} colorScheme="light" />
85124 ) : null;
86125};
87126```
8812789128## Compose your own component
901299191-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:
130130+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:
9213193132```tsx
94133import { useLatestRecord, useColorScheme, AtProtoRecord } from "atproto-ui";
+19
lib/components/BlueskyPost.tsx
···2222 */
2323 rkey: string;
2424 /**
2525+ * Prefetched post record. When provided, skips fetching the post from the network.
2626+ * Note: Profile and avatar data will still be fetched unless a custom renderer is used.
2727+ */
2828+ record?: FeedPostRecord;
2929+ /**
2530 * Custom renderer component that receives resolved post data and status flags.
2631 */
2732 renderer?: React.ComponentType<BlueskyPostRendererInjectedProps>;
···119124export const BlueskyPost: React.FC<BlueskyPostProps> = ({
120125 did: handleOrDid,
121126 rkey,
127127+ record,
122128 renderer,
123129 fallback,
124130 loadingIndicator,
···197203 );
198204 }
199205206206+ // When record is provided, pass it directly to skip fetching
207207+ if (record) {
208208+ return (
209209+ <AtProtoRecord<FeedPostRecord>
210210+ record={record}
211211+ renderer={Wrapped}
212212+ fallback={fallback}
213213+ loadingIndicator={loadingIndicator}
214214+ />
215215+ );
216216+ }
217217+218218+ // Otherwise fetch the record using did, collection, and rkey
200219 return (
201220 <AtProtoRecord<FeedPostRecord>
202221 did={repoIdentifier}
+20
lib/components/BlueskyProfile.tsx
···1717 did: string;
1818 /**
1919 * Record key within the profile collection. Typically `'self'`.
2020+ * Optional when `record` is provided.
2021 */
2122 rkey?: string;
2323+ /**
2424+ * Prefetched profile record. When provided, skips fetching the profile from the network.
2525+ */
2626+ record?: ProfileRecord;
2227 /**
2328 * Optional renderer override for custom presentation.
2429 */
···9499export const BlueskyProfile: React.FC<BlueskyProfileProps> = ({
95100 did: handleOrDid,
96101 rkey = "self",
102102+ record,
97103 renderer,
98104 fallback,
99105 loadingIndicator,
···128134 />
129135 );
130136 };
137137+138138+ // When record is provided, pass it directly to skip fetching
139139+ if (record) {
140140+ return (
141141+ <AtProtoRecord<ProfileRecord>
142142+ record={record}
143143+ renderer={Wrapped}
144144+ fallback={fallback}
145145+ loadingIndicator={loadingIndicator}
146146+ />
147147+ );
148148+ }
149149+150150+ // Otherwise fetch the record using did, collection, and rkey
131151 return (
132152 <AtProtoRecord<ProfileRecord>
133153 did={repoIdentifier}
+18
lib/components/LeafletDocument.tsx
···3030 */
3131 rkey: string;
3232 /**
3333+ * Prefetched Leaflet document record. When provided, skips fetching from the network.
3434+ */
3535+ record?: LeafletDocumentRecord;
3636+ /**
3337 * Optional custom renderer for advanced layouts.
3438 */
3539 renderer?: React.ComponentType<LeafletDocumentRendererInjectedProps>;
···7074export const LeafletDocument: React.FC<LeafletDocumentProps> = ({
7175 did,
7276 rkey,
7777+ record,
7378 renderer,
7479 fallback,
7580 loadingIndicator,
···116121 );
117122 };
118123124124+ // When record is provided, pass it directly to skip fetching
125125+ if (record) {
126126+ return (
127127+ <AtProtoRecord<LeafletDocumentRecord>
128128+ record={record}
129129+ renderer={Wrapped}
130130+ fallback={fallback}
131131+ loadingIndicator={loadingIndicator}
132132+ />
133133+ );
134134+ }
135135+136136+ // Otherwise fetch the record using did, collection, and rkey
119137 return (
120138 <AtProtoRecord<LeafletDocumentRecord>
121139 did={did}
+17
lib/components/TangledString.tsx
···1111 did: string;
1212 /** Record key within the `sh.tangled.string` collection. */
1313 rkey: string;
1414+ /** Prefetched Tangled String record. When provided, skips fetching from the network. */
1515+ record?: TangledStringRecord;
1416 /** Optional renderer override for custom presentation. */
1517 renderer?: React.ComponentType<TangledStringRendererInjectedProps>;
1618 /** Fallback node displayed before loading begins. */
···5860export const TangledString: React.FC<TangledStringProps> = ({
5961 did,
6062 rkey,
6363+ record,
6164 renderer,
6265 fallback,
6366 loadingIndicator,
···7881 canonicalUrl={`https://tangled.org/strings/${did}/${encodeURIComponent(rkey)}`}
7982 />
8083 );
8484+8585+ // When record is provided, pass it directly to skip fetching
8686+ if (record) {
8787+ return (
8888+ <AtProtoRecord<TangledStringRecord>
8989+ record={record}
9090+ renderer={Wrapped}
9191+ fallback={fallback}
9292+ loadingIndicator={loadingIndicator}
9393+ />
9494+ );
9595+ }
9696+9797+ // Otherwise fetch the record using did, collection, and rkey
8198 return (
8299 <AtProtoRecord<TangledStringRecord>
83100 did={did}