Next.js Discord

Discord Forum

is there a way to prevent revalidation even if revalidatePath is run from a server action

Unanswered
Pacific sand lance posted this in #help-forum
Open in Discord
Pacific sand lanceOP
i have an infinite scrolling component for my twitter clone and when you do anything that runs a server action that has revalidatePath("/") in it such as like a post or add a new comment to a post, it causes the infinite scrolling component to rerender and then it reloads the cards that are in view.

i tried adding client side state for this so that the ui shows only the state instead of whats i the db that way this doesnt happen but having to add state to every single component is alot of work. any ideas?

180 Replies

Pacific sand lanceOP
or if my infinite scrolling component is just bad you could say that too /app/components/loadmorediscover
Pacific sand lanceOP
ok ive heard about this revalidateTag thing but i didnt understand what its for when revalidatePath exists
yes but you need to tag your data function with unstable_cache since you are using prisma
@Ray yes but you need to tag your data function with `unstable_cache` since you are using prisma
Pacific sand lanceOP
ok revalidateTag takes a string as the path, but how do you know whixh tag to use? this was the part that always confused me
'use server'
 
import { revalidateTag } from 'next/cache'
 
export default async function submit() {
  await addPost()
  revalidateTag('posts')
}
here they say posts but where do they get that from or how was it deceided
the tag is what you assign in unstable_cache
Pacific sand lanceOP
ok one sec
@Ray the tag is what you assign in `unstable_cache`
Pacific sand lanceOP
export default async function Page() {
  try {
    const getCachedUser = unstable_cache(async id => getPosts(id), ['posts']);
    const { timelinePosts, timelinePostsCount } = (await getPosts(1)) ?? [];

    if (timelinePosts === undefined || timelinePosts.length === 0) return;

    await Promise.allSettled(
      timelinePosts.map(async (post: PostWithAuthor) => {
        try {
          const time = await getPostTime(post.createdAt);
          post.postTime = time;
        } catch (error) {
          return { message: 'Error calculating post time' };
        }
      })
    );
    const user = await getCachedUser(1);
    console.log(user);
    return (
      <Box minHeight="100vh">
        <TimeLineTabs />
        <LoadMoreForYou initialPosts={timelinePosts} timelinePostsCount={timelinePostsCount} />
      </Box>
    );
  } catch (error) {
    return <NoDataFound />;
  }
}
ok i did something like this, so then in the getPosts server action, i woudl do
revalidatePath ?
const getCachedUser = unstable_cache(async id => getPosts(id), ['posts'], { tags: ['posts'] });
and move it out of the component
then use revalidateTag('posts') in your server action
Pacific sand lanceOP
ok
@Ray ts const getCachedUser = unstable_cache(async id => getPosts(id), ['posts'], { tags: ['posts'] });
Pacific sand lanceOP
'use client';
import { useEffect, useState, useCallback } from 'react';
import { unstable_cache } from 'next/cache';

const getCachedUser = unstable_cache(async id => getPosts(id), ['posts'], { tags: ['posts'] });

