Search code examples
next.jsreact-hooksscrollreact-query

My Custom useScrollPosition Hook isn't Working in Next JS, When I'm Trying to Fetch the Next Page of Results


Details

I'm using tRPC and using useInfiniteQuery to create the infinite scroll effect in a nextjs twitter clone. I believe that is working fine. I've tested it and I can fetch the next page of tweets.

However I've made a custom hook that will check where the users current position is on the page and the state doesn't appear to be updating at all. It will get the scroll position on page load and that's it.

Code for Feed Component

// Dependencies
import Link from 'next/link';
import { useState, useEffect } from 'react';

// API
import { api } from '~/utils/api';

// Components
import { LoadingSpinner } from '~/components/LoadingSpinner';
import { TweetView } from '~/components/TweetView';

function useScrollPosition() {
  const [scrollPosition, setScrollPosition] = useState(0);

  const handleScroll = () => {
    const height =
      document.documentElement.scrollHeight -
      document.documentElement.clientHeight;
    const winScroll =
      document.body.scrollTop || document.documentElement.scrollTop;

    const scrolled = (winScroll / height) * 100;
    setScrollPosition(scrolled);
  };

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);

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

  return scrollPosition;
}

// Feed Limit
const LIMIT = 10;

export const Feed = () => {
  const {
    data,
    isLoading: tweetsLoading,
    hasNextPage,
    fetchNextPage,
    isFetching,
  } = api.tweet.getAll.useInfiniteQuery(
    {
      limit: LIMIT,
    },
    {
      getNextPageParam: (lastPage) => lastPage.nextCursor,
    }
  );

  const scrollPosition = useScrollPosition();

  const tweets = data?.pages.flatMap((page) => page.tweetsWithUsers) ?? [];
  console.log(scrollPosition);

  useEffect(() => {
    const nextTweetPage = async () => {
      // Get the next page of data if the scroll position is at x
      if (scrollPosition > 90 && hasNextPage && !isFetching) {
        await fetchNextPage();
      }
    };
    nextTweetPage().catch((e) => console.log(e));
  }, [scrollPosition, hasNextPage, isFetching, fetchNextPage]);

  if (tweetsLoading) {
    return (
      <div className="mt-4 flex h-screen items-center justify-center">
        <LoadingSpinner size={40} />
      </div>
    );
  }

  if (!data) {
    return (
      <div className="flex flex-col items-center justify-center gap-6 p-6 text-center">
        <h3>{`Hmmm... Something went wrong getting these twoots, try refreshing?`}</h3>
        <Link
          href="/"
          className="rounded-full bg-bright-pink py-2.5 px-3.5 text-base font-bold text-white shadow-sm hover:bg-pink-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-bright-pink"
        >
          Home
        </Link>
      </div>
    );
  }

  return (
    <div className="h-full">
      {tweets.map((fullTweet) => (
        <TweetView
          tweetData={{ ...fullTweet }}
          key={fullTweet.tweet.id}
          input={{ limit: LIMIT }}
        />
      ))}
    </div>
  );
};

What I've Done

I've tried finding other solutions, like useCallback, and looking at other similar questions on stack overflow but none of the solutions are working.

Been stumped on this for a while and I can't move forward on the project without this working.


Solution

  • Current Hacky Solution

    I needed to get this working today realistically. I used a package called react-intersection-observer. created a div that will render at the bottom of the feed. Once that div with the intersection ref comes into view the next page will be fetched.

    I'm a bit annoyed that I can't get the hooks to work for whatever reason. But this does the job just fine.

    Here's the code if anyone else is struggling.

    Feed Component

    // Dependencies
    import Link from 'next/link';
    import { useEffect } from 'react';
    import { useInView } from 'react-intersection-observer';
    
    // API
    import { api } from '~/utils/api';
    
    // Components
    import { LoadingSpinner } from '~/components/LoadingSpinner';
    import { TweetView } from '~/components/TweetView';
    
    // Feed Limit
    const LIMIT = 10;
    
    export const Feed = () => {
      const {
        data,
        isLoading: tweetsLoading,
        hasNextPage,
        fetchNextPage,
        isFetching,
      } = api.tweet.getAll.useInfiniteQuery(
        {
          limit: LIMIT,
        },
        {
          getNextPageParam: (lastPage) => lastPage.nextCursor,
        }
      );
      const { ref, inView } = useInView({ threshold: 0 });
    
      const tweets = data?.pages.flatMap((page) => page.tweetsWithUsers) ?? [];
    
      useEffect(() => {
        const nextTweetPage = async () => {
          // Get the next page of data if the intersection comes into view
          if (inView && hasNextPage && !isFetching) {
            await fetchNextPage();
          }
        };
        nextTweetPage().catch((e) => console.log(e));
      }, [inView, hasNextPage, isFetching, fetchNextPage]);
    
      if (tweetsLoading) {
        return (
          <div className="mt-4 flex h-screen items-center justify-center">
            <LoadingSpinner size={40} />
          </div>
        );
      }
    
      if (!data) {
        return (
          <div className="flex flex-col items-center justify-center gap-6 p-6 text-center">
            <h3>{`Hmmm... Something went wrong getting these twoots, try refreshing?`}</h3>
            <Link
              href="/"
              className="rounded-full bg-bright-pink py-2.5 px-3.5 text-base font-bold text-white shadow-sm hover:bg-pink-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-bright-pink"
            >
              Home
            </Link>
          </div>
        );
      }
    
      return (
        <>
          <div className="h-full">
            {tweets.map((fullTweet) => (
              <TweetView
                tweetData={{ ...fullTweet }}
                key={fullTweet.tweet.id}
                input={{ limit: LIMIT }}
              />
            ))}
          </div>
          <div ref={ref}></div>
          {isFetching && (
            <div className="flex h-auto w-full items-center justify-center p-4">
              <LoadingSpinner size={40} />
            </div>
          )}
        </>
      );
    };