mirror of
https://gitlab.com/soapbox-pub/mkstack.git
synced 2025-09-23 17:56:07 +00:00
Merge branch 'infiniteScroll' into 'main'
Add context for pagination (i.e., infinite feed scroll) See merge request soapbox-pub/mkstack!16
This commit is contained in:
commit
d1744b7d71
64
AGENTS.md
64
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 (
|
||||
<div className="space-y-4">
|
||||
{posts.map((post) => (
|
||||
<PostCard key={post.id} post={post} />
|
||||
))}
|
||||
{hasNextPage && (
|
||||
<div ref={ref} className="py-4">
|
||||
{isFetchingNextPage && <Skeleton className="h-20 w-full" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Efficient Query Design
|
||||
|
||||
**Critical**: Always minimize the number of separate queries to avoid rate limiting and improve performance. Combine related queries whenever possible.
|
||||
|
16
package-lock.json
generated
16
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user