How to keep React state and db in sync with each other

I am making a like feature for a post where if user likes the post the counter increases on the ui and registers the like in db but sometimes the like counter goes into negative.
const PostPage: NextPage = ({}) => {
const router = useRouter();
const { slug } = router.query;
const postId = slug as string;
const { data: session, status } = useSession();
const [currentLike, setCurrentLike] = useState<boolean | undefined>(
undefined
);
const [currentStar, setCurrentStar] = useState<boolean | undefined>(
undefined
);
const { data: post } = api.post.getPostById.useQuery({
postId: postId,
});
const [likesAmt, setLikesAmt] = useState(post?.likes.length || 0);
console.log(post?.likes.length, likesAmt);
const [starsAmt, setStarsAmt] = useState(post?.stars.length ?? 0);
const { mutate: toggleLike } = api.post.toggleLike.useMutation({
onMutate: () => {
if (currentLike) {
setCurrentLike(undefined);
setLikesAmt((prev) => {
// if (prev) {
if (prev == 1) {
return 0;
}
return prev - 1;
// }
});
} else {
setCurrentLike(true);
setLikesAmt((prev) => {
// if (prev) {
return prev + 1;
// }
});
}
},
});

useEffect(() => {
if (post) {
post.likes.map((like) => {
if (like.userId === session?.user.id) {
setCurrentLike(true);
}
});
post.stars.map((star) => {
if (star.userId === session?.user.id) {
setCurrentStar(true);
}
});
}
}, [post, session?.user.id]);
const PostPage: NextPage = ({}) => {
const router = useRouter();
const { slug } = router.query;
const postId = slug as string;
const { data: session, status } = useSession();
const [currentLike, setCurrentLike] = useState<boolean | undefined>(
undefined
);
const [currentStar, setCurrentStar] = useState<boolean | undefined>(
undefined
);
const { data: post } = api.post.getPostById.useQuery({
postId: postId,
});
const [likesAmt, setLikesAmt] = useState(post?.likes.length || 0);
console.log(post?.likes.length, likesAmt);
const [starsAmt, setStarsAmt] = useState(post?.stars.length ?? 0);
const { mutate: toggleLike } = api.post.toggleLike.useMutation({
onMutate: () => {
if (currentLike) {
setCurrentLike(undefined);
setLikesAmt((prev) => {
// if (prev) {
if (prev == 1) {
return 0;
}
return prev - 1;
// }
});
} else {
setCurrentLike(true);
setLikesAmt((prev) => {
// if (prev) {
return prev + 1;
// }
});
}
},
});

useEffect(() => {
if (post) {
post.likes.map((like) => {
if (like.userId === session?.user.id) {
setCurrentLike(true);
}
});
post.stars.map((star) => {
if (star.userId === session?.user.id) {
setCurrentStar(true);
}
});
}
}, [post, session?.user.id]);
15 Replies
Mocha
Mocha10mo ago
This won't solve the bug, but you could
return Math.max(prev - 1, 0)
return Math.max(prev - 1, 0)
Also, I'm curious if you're returning all the like/star objects just to check for their userId?
aditya
aditya10mo ago
where do you mean?
Mocha
Mocha10mo ago
If you’re returning all likes just to map and check for user id, then you could just do something like
const isLiked = prisma.likes.findFirst({ where: { postId, userId } }) !== null
const isLiked = prisma.likes.findFirst({ where: { postId, userId } }) !== null
aditya
aditya10mo ago
i am doing this
const like = Boolean(
post.likes.find((like) => like.userId === session?.user.id)
);
const like = Boolean(
post.likes.find((like) => like.userId === session?.user.id)
);
as im querying the post data, likes, comments and stars in one single request
Mocha
Mocha10mo ago
Oh yes if the data is already there for other purposes from a single request, then yes. I would personally still use something to use a Prisma transaction or raw SQL to avoid reading & returning too much data. Something to worry about when you have many likes
aditya
aditya10mo ago
thanks for the tip will try to move to single queries after i get everything working properly
Apestein
Apestein10mo ago
Just don't use state at all, it's not necessary.
aditya
aditya10mo ago
how to do it then?
ItsBrumm
ItsBrumm10mo ago
What you could do is set the like/follow button into a loading state and when the like succeeds you can update the state to the expected state when refreshing the page you could then fetch it from the database
Ramsay
Ramsay10mo ago
You should never try to sync local state with server state. The bug you're running into is an example of why you shouldn't. All state should be derived from the query data. Remove the useStates, only show the query data, and update it with optimistic updates https://tanstack.com/query/v4/docs/react/guides/optimistic-updates
Optimistic Updates | TanStack Query Docs
When you optimistically update your state before performing a mutation, there is a chance that the mutation will fail. In most of these failure cases, you can just trigger a refetch for your optimistic queries to revert them to their true server state. In some circumstances though, refetching may not work correctly and the mutation error could ...
Apestein
Apestein10mo ago
I have an example with trpc here. Basically only use query data then invalidate(refetch) on successful mutation. You can use optimistic update with trpc as well which just wraps react query. Just an example btw, how I did it is probably not best practice. https://github.com/Apestein/chirp/blob/main/src/components/Post.tsx
aditya
aditya10mo ago
ahh i see thanks for this yeah ill do that, just run a count query because my schema has a likes table for the post likes update: everythings working as expected optimistic updates was the solution
aditya
aditya10mo ago
do these many queries work properly with huge data?
Mocha
Mocha10mo ago
Depends on what makes sense for your page. Are all queries supposed to load on initial page load? If so, I'd try to minimize the number of requests, at least to avoid latency issues. For example, if I'll always need to see whether a post is liked once it loads, I'd just include isLiked, isStarred, countComments in the post request (especially since it's really easy with tRPC's context.session). Other details may only be needed when the user interacts with the post, like seeing the actual comments and the name/photo of commenters, so they can be their own requests and wait until called. Optimistic updates are also awesome for comments. You may want to show the comment UI feedback without waiting for it to be on the database
aditya
aditya10mo ago
thanks for the tips 🤝 @Apestein hey i was trying to implement infinite scrolling and saw your implementation, but its not working properly. It just keeps on loading forever