+55
-16
README.md
+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
+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
+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
+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
+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}