export default function LoadMoreDiscover({
  initialPosts,
  otherTimelinePostsCount,
}: LoadMoreDiscoverProps) {
  const [posts, setPosts] = useState<PostWithAuthor[]>([]);
  const [totalPostCount, setTotalPostCount] = useState(0);
  const [isLoading, setIsLoading] = useState(false);
  const [initialLoading, setInitialLoading] = useState(true);
  const { ref, inView } = useInView();
  const { data: session } = useSession();

  useEffect(() => {
    setPosts(initialPosts);
    setTotalPostCount(otherTimelinePostsCount);
    setTimeout(() => setInitialLoading(false), 1000);
  }, [initialPosts, otherTimelinePostsCount]);

  const getNewPosts = useCallback(async () => {
    const nextPage = posts.length / 5 + 1;

    // const { otherTimeLinePosts, otherTimelinePostsCount } = (await getPosts(nextPage)) ?? [];
    const { otherTimeLinePosts, otherTimelinePostsCount } = (await getCachedUser(nextPage)) ?? [];

    if (otherTimeLinePosts === undefined) return;

    await Promise.allSettled(
      otherTimeLinePosts.map(async (post: PostWithAuthor) => {
        try {
          const test = await getPostTime(post.createdAt);
          post.postTime = test;
        } catch (error) {
          return { message: 'Error calculating post time' };
        }
      })
    );
    setPosts(prevPosts => {
      const newPosts = otherTimeLinePosts.filter(
        newPost => !prevPosts.some(prevPost => prevPost.id === newPost.id)
      );
      return [...prevPosts, ...newPosts];
    });
    setTotalPostCount(otherTimelinePostsCount);
    setIsLoading(false);
  }, [posts.length]);
ok so i am assuming that this doesnt work in client components?`
cause it says Error: Invariant: incrementalCache missing in unstable_cache async (id)=>(0,_lib_actions__WEBPACK_IMPORTED_MODULE_2__.getPosts)(id)
are you calling getCachedUser in client component?
Pacific sand lanceOP
yea
const { otherTimeLinePosts, otherTimelinePostsCount } = (await getCachedUser(nextPage)) ?? [];
it need to be use on server
Pacific sand lanceOP
ok
@Ray it need to be use on server
Pacific sand lanceOP
import NoDataFound from '../components/NoDataFound';
import { Box } from '@chakra-ui/react';
import TimeLineTabs from '../components/TimeLineTabs';
import { getPosts, getPostTime } from '../lib/actions';
import { PostWithAuthor } from '../lib/definitions';
import LoadMoreDiscover from '../components/LoadMoreDiscover';
import { unstable_cache } from 'next/cache';

const getCachedUser = unstable_cache(async id => getPosts(id), ['posts'], { tags: ['posts'] });

export default async function Page() {
  try {
    const { otherTimeLinePosts, otherTimelinePostsCount } = (await getCachedUser(offset)) ?? [];

    if (otherTimeLinePosts === undefined || otherTimeLinePosts.length === 0) return;

    await Promise.allSettled(
      otherTimeLinePosts.map(async (post: PostWithAuthor) => {
        try {
          const time = await getPostTime(post.createdAt);
          post.postTime = time;
        } catch (error) {
          return { message: 'Error calculating post time' };
        }
      })
    );

    return (
      <Box minHeight="100vh">
        <TimeLineTabs />
        <LoadMoreDiscover
          initialPosts={otherTimeLinePosts}
          otherTimelinePostsCount={otherTimelinePostsCount}
        />
      </Box>
    );
  } catch (error) {
    return <NoDataFound />;
  }
}
problem is though how will i know what offset to use for my infinte scroll if i am in a server component? there is no state to remember anything?
dont really think it makes sense to use query params for an infinte scroll either
I think you should load the first set of data on server component then pass it to a client component with a useState hook. And create a server action for client component to fetch with offset
and the other server action, use revalidateTag to invalidate the data instead of revalidatePath('/')
assuming the infinity scroll is on the route "/"
Pacific sand lanceOP
ok
Pacific sand lanceOP
still doesnt seem to work. its not refreshing the infinite scroll anymore thankfully but i dont think that any of this data is being refreshed. i can press the ❤️ or comment and the ui doesnt change
export async function getFirstPosts() {
  try {
/* gets first 5 posts */
  ...stuff
    revalidateTag('posts');

    return {
      timelinePosts,
      otherTimeLinePosts,
      userId,
    };
  } catch (error) {
    return { message: 'Unable to fetch posts' };
  }
}

export async function getPosts(page: number = 1) {
  try {
/* gets posts from offset*/
   ...stuff
    revalidateTag('/');
    return {
      timelinePosts,
      otherTimeLinePosts,
      timelinePostsCount,
      otherTimelinePostsCount,
      userId,
    };
  } catch (error) {
    return { message: 'Unable to fetch posts' };
  }
}
const getCachedUser = unstable_cache(async () => getFirstPosts(), ['posts'], { tags: ['posts'] });

export default async function Page() {
  try {
    const { otherTimeLinePosts } = (await getCachedUser()) ?? [];

    if (otherTimeLinePosts === undefined || otherTimeLinePosts.length === 0) return;

    await Promise.allSettled(
      otherTimeLinePosts.map(async (post: PostWithAuthor) => {
        try {
          const time = await getPostTime(post.createdAt);
          post.postTime = time;
        } catch (error) {
          return { message: 'Error calculating post time' };
        }
      })
    );

    return (
      <Box minHeight="100vh">
        <TimeLineTabs />
        <LoadMoreDiscover
          initialPosts={otherTimeLinePosts}
          otherTimelinePostsCount={otherTimeLinePosts.length}
        />
      </Box>
    );
  } catch (error) {
    return <NoDataFound />;
  }
}
'use client';

export default function LoadMoreDiscover({
  initialPosts,
  otherTimelinePostsCount,
}: LoadMoreDiscoverProps) {
  const [posts, setPosts] = useState<PostWithAuthor[]>(initialPosts);
  const [totalPostCount, setTotalPostCount] = useState(otherTimelinePostsCount);
  const [isLoading, setIsLoading] = useState(false);
  const [initialLoading, setInitialLoading] = useState(true);
  const { ref, inView } = useInView();
  const { data: session } = useSession();

  useEffect(() => {
    setTimeout(() => setInitialLoading(false), 1000);
  }, []);

  const getNewPosts = useCallback(async () => {
    const nextPage = posts.length / 5 + 1;

    const { otherTimeLinePosts, otherTimelinePostsCount } = (await getPosts(nextPage)) ?? [];

    if (otherTimeLinePosts === undefined) return;

    await Promise.allSettled(
      otherTimeLinePosts.map(async (post: PostWithAuthor) => {
        try {
          const test = await getPostTime(post.createdAt);
          post.postTime = test;
        } catch (error) {
          return { message: 'Error calculating post time' };
        }
      })
    );
    setPosts(prevPosts => {
      const newPosts = otherTimeLinePosts.filter(
        newPost => !prevPosts.some(prevPost => prevPost.id === newPost.id)
      );
      return [...prevPosts, ...newPosts];
    });
    setTotalPostCount(otherTimelinePostsCount);
    setIsLoading(false);
  }, [posts.length]);
why you have revalidateTag('/') in the getPosts function?
Pacific sand lanceOP
oh
ok i changed it to revalidatePath("/")
but still the same result
i see the post flicker when i press ❤️ but no ui changes
well, i think getPost doesn't need to revalidate at all?
Pacific sand lanceOP
i commented it out but stillt he saem result,
when i press ❤️ the post flickers but i dont see any updates
what function is called when you press ❤️
Pacific sand lanceOP
likeComment
and how you fetch the data of like?
Pacific sand lanceOP
export async function likeComment(commentId: number, postId: number) {
  try {
    const userId = await getUserId();

    const userLike = await prisma.commentLike.findMany({
      where: {
        authorId: userId,
        commentId: commentId,
        postId: postId,
      },
    });

    if (userLike.length > 0) {
      const deletedLike = await prisma.commentLike.delete({
        where: {
          id: userLike[0].id,
        },
      });
    } else {
      const likeData = {
        authorId: userId,
        commentId: commentId,
        createdAt: new Date(),
        postId: postId,
      };

      const createdLike = await prisma.commentLike.create({
        data: likeData,
      });
    }

    revalidateTag('/');
  } catch (error) {
    return { message: `Unable to like comment` };
  }
}
revalidateTag('/')
tag should be a tag, not a path
you need to tag the fetch comment function then use revalidateTag with the tag
Pacific sand lanceOP
i dont have a getComment function persay i just use getPosts and get the comments from there it is part of the object
export async function getPosts(page: number = 1) {
  try {
/* gets posts from offset*/
   ...stuff
    revalidateTag('/');
    return {
      timelinePosts,
      otherTimeLinePosts,
      timelinePostsCount,
      otherTimelinePostsCount,
      userId,
    };
  } catch (error) {
    return { message: 'Unable to fetch posts' };
  }
}
use revalidateTag('posts') if getPosts is taged with 'posts'
and see if the ui change
Pacific sand lanceOP
no ui change it seems
export async function getPosts(page: number = 1) {
  try {
   stuff
    revalidateTag('posts');
    return {
      timelinePosts,
      otherTimeLinePosts,
      timelinePostsCount,
      otherTimelinePostsCount,
      userId
,
you don't need revalidateTag('posts') inside the getPosts function
Pacific sand lanceOP
ok
where is this page on github?
i think you should look at this version
all the ones after i started added client state to fix the issue
the commit is called
styling changes
after this point i started messing everyting up i think
const [userId, post] = await Promise.all([getUserId(), getPost(postId)]);
the data is comming from these, right?
Pacific sand lanceOP
yes
i meanno
no
my bad
for the discover page which si the home route
if you press on the top left icon
its using /discover
/post is just for individual posts with slugs and stuff
its no problem if i use revalidatePath(/) for /post
the problem is /discover and /for-you
const getCachedPost = (postId: number) => {
  return unstable_cache(id => getPost(id), ['post'], {tags: [`post-${postId}`]})(postId)
}

export default async function Page({ params }: { params: { slug: string } }) {
  try {
    if (params.slug === null) throw new Error();

    const postId = Number(params.slug);

    const [userId, post] = await Promise.all([getUserId(), getCachedPost(postId)]);

    if (post === null || post === undefined || userId === undefined) return <NoDataFound />;

    await new Promise(resolve => setTimeout(resolve, 500));

    return (
      <Box mt={10} minH={'100vh'} mb={10}>
        <Post post={post} userId={userId} />
      </Box>
    );
  } catch (error) {
    return <NoDataFound />;
  }
}
export async function likeComment(commentId: number, postId: number) {
  try {
    const userId = await getUserId();

    const userLike = await prisma.commentLike.findMany({
      where: {
        authorId: userId,
        commentId: commentId,
        postId: postId,
      },
    });

    if (userLike.length > 0) {
      const deletedLike = await prisma.commentLike.delete({
        where: {
          id: userLike[0].id,
        },
      });
    } else {
      const likeData = {
        authorId: userId,
        commentId: commentId,
        createdAt: new Date(),
        postId: postId,
      };

      const createdLike = await prisma.commentLike.create({
        data: likeData,
      });
    }

    revalidateTag(`post-${postId}`);
  } catch (error) {
    return { message: `Unable to like comment` };
  }
}
Pacific sand lanceOP
ok
1. tag the data function with unstable_cache
2. in the server action, use revalidateTag with the tag in the data function
Pacific sand lanceOP
ok
try it, and like a comment to see if the ui change
Pacific sand lanceOP
no it does not for some reason
i see the flickerish behavior thing but the ui doesnt change when i press ❤️
ah i know the problem. its because revalidateTag only invalidate the cache
Pacific sand lanceOP
ok
which action has revalidatePath("/") ?
Pacific sand lanceOP
one sec
createComment, deleteComment,
and
i think thats it
atleast from what i ssee
so if I make a comment, the infinity scroll will be scrolled to top?
Pacific sand lanceOP
yes
well not to the top it will just reload the last 5 posts, using the current state posts..length
it doesn't scroll to top for me on https://odin-book-sand.vercel.app/discover
Pacific sand lanceOP
oh
that is because this is a newer deployment that has client state
ah ok
Pacific sand lanceOP
i shold have kept the old oen i think
mayb ei can get it back i think
i will get it back
yes state is better for this
I thought you have data in state and it still reset the scroll position with revalidatePath
Pacific sand lanceOP
oh ok
no it was cause before when i would like a comment for example, it would refetch all of the data for the post and then it would reload the infinite scroll
i would see the loading spinner
and it looks kidna weird so
i thought maybe i should add state
but then i realized that all components that need to refetch data would need state like comment component and post component
there is a bug where openign the sidebar will trigger the infinite scrol too
cause
i havent fixed that yet
where is the sidebar?
@Ray where is the sidebar?
Pacific sand lanceOP
it is the message icon on the top right
I think you just need to put the posts in state to keep the scroll position
Pacific sand lanceOP
export default function LoadMoreDiscover({
  initialPosts,
  otherTimelinePostsCount,
}: LoadMoreDiscoverProps) {
  const [posts, setPosts] = useState<PostWithAuthor[]>(initialPosts);
  const [totalPostCount, setTotalPostCount] = useState(otherTimelinePostsCount);
  const [isLoading, setIsLoading] = useState(false);
  const [initialLoading, setInitialLoading] = useState(true);
  const { ref, inView } = useInView();
  const { data: session } = useSession();
yeah i have them in state, here i set the initial state to the initial posts.
why you need revalidatePath('/'); in getFriendsSideBar?
Pacific sand lanceOP
im not sure tbh, making the notifications show hen i wanted too was kinda finicky and i dont know anything about web sockets so i just kept using revalidate path where i wanted dat ato frefresh
i will try removing it and see
ok removing it fixes it so it doesnt trigger infinite scroll to reload anymore
but
i think now notifications may be broken i have to test it i think
not showing notification?
Pacific sand lanceOP
oh
nvm i think its workign fine without it i think
without the revalidatePath("/")
i mean
one sec
ok i think its workign good actually
without the revalidatePath("/")
i dont think that i need it
cool
Pacific sand lanceOP
ok
@Ray cool
Pacific sand lanceOP
i think that my problems are fixed then i think but what do you think about my code though
i dont think anyone has ever looked at it before i think this is the first time
firs titme using next too
I think you should separate the data loading function and server action
the data function itself shouldn't use revalidatePath or revalidateTag
Pacific sand lanceOP
oh
Pacific sand lanceOP
ok so the genreal rule is to seperate server actions that fetch data and then server actions that perform creates, updates, deletes?
cause i always jsut ptu eveyrting in the same actions.ts
and revalidatePath should only be used in queries.ts
revalidatePath should only use in action.ts
and you could use not-found.tsx
Pacific sand lanceOP
ok, what is the reason for seperatering the server actions like that?
then you can do
import { notFound } from 'next/navigation'

if (userData === undefined || userId === undefined) notFound();
instead of try catch the whole page component
Pacific sand lanceOP
oh
oh
ok
@Pacific sand lance ok, what is the reason for seperatering the server actions like that?
because 'use server' is for server action
Pacific sand lanceOP
yeah for your blog, you have actions.ts and queries.ts, they are both full of server actions. so they both have 'use server' at the top
oh really lol
Pacific sand lanceOP
yes
its not my blog btw😆
Pacific sand lanceOP
oh ok
https://github.com/vercel/next.js/pull/59602
he made change on the doc of nextjs too
Pacific sand lanceOP
so they have actions.ts and data.ts. in actions.ts they are both querying the database, so why are they seperated, i am just tyring to understand this part
action.ts is mostly for data mutation
server action should be the user trigger a event on their browser then it make a post request to the server
not the function render the page
Pacific sand lanceOP
ok so action.ts is like for post requests and stuff, but to render a page you would use data.ts
it can be querying the database, like what you did with infinity scroll
yep
Pacific sand lanceOP
ok
i had no idea
i just put eveyrthing in the same file
became so long
😆
well it fine, personal preference
Pacific sand lanceOP
ok thanks for the links i will save them. do you haev any other advice for me i think we are about doen here right?
no other things look good
Pacific sand lanceOP
ok thx this was the firs titme someone actuall ylooked at my code i think so i appreciate it