typescriptintermediate

Infinite Scroll with Server Actions

Implement infinite scroll using server actions with cursor-based pagination.

typescript
// app/feed/actions.ts
'use server';

interface Post {
  id: string;
  title: string;
  excerpt: string;
  createdAt: string;
}

interface FeedResult {
  posts: Post[];
  nextCursor: string | null;
}

export async function loadMorePosts(cursor?: string): Promise<FeedResult> {
  const params = new URLSearchParams({ limit: '10' });
  if (cursor) params.set('cursor', cursor);

  const res = await fetch(`https://api.example.com/posts?${params}`);
  const data = await res.json();

  return {
    posts: data.posts,
    nextCursor: data.nextCursor,
  };
}

// app/feed/InfiniteFeed.tsx
'use client';

import { useEffect, useRef, useState, useCallback } from 'react';
import { loadMorePosts } from './actions';

interface Post {
  id: string;
  title: string;
  excerpt: string;
  createdAt: string;
}

export function InfiniteFeed({ initialPosts, initialCursor }: {
  initialPosts: Post[];
  initialCursor: string | null;
}) {
  const [posts, setPosts] = useState(initialPosts);
  const [cursor, setCursor] = useState(initialCursor);
  const [loading, setLoading] = useState(false);
  const loaderRef = useRef<HTMLDivElement>(null);

  const loadMore = useCallback(async () => {
    if (loading || !cursor) return;
    setLoading(true);
    const result = await loadMorePosts(cursor);
    setPosts((prev) => [...prev, ...result.posts]);
    setCursor(result.nextCursor);
    setLoading(false);
  }, [cursor, loading]);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) loadMore();
      },
      { threshold: 0.1 }
    );
    if (loaderRef.current) observer.observe(loaderRef.current);
    return () => observer.disconnect();
  }, [loadMore]);

  return (
    <div>
      {posts.map((post) => (
        <article key={post.id} className="p-4 border-b">
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
      <div ref={loaderRef} className="py-8 text-center">
        {loading && <span>Loading...</span>}
        {!cursor && <span>No more posts</span>}
      </div>
    </div>
  );
}

Use Cases

  • social feeds
  • product listings
  • blog archives

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.