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",