Next.js Discord

Discord Forum

How to achieve SSG + infinite scroll?

Answered
Chipping Sparrow posted this in #help-forum
Open in Discord
Chipping SparrowOP
I'm developing post pages using Next.js 14 with the app router and the generateStaticParams function to achieve Static Site Generation (SSG). Each post page is located at app/posts/[slug]/page.tsx and is created once on the backend to be served statically. However, I also want to implement infinite scrolling so that when a user scrolls down, new static pages load seamlessly on the same page without a full reload, and the URL updates accordingly.

Example: https://eightify.app/how-to/how-to-use-mla-format-guide-tips

Can someone give me a direction how to achieve this? The question is not so about how to implement the infinite scroll, but about how to combine the static generation and dynamic components which uses useEffect and similar.
Answered by B33fb0n3
to create a static generation of the page, but still the dynamic content, you can fetch the first chunk of data from the server and display it serverside. The client component that handles the infinite scrolling then fetches the next chunk when the user scrolled down. Like that you have a static generation serverside (first chunk loaded by server), but still dynamic content (infinite scroll on client)
View full answer

20 Replies

Answer
Chipping SparrowOP
@B33fb0n3 thank u very much for finding time to answer!

The problem is that next js saying that I can't import client side component in server component.
Chipping SparrowOP
Error: 
  × You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
I can return back with full example code, I think it would be better.
@Chipping Sparrow Error: × You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
thanks for sharing. Inside the component, that uses useState add a use client on top of the file. You know what I mean? Else share your code please. I show you where
Chipping SparrowOP
Well, if I put 'use client' in server component would it be statically generated still?
@Chipping Sparrow Well, if I put `'use client'` in server component would it be statically generated still?
It will be rendered with SSR, but hydrated on the client
Chipping SparrowOP
I will get back with example, thank u very much!
@Chipping Sparrow I will get back with example, thank u very much!
When you will provide the example?
Chipping SparrowOP
well, in 30 min / 1 hour
Chipping SparrowOP
├ ● /posts/[slug]                        782 B           396 kB
├   ├ /posts/post-1
├   ├ /posts/post-2
├   └ /posts/post-3

●  (SSG)      prerendered as static HTML (uses getStaticProps)


import { ClientSidePostPage } from '@/app/posts/[slug]/ClientSidePostPage';
import { fetchPostData, postsData } from '@/app/posts/[slug]/data';

// Static generation
export async function generateStaticParams() {
  return postsData.map((post) => ({ slug: post.slug }));
}

// Server component
// @ts-ignore
export default async function PostPage({ params: { slug } }) {
  const initialPost = await fetchPostData(slug);

  if (!initialPost) {
    // Handle the case where the post is not found (e.g., return a 404 page)
    return <div>Post not found</div>;
  }

  return <ClientSidePostPage initialPost={initialPost} />;
}


// Interface for Post
export interface Post {
  slug: string;
  title: string;
  content: string;
  related: string[];
}

// Dummy JSON data
export const postsData: Post[] = [
  {
    slug: 'post-1',
    title: 'Post 1',
    content: 'text '.repeat(500),
    related: ['post-2', 'post-3'],
  },
  {
    slug: 'post-2',
    title: 'Post 2',
    content: 'text '.repeat(500),
    related: ['post-1', 'post-3'],
  },
  {
    slug: 'post-3',
    title: 'Post 3',
    content: 'text '.repeat(500),
    related: ['post-1', 'post-2'],
  },
];

// Client component for handling infinite scrolling

// Simulating fetchPostData using dummy data
export const fetchPostData = async (
  slug: string,
): Promise<Post | undefined> => {
  return postsData.find((post) => post.slug === slug);
};
'use client';

import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';

import type { Post } from '@/app/posts/[slug]/data';
import { fetchPostData } from '@/app/posts/[slug]/data';

export function ClientSidePostPage({ initialPost }: { initialPost: Post }) {
  const router = useRouter();
  const [relatedPosts, setRelatedPosts] = useState<Post[]>([]);
  const [currentPost, setCurrentPost] = useState<Post>(initialPost);

  useEffect(() => {
    const loadMorePosts = async (slugs: string[]) => {
      const relatedData = await Promise.all(
        slugs.map((slug) => fetchPostData(slug)),
      );
      setRelatedPosts(
        relatedData.filter((post) => post !== undefined) as Post[],
      );
    };

    loadMorePosts(currentPost.related || []);
  }, [currentPost]);

  const handleScroll = () => {
    if (window.innerHeight + window.pageYOffset >= document.body.offsetHeight) {
      if (relatedPosts.length > 0) {
        const nextPost = relatedPosts.shift()!;
        router.push(`/posts/${nextPost.slug}`);
        setCurrentPost(nextPost);
        setRelatedPosts([...relatedPosts]);
      }
    }
  };

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [relatedPosts]);

  return (
    <div className="max-w-96 rounded-lg">
      <h1>{currentPost.title}</h1>
      <p>{currentPost.content}</p>

      {/* Load related posts */}
      {relatedPosts.map((post) => (
        <div key={post.slug}>
          <h1>{post.title}</h1>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  );
}
seem like working!!!!
@B33fb0n3 you are the goat 🔥
and html contains the initial first post, awesome!
@Chipping Sparrow seem like working!!!! <@301376057326567425> you are the goat 🔥
you are good. I just told you the solution how to do it. But you created it! Good job 👏
Please mark solution
Chipping SparrowOP
done