diff --git a/AGENTS.md b/AGENTS.md
index 0fc10c4..4104325 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -303,6 +303,70 @@ function usePosts() {
}
```
+### Infinite Scroll for Feeds
+
+For feed-like interfaces, implement infinite scroll using TanStack Query's `useInfiniteQuery` with Nostr's timestamp-based pagination:
+
+```typescript
+import { useNostr } from '@nostrify/react';
+import { useInfiniteQuery } from '@tanstack/react-query';
+
+export function useGlobalFeed() {
+ const { nostr } = useNostr();
+
+ return useInfiniteQuery({
+ queryKey: ['global-feed'],
+ queryFn: async ({ pageParam, signal }) => {
+ const filter = { kinds: [1], limit: 20 };
+ if (pageParam) filter.until = pageParam;
+
+ const events = await nostr.query([filter], {
+ signal: AbortSignal.any([signal, AbortSignal.timeout(1500)])
+ });
+
+ return events;
+ },
+ getNextPageParam: (lastPage) => {
+ if (lastPage.length === 0) return undefined;
+ return lastPage[lastPage.length - 1].created_at - 1; // Subtract 1 since 'until' is inclusive
+ },
+ initialPageParam: undefined,
+ });
+}
+```
+
+Example usage with intersection observer for automatic loading:
+
+```tsx
+import { useInView } from 'react-intersection-observer';
+
+function GlobalFeed() {
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useGlobalFeed();
+ const { ref, inView } = useInView();
+
+ useEffect(() => {
+ if (inView && hasNextPage) {
+ fetchNextPage();
+ }
+ }, [inView, hasNextPage, fetchNextPage]);
+
+ const posts = data?.pages.flat() || [];
+
+ return (
+
+ {posts.map((post) => (
+
+ ))}
+ {hasNextPage && (
+
+ {isFetchingNextPage && }
+
+ )}
+
+ );
+}
+```
+
#### Efficient Query Design
**Critical**: Always minimize the number of separate queries to avoid rate limiting and improve performance. Combine related queries whenever possible.
diff --git a/package-lock.json b/package-lock.json
index 1ed38a2..3e7b594 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -56,6 +56,7 @@
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
+ "react-intersection-observer": "^9.16.0",
"react-resizable-panels": "^2.1.3",
"react-router-dom": "^6.26.2",
"recharts": "^2.12.7",
@@ -6627,6 +6628,21 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
+ "node_modules/react-intersection-observer": {
+ "version": "9.16.0",
+ "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz",
+ "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
diff --git a/package.json b/package.json
index d6adff7..98f7eb5 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
+ "react-intersection-observer": "^9.16.0",
"react-resizable-panels": "^2.1.3",
"react-router-dom": "^6.26.2",
"recharts": "^2.12.7",