Next.js Discord

Discord Forum

CSR response not updating UI after ISR page load

Answered
Whiteleg shrimp posted this in #help-forum
Open in Discord
Whiteleg shrimpOP
I have a recipe detail page loaded via useStaticProps with trpc prefetch. Then in my component I am also using trpc to fetch the data. The reason I am doing both is because I want the page to load quickly since it is not a page updated often, but when fetched on the server at build time, there is no session to determine if the user logged in has favorited the recipe. When the recipe loads on the client, the props are passed down to sub components in my JSX, but the UI isnt updating when recipe.isFavorited is passed into my component.

Here is the line causing issues: const [isLiked, setLiked] = useState(isInitiallyLiked); where isInitiallyLiked is a prop (using recipe.isFavorited

If I add the following code it works:

useEffect(() => {
    setLiked(isInitiallyLiked);
  }, [isInitiallyLiked]);


Why isnt the state var being re-initialized and rerendering when isInitiallyLiked is passed in with the updated CSR value? I have confirmed via logs that isInitiallyLiked updates, but isLiked does not.
Answered by joulev
Then useEffect is what I would do here
View full answer

21 Replies

can you show more code?
from this there isn't anything wrong that i can think of
@joulev can you show more code?
Whiteleg shrimpOP
Absolutely. I didnt want to spam the chat with the code. Here is my full ISR page code:

export default function Recipe({ id }: { id: string }) {
  const { data, isLoading, refetch } = api.recipes.getDetails.useQuery({
    id: id,
  });

  const recipe: Recipe | undefined | null = data?.recipe;

  return (
    <>
       {/* removing extra JSX for demo */}
       <LikeButton
          isInitiallyLiked={recipe.isFavorited}
        />
    </>
  );
}

export async function getStaticPaths({}: GetStaticPropsContext) {
  const helpers = createServerSideHelpers({
    router: appRouter,
    ctx: { session: null, prisma },
    transformer: superjson,
  });

  const recipes = await helpers.recipes.getAll.fetch({ max: 10 });
  const paths = recipes.map((recipe) => ({ params: { id: recipe.id } }));

  return {
    paths,
    fallback: "blocking",
  };
}

export async function getStaticProps(
  context: GetStaticPropsContext<{ id: string }>,
) {
  const helpers = createServerSideHelpers({
    router: appRouter,
    ctx: { session: null, prisma },
    transformer: superjson,
  });
  const id = context.params?.id ?? "";
  await helpers.recipes.getDetails.prefetch({ id });

  return {
    props: {
      trpcState: helpers.dehydrate(),
      id,
    },
    revalidate: 10,
  };
}


And here is my like button component:

const LikeButton = ({
  isDisabled = false,
  isInitiallyLiked,
  onClick,
}: Props) => {
  const [isLiked, setLiked] = useState(isInitiallyLiked);

  // this is the code that makes this work
  // useEffect(() => {
  //   setLiked(isInitiallyLiked);
  // }, [isInitiallyLiked]);

  const handleClick = (event: React.FormEvent) => {
    event.preventDefault();
    setLiked((prev) => {
      onClick?.(!prev);
      return !prev;
    });
  };

  return (
    {/* removing JSX for demo */}
    <IoHeart
      className={`h-6 w-6 fill-current ${
        isLiked ? "text-red-500" : "text-gray-300"
      }`}
    />
  );
};

export default LikeButton;
@Whiteleg shrimp Absolutely. I didnt want to spam the chat with the code. Here is my full ISR page code: export default function Recipe({ id }: { id: string }) { const { data, isLoading, refetch } = api.recipes.getDetails.useQuery({ id: id, }); const recipe: Recipe | undefined | null = data?.recipe; return ( <> {/* removing extra JSX for demo */} <LikeButton isInitiallyLiked={recipe.isFavorited} /> </> ); } export async function getStaticPaths({}: GetStaticPropsContext) { const helpers = createServerSideHelpers({ router: appRouter, ctx: { session: null, prisma }, transformer: superjson, }); const recipes = await helpers.recipes.getAll.fetch({ max: 10 }); const paths = recipes.map((recipe) => ({ params: { id: recipe.id } })); return { paths, fallback: "blocking", }; } export async function getStaticProps( context: GetStaticPropsContext<{ id: string }>, ) { const helpers = createServerSideHelpers({ router: appRouter, ctx: { session: null, prisma }, transformer: superjson, }); const id = context.params?.id ?? ""; await helpers.recipes.getDetails.prefetch({ id }); return { props: { trpcState: helpers.dehydrate(), id, }, revalidate: 10, }; } And here is my like button component: const LikeButton = ({ isDisabled = false, isInitiallyLiked, onClick, }: Props) => { const [isLiked, setLiked] = useState(isInitiallyLiked); // this is the code that makes this work // useEffect(() => { // setLiked(isInitiallyLiked); // }, [isInitiallyLiked]); const handleClick = (event: React.FormEvent) => { event.preventDefault(); setLiked((prev) => { onClick?.(!prev); return !prev; }); }; return ( {/* removing JSX for demo */} <IoHeart className={`h-6 w-6 fill-current ${ isLiked ? "text-red-500" : "text-gray-300" }`} /> ); }; export default LikeButton;
if you put if (!data) return <div>Loading</div> after the useQuery call, does the loading div ever show up?
@joulev if you put `if (!data) return <div>Loading</div>` after the `useQuery` call, does the loading div ever show up?
Whiteleg shrimpOP
Unfortunately not at my computer for a few hours, but wouldn’t that defeat the purpose of ISR? Or are you just asking to see this as a test?
Yes just a test. Your code looks fine for me that’s why I asked this
@joulev Yes just a test. Your code looks fine for me that’s why I asked this
Whiteleg shrimpOP
Gotcha. Just confirmed that never gets rendered. And I know data is populated because when I add the useEffect() in the <LikeButton /> component, it updates the UI of the heart icon to red (just takes like 500ms)
Whiteleg shrimpOP
And the logs log true on my front end render for recipe.isFavorited
Hmm so that basically proves the code is working, why is the LikeButton not working…

If you console.log(isInitiallyLiked) inside the LikeButton, does it initially become false and only become true after the first render or something?
@joulev Hmm so that basically proves the code is working, why is the LikeButton not working… If you console.log(isInitiallyLiked) inside the LikeButton, does it initially become false and only become true after the first render or something?
Whiteleg shrimpOP
Sorry I thought I included that in the initial message. If I add these logs to the like button:

const LikeButton = ({
  isDisabled = false,
  isInitiallyLiked,
  onClick,
}: Props) => {
  console.log(isInitiallyLiked);
  const [isLiked, setLiked] = useState(isInitiallyLiked);
  console.log(isLiked);
  // rest of component
}


I get this output:
false
false
true
false


So it looks like initial render both are false (which makes sense based on the ISR state, and then on the rerender, isInitiallyLiked is set to true, but isLiked isnt...no idea what is going on.
@joulev You need to ensure isInitiallyLiked is truthy at the first render
Whiteleg shrimpOP
Sorry, I dont think I followed this. Are you saying i need to change the isLiked var to this?:

const [isLiked, setLiked] = useState(!!isInitiallyLiked);

I was under the impression the state would update as the provided prop updates...
Is there a way around this without the useEffect()? Its not the end of the world to add it..just didn't think it would be necessary
@Whiteleg shrimp Sorry, I dont think I followed this. Are you saying i need to change the `isLiked` var to this?: `const [isLiked, setLiked] = useState(!!isInitiallyLiked);` I was under the impression the state would update as the provided prop updates...
https://react.dev/reference/react/useState#parameters
initialState: The value you want the state to be initially. It can be a value of any type, but there is a special behavior for functions. This argument is ignored after the initial render.

This is how useState has always worked
@Whiteleg shrimp Is there a way around this without the `useEffect()`? Its not the end of the world to add it..just didn't think it would be necessary
If isInitiallyLiked matters after the first render, should you even use useState here? I would just ditch useState and use isInitiallyLiked in isLiked’s place
@joulev If isInitiallyLiked matters after the first render, should you even use useState here? I would just ditch useState and use isInitiallyLiked in isLiked’s place
Whiteleg shrimpOP
That is a very good point, thank you! I guess useState would allow me to optimistically update the UI vs waiting for the req to finish to propogate the new liked state down, but I suppose that is the tradeoff. Thank you for the suggestion
Answer
@joulev Then useEffect is what I would do here
Whiteleg shrimpOP
Yup that’s what I’m going with. Thanks again for your help!