Diego
Diego

Reputation: 681

Swr's cache updates but UI doesn't for no reason - swr hook Next.js ( with typescript )

I'm doing a facebook clone, and everytime i press like's button, i want to see the change immediately, that's something that swr provides, but, it only updates after 4-8 seconds :/

What i tried to do is the following: when i click like's button, i first mutate the cache that swr provides, then i make the call to the API, then revalidate data to see if everything is right with the data, actually i console log the cache and it updates immediately, but it the UI doesn't and i don't know why

Let me give sou some context with my code

This is how my publication looks like ( inside pub it's the likes property )


export type theLikes = {
  identifier: string;
};

export type theComment = {
  _id?: string;
  body: string;
  name: string;
  perfil?: string;
  identifier: string;
  createdAt: string;
  likesComments?: theLikes[];
};

export interface Ipublication {
  _id?: string;
  body: string;

  photo: string;

  creator: {
    name: string;
    perfil?: string;
    identifier: string;
  };

  likes?: theLikes[];

  comments?: theComment[];

  createdAt: string;
}

export type thePublication = {
  data: Ipublication[];
};

This is where i'm asking for all publications with getStaticProps

const PublicationsHome = ({ data: allPubs }) => {
  // All pubs

  const { data: Publications }: thePublication = useSWR(
    `${process.env.URL}/api/publication`,
    {
      initialData: allPubs,
      revalidateOnFocus: false
    }
  );


  return (
    <>
      {Publications ? (
        <>
          <PublicationsHomeHero>
            {/* Create pub */}
            <CreatePubs />
            {/* Show pub */}
            {Publications.map(publication => {
              return <Pubs key={publication._id} publication={publication} />;
            })}
          </PublicationsHomeHero>
        </>
      ) : (
        <div></div>
      )}
    </>
  );
};

export const getStaticProps: GetStaticProps = async () => {
  const { data } = await axios.get(`${process.env.URL}/api/publication`);

  return {
    props: data
  };
};

export default PublicationsHome;

This is where the like button is ( focus on LikePub, there is where the logic is )

The conditional is simple, if user already liked a pub, cut the like, otherwise, like the pub


interface IlikesCommentsProps {
  publication: Ipublication;
}

const LikesComments: React.FC<IlikesCommentsProps> = ({ publication }) => {

const LikePub = async (): Promise<void> => {
    try {
      if (publication.likes.find(f => f.identifier === userAuth.user.id)) {
        mutate(
          `${process.env.URL}/api/publication`,
          (allPublications: Ipublication[]) => {
            const currentPub = allPublications.find(f => f === publication);

            const deleteLike = currentPub.likes.findIndex(
              f => f.identifier === userAuth.user.id
            );

            currentPub.likes.splice(deleteLike, 1);

            const updatePub = allPublications.map(pub =>
              pub._id === currentPub._id ? currentPub : pub
            );

            return updatePub;
          },
          false
        );
      } else {
        mutate(
          `${process.env.URL}/api/publication`,
          (allPublications: Ipublication[]) => {
            const currentPub = allPublications.find(f => f === publication);
            currentPub.likes.push({ identifier: userAuth.user.id });

            const updatePub = allPublications.map(pub =>
              pub._id === currentPub._id ? currentPub : pub
            );

            return updatePub;
          },
          false
        );
      }
      console.log(publication.likes);
      await like({ identifier: userAuth.user.id }, publication._id);
      mutate(`${process.env.URL}/api/publication`);
    } catch (err) {
      if (err) {
        mutate(`${process.env.URL}/api/publication`);
      }
    }
  };

return (

      <motion.div
          onClick={LikePub}
          variants={Actions}
          whileHover="whileHover"
          whileTap="whileTap"
        >
              <motion.span>
                <LikeIcon colorIcon="rgb(32, 120, 244)" />
              </motion.span>
              <LikeCommentText
                separation="0.2rem"
                colorText="rgb(32, 120, 244)"
              >
                Me gusta
              </LikeCommentText>
        </motion.div>

)
}

As you can see as well, i console.log publication's likes, and this is what happened

enter image description here

The identifier is added to the cache, meaning that user liked the pub, but the UI doesn't update, it takes 4 - 7 seconds to update, probably even more, the same thing happened with removing the like, look at this

enter image description here

Cut the like, but, the UI doesn't update

I'm desperate, i've tried everything, i've been trying to fix this for almost a week, but found nothing, what am i doing wrong, is this a bug ?

Upvotes: 0

Views: 1332

Answers (1)

aleksxor
aleksxor

Reputation: 8370

I believe the problem is you're directly mutating (in the javascript not swr sense) swr's data that is completely invisible to swr. And only when response is returned from the API your state is updated and that finally triggers swr's observers.


Here you may notice that currentPub.likes is an array (reference) inside currentPub object. You're directly mutating it (with splice) and then insert the same reference back into allPublications object. From swr's perspective the likes array didn't change. It still holds the same reference as before the mutation:

(allPublications: Ipublication[]) => {
    const currentPub = allPublications.find(f => f === publication);

    const deleteLike = currentPub.likes.findIndex(
        f => f.identifier === userAuth.user.id
    );

    currentPub.likes.splice(deleteLike, 1);
    const updatePub = allPublications.map(pub =>
        pub._id === currentPub._id ? currentPub : pub
    );

    return updatePub;
}

Snippet to illustrate the behavior:

const allPublications = [{ attr: 'attr', likes: [1, 2, 3] }]
const currentPub = allPublications[0]
currentPub.likes.splice(1, 1)
const updatePub = allPublications.map((pub, idx) => idx === 0 ? currentPub :  pub)

console.log(updatePub[0] === allPublications[0]) // true
console.log(updatePub[0].likes === allPublications[0].likes) // true. the reference stays the same
console.log(updatePub[0]) // while the object has changed

You should rewrite it to exlude direct mutation and always return changed references for changed objects/arrays. Something like:

(allPublications: Ipublication[]) => {
    const currentPub = allPublications.find(f => f === publication);

    const likes = currentPub.likes.filter( // filter creates new array (reference)
        f => f.identifier !== userAuth.user.id
    );
    const updatePub = allPublications.map(pub => // map creates new array
        pub._id === currentPub._id ? { ...currentPub, likes } : pub // {} creates new object (reference)
    );

    return updatePub;
}

const allPublications = [{ attr: 'attr', likes: [1, 2, 3] }]
const currentPub = allPublications[0]

const likes = currentPub.likes.filter((el) => el !== 2)
const updatePub = allPublications.map((pub, idx) => 
  idx === 0 ? { ...currentPub, likes } :  pub
)

console.log(updatePub[0] === allPublications[0]) // false
console.log(updatePub[0].likes === allPublications[0].likes) // false
console.log(updatePub[0])

And do the same for else branch mutation function.

Upvotes: 1

Related Questions