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
···
## Features
- Drop-in components for common record types (`BlueskyPost`, `BlueskyProfile`, `TangledString`, etc.).
+
- Pass prefetched data directly to components to skip API calls—perfect for server-side rendering, caching, or when you already have the data.
- Hooks and helpers for composing your own renderers for your own applications, (PRs welcome!)
- Built on the lightweight [`@atcute/*`](https://github.com/atcute) clients.
···
}
```
+
## Passing prefetched data to skip API calls
+
+
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.
+
+
```tsx
+
import { BlueskyPost, useLatestRecord } from "atproto-ui";
+
import type { FeedPostRecord } from "atproto-ui";
+
+
const MyComponent: React.FC<{ did: string }> = ({ did }) => {
+
// Fetch the latest post using the hook
+
const { record, rkey, loading } = useLatestRecord<FeedPostRecord>(
+
did,
+
"app.bsky.feed.post"
+
);
+
+
if (loading) return <p>Loading…</p>;
+
if (!record || !rkey) return <p>No posts found.</p>;
+
+
// Pass the fetched record directly—BlueskyPost won't re-fetch it
+
return <BlueskyPost did={did} rkey={rkey} record={record} />;
+
};
+
```
+
+
The same pattern works for all components:
+
+
```tsx
+
// BlueskyProfile with prefetched data
+
<BlueskyProfile did={did} record={profileRecord} />
+
+
// TangledString with prefetched data
+
<TangledString did={did} rkey={rkey} record={stringRecord} />
+
+
// LeafletDocument with prefetched data
+
<LeafletDocument did={did} rkey={rkey} record={documentRecord} />
+
```
+
### Available building blocks
| Component / Hook | What it does |
| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
-
| `AtProtoProvider` | Configures PLC directory (defaults to `https://plc.directory`) and shares protocol clients via React context. |
-
| `BlueskyProfile` | Renders a profile card for a DID/handle. Accepts `fallback`, `loadingIndicator`, `renderer`, and `colorScheme`. |
-
| `BlueskyPost` / `BlueskyQuotePost` | Shows a single Bluesky post, with quotation support, custom renderer overrides, and the same loading/fallback knobs. |
+
| `AtProtoProvider` | Configures PLC directory (defaults to `https://plc.directory`) and shares protocol clients via React context. |
+
| `AtProtoRecord` | Core component that fetches and renders any AT Protocol record. **Accepts a `record` prop to use prefetched data and skip API calls.** |
+
| `BlueskyProfile` | Renders a profile card for a DID/handle. **Accepts a `record` prop to skip fetching.** Also supports `fallback`, `loadingIndicator`, `renderer`, and `colorScheme`. |
+
| `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. |
| `BlueskyPostList` | Lists the latest posts with built-in pagination (defaults: 5 per page, pagination controls on). |
-
| `TangledString` | Renders a Tangled string (gist-like record) with optional renderer overrides. |
-
| `LeafletDocument` | Displays long-form Leaflet documents with blocks, theme support, and renderer overrides. |
-
| `useDidResolution`, `useLatestRecord`, `usePaginatedRecords`, … | Hook-level access to records if you want to own the markup or prefill components. |
+
| `TangledString` | Renders a Tangled string (gist-like record). **Accepts a `record` prop to skip fetching.** Optional renderer overrides available. |
+
| `LeafletDocument` | Displays long-form Leaflet documents with blocks and theme support. **Accepts a `record` prop to skip fetching.** Renderer overrides available. |
+
| `useDidResolution`, `useLatestRecord`, `usePaginatedRecords`, … | Hook-level access to records. `useLatestRecord` returns both the `record` and `rkey` so you can pass them directly to components. |
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.
-
### Prefill components with the latest record
+
### Using hooks to fetch data once
-
`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`.
+
`useLatestRecord` gives you the most recent record for any collection along with its `rkey`. You can pass both to components to skip the fetch:
```tsx
import { useLatestRecord, BlueskyPost } from "atproto-ui";
import type { FeedPostRecord } from "atproto-ui";
const LatestBlueskyPost: React.FC<{ did: string }> = ({ did }) => {
-
const { rkey, loading, error, empty } = useLatestRecord<FeedPostRecord>(
+
const { record, rkey, loading, error, empty } = useLatestRecord<FeedPostRecord>(
did,
"app.bsky.feed.post",
);
if (loading) return <p>Fetching latest post…</p>;
if (error) return <p>Could not load: {error.message}</p>;
-
if (empty || !rkey) return <p>No posts yet.</p>;
+
if (empty || !record || !rkey) return <p>No posts yet.</p>;
-
return <BlueskyPost did={did} rkey={rkey} colorScheme="system" />;
+
// Pass both record and rkey—no additional API call needed
+
return <BlueskyPost did={did} rkey={rkey} record={record} colorScheme="system" />;
};
```
-
The same pattern works for other components: swap the collection NSID and the component you render once you have an `rkey`.
+
The same pattern works for other components. Just swap the collection NSID and component:
```tsx
const LatestLeafletDocument: React.FC<{ did: string }> = ({ did }) => {
-
const { rkey } = useLatestRecord(did, "pub.leaflet.document");
-
return rkey ? (
-
<LeafletDocument did={did} rkey={rkey} colorScheme="light" />
+
const { record, rkey } = useLatestRecord(did, "pub.leaflet.document");
+
return record && rkey ? (
+
<LeafletDocument did={did} rkey={rkey} record={record} colorScheme="light" />
) : null;
};
```
## Compose your own component
-
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:
+
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:
```tsx
import { useLatestRecord, useColorScheme, AtProtoRecord } from "atproto-ui";
+19
lib/components/BlueskyPost.tsx
···
*/
rkey: string;
/**
+
* Prefetched post record. When provided, skips fetching the post from the network.
+
* Note: Profile and avatar data will still be fetched unless a custom renderer is used.
+
*/
+
record?: FeedPostRecord;
+
/**
* Custom renderer component that receives resolved post data and status flags.
*/
renderer?: React.ComponentType<BlueskyPostRendererInjectedProps>;
···
export const BlueskyPost: React.FC<BlueskyPostProps> = ({
did: handleOrDid,
rkey,
+
record,
renderer,
fallback,
loadingIndicator,
···
);
}
+
// When record is provided, pass it directly to skip fetching
+
if (record) {
+
return (
+
<AtProtoRecord<FeedPostRecord>
+
record={record}
+
renderer={Wrapped}
+
fallback={fallback}
+
loadingIndicator={loadingIndicator}
+
/>
+
);
+
}
+
+
// Otherwise fetch the record using did, collection, and rkey
return (
<AtProtoRecord<FeedPostRecord>
did={repoIdentifier}
+20
lib/components/BlueskyProfile.tsx
···
did: string;
/**
* Record key within the profile collection. Typically `'self'`.
+
* Optional when `record` is provided.
*/
rkey?: string;
+
/**
+
* Prefetched profile record. When provided, skips fetching the profile from the network.
+
*/
+
record?: ProfileRecord;
/**
* Optional renderer override for custom presentation.
*/
···
export const BlueskyProfile: React.FC<BlueskyProfileProps> = ({
did: handleOrDid,
rkey = "self",
+
record,
renderer,
fallback,
loadingIndicator,
···
/>
);
};
+
+
// When record is provided, pass it directly to skip fetching
+
if (record) {
+
return (
+
<AtProtoRecord<ProfileRecord>
+
record={record}
+
renderer={Wrapped}
+
fallback={fallback}
+
loadingIndicator={loadingIndicator}
+
/>
+
);
+
}
+
+
// Otherwise fetch the record using did, collection, and rkey
return (
<AtProtoRecord<ProfileRecord>
did={repoIdentifier}
+18
lib/components/LeafletDocument.tsx
···
*/
rkey: string;
/**
+
* Prefetched Leaflet document record. When provided, skips fetching from the network.
+
*/
+
record?: LeafletDocumentRecord;
+
/**
* Optional custom renderer for advanced layouts.
*/
renderer?: React.ComponentType<LeafletDocumentRendererInjectedProps>;
···
export const LeafletDocument: React.FC<LeafletDocumentProps> = ({
did,
rkey,
+
record,
renderer,
fallback,
loadingIndicator,
···
);
};
+
// When record is provided, pass it directly to skip fetching
+
if (record) {
+
return (
+
<AtProtoRecord<LeafletDocumentRecord>
+
record={record}
+
renderer={Wrapped}
+
fallback={fallback}
+
loadingIndicator={loadingIndicator}
+
/>
+
);
+
}
+
+
// Otherwise fetch the record using did, collection, and rkey
return (
<AtProtoRecord<LeafletDocumentRecord>
did={did}
+17
lib/components/TangledString.tsx
···
did: string;
/** Record key within the `sh.tangled.string` collection. */
rkey: string;
+
/** Prefetched Tangled String record. When provided, skips fetching from the network. */
+
record?: TangledStringRecord;
/** Optional renderer override for custom presentation. */
renderer?: React.ComponentType<TangledStringRendererInjectedProps>;
/** Fallback node displayed before loading begins. */
···
export const TangledString: React.FC<TangledStringProps> = ({
did,
rkey,
+
record,
renderer,
fallback,
loadingIndicator,
···
canonicalUrl={`https://tangled.org/strings/${did}/${encodeURIComponent(rkey)}`}
/>
);
+
+
// When record is provided, pass it directly to skip fetching
+
if (record) {
+
return (
+
<AtProtoRecord<TangledStringRecord>
+
record={record}
+
renderer={Wrapped}
+
fallback={fallback}
+
loadingIndicator={loadingIndicator}
+
/>
+
);
+
}
+
+
// Otherwise fetch the record using did, collection, and rkey
return (
<AtProtoRecord<TangledStringRecord>
did={did}