skip to Main Content

I have a component where I get answers to a post, and I want the answers to be displayed like an infinite scroll, but when I call the Server Action on the client, I get the error Maximum call stack size exceeded.

The server actions:

export async function getAnswers(params: GetAnswersParams) {
  try {
    connectToDatabase();

    const { questionId, sortBy, page = 1, pageSize = 10 } = params;

    const skipAmount = (page - 1) * pageSize;

    let sortOptions = {};

    switch (sortBy) {
      case 'highestUpvotes':
        sortOptions = { upvotes: -1 };
        break;
      case 'lowestUpvotes':
        sortOptions = { upvotes: 1 };
        break;
      case 'recent':
        sortOptions = { createdAt: -1 };
        break;
      case 'old':
        sortOptions = { createdAt: 1 };
        break;

      default:
        break;
    }

    const answers = await Answer.find({ question: questionId })
      .populate('author', '_id clerkId name picture')
      .sort(sortOptions)
      .skip(skipAmount)
      .limit(pageSize);

    const totalAnswers = await Answer.countDocuments({
      question: questionId,
    });

    const totalPages = Math.ceil(totalAnswers / pageSize);

    const isNext = totalAnswers > skipAmount + answers.length;

    return { answers, isNext, totalPages };
  } catch (error) {
    console.log(error);
    throw error;
  }
}

The server component:

const AllAnswers = async ({ questionId, userId, totalAnswers, page, filter }: Props) => {
  const { answers, isNext, totalPages } = await getAnswers({
    questionId,
    page: page ? +page : 1,
    sortBy: filter,
  });

  return (
    <div className='mt-11'>
      <div className='flex items-center justify-between'>
        <h3 className='primary-text-gradient'>{`${totalAnswers} ${totalAnswers === 1 ? 'Answer' : 'Answers'}`}</h3>

        <Filter filters={AnswerFilters} />
      </div>

      <LoadMoreAnswers
        initialAnswers={JSON.stringify(answers)}
        questionId={JSON.stringify(questionId)}
        userId={userId}
        filter={filter}
      />
    </div>
  );
};

export default AllAnswers;

The client component:

const LoadMoreAnswers = ({ initialAnswers, questionId, userId, filter }: Props) => {
  const [allAnswers, setAllAnswers] = useState(JSON.parse(initialAnswers));
  // const { ref, inView } = useInView();

  const loadMoreAnswers = async () => {
    try {
      const newAnswers = await getAnswers({
        questionId: JSON.parse(questionId),
        page: 2,
        sortBy: 'recent',
      });

      // setAllAnswers((oldAnswers) => [...oldAnswers, ...newAnswers]);

      console.log(newAnswers);
    } catch (error) {
      console.error('Error loading more answers:', error);
    }
  };



  return (
    <>
      {allAnswers.map((answer) => (
        <article key={answer._id} className='light-border border-b py-10'>
          <div className='mb-8 flex flex-col-reverse justify-between gap-5 sm:flex-row sm:items-center sm:gap-2'>
            <Link
              href={`/profile/${answer.author.clerkId}`}
              className='flex flex-1 items-start gap-1 sm:items-center'
            >
              <Image
                src={answer.author.picture}
                alt='profile picture'
                width={18}
                height={18}
                className='rounded-full object-cover max-sm:mt-0.5'
              />
              <div className='flex flex-col sm:flex-row sm:items-center'>
                <p className='body-semibold text-dark300_light700'>
                  {answer.author.name}
                </p>
                <p className='small-regular text-dark400_light500 mt-0.5 line-clamp-1'>
                  <span className='mx-0.5 max-sm:hidden'>•</span>
                  {/* answered {getTimestamp(answer.createdAt)} */}
                </p>
              </div>
            </Link>

            <div className='flex justify-end'>
              <Votes
                type='Answer'
                itemId={JSON.stringify(answer._id)}
                userId={userId}
                upvotes={answer.upvotes.length}
                hasupVoted={answer.upvotes.includes(userId)}
                downvotes={answer.downvotes.length}
                hasdownVoted={answer.downvotes.includes(userId)}
              />
            </div>
          </div>

          <ParseHTML data={answer.content} />
        </article>
      ))}

      {/* <div className='mt-11 flex w-full items-center justify-center' ref={ref}> */}
      {/* <Icons.Spinner className='size-10 animate-spin' /> */}
      <Button onClick={loadMoreAnswers}>Load more</Button>
      {/* </div> */}
    </>
  );
};

export default LoadMoreAnswers;

I tried to do it with a spinner at the bottom, using react-intersection-observer, but now I just added a button to test the fetch and it doesn’t work… I don’t understand why it doesn’t work.

For now, I just want to add page 2 when clicking the button, then I think I can figure out the rest.

2

Answers


  1. Chosen as BEST ANSWER

    This is the correct server action up to this point. Notice the total pages implementation might've been wrong too. The problem was the format I was returning the answers from the server action:

        export async function getAnswers(params: GetAnswersParams) {
      try {
        connectToDatabase();
    
        const { questionId, sortBy, page = 1, pageSize = 5 } = params;
    
        const skipAmount = (page - 1) * pageSize;
    
        let sortOptions = {};
        switch (sortBy) {
          case 'highestUpvotes':
            sortOptions = { upvotes: -1 };
            break;
          case 'lowestUpvotes':
            sortOptions = { upvotes: 1 };
            break;
          case 'recent':
            sortOptions = { createdAt: -1 };
            break;
          case 'old':
            sortOptions = { createdAt: 1 };
            break;
    
          default:
            break;
        }
    
        const answers = await Answer.find({ question: questionId })
          .populate('author', '_id clerkId name picture')
          .sort(sortOptions)
          .skip(skipAmount)
          .limit(pageSize);
    
        const totalAnswers = await Answer.countDocuments({
          question: questionId,
        });
    
        // const totalPages = Math.ceil(totalAnswers / pageSize);
    
        const isNext = totalAnswers > skipAmount + answers.length;
    
        return {
          answers: JSON.parse(JSON.stringify(answers)),
          isNext,
        };
      } catch (error) {
        console.log(error);
        throw error;
      }
    }
    

  2. First remark i see in your code is that you call a server action in a client component but the action isn’t declared as server action. Also if you have to await for a server action response in a client component, one way is to use useTransition hook.

    Step 1: Make use you server action file starts with 'use server'

    Step 2: in your client component, add this:

    const [isLoading, startTransition] = useTransition();
    const [allAnswers, setAllAnswers] = useState(JSON.parse(initialAnswers));
      // const { ref, inView } = useInView();
    
      const loadMoreAnswers = () => startTransition(async () => {
        try {
          const newAnswers = await getAnswers({
            questionId: JSON.parse(questionId),
            page: 2,
            sortBy: 'recent',
          });
    
          // setAllAnswers((oldAnswers) => [...oldAnswers, ...newAnswers]);
    
          console.log(newAnswers);
        } catch (error) {
          console.error('Error loading more answers:', error);
        }
      });
    

    You can use the isLoading state to show a loading spinner or whatever.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search