skip to Main Content

When the first request is sent, isVoting will be set to "true". In this case, until isVoting is set to "false", another request should not be allowed to be sent. But somehow isVoting is still "false" when i try to click downvote multiple times fast. And downvote request is being sent two times, which is incorrect. How can I solve this?

"use client";
import { downvoteAnswer } from "@/lib/actions/answer.action";
import Image from "next/image";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";

interface Props {
  type: string;
  itemId: string;
  userId: string;
  upvotes: number;
  hasupVoted: boolean;
  downvotes: number;
  hasdownVoted: boolean;
  authorId: string;
  hasSaved?: boolean;
}

const Votes = ({
  type,
  itemId,
  userId,
  upvotes,
  hasupVoted,
  authorId,
  downvotes,
  hasdownVoted,
  hasSaved,
}: Props) => {
  const [isVoting, setIsVoting] = useState(false);
  const pathname = usePathname();
  const router = useRouter();

  const handleVote = async (action: string) => {
    console.log("isVoting:", isVoting);
    if (isVoting) {
      console.log("Request denied");
      return;
    }

      setIsVoting(true);
      await downvoteAnswer({
        answerId: JSON.parse(itemId),
        userId: JSON.parse(userId),
        hasupVoted,
        hasdownVoted,
        path: pathname,
      });
      setIsVoting(false);
    }
  };


  return (
    <div className="flex gap-5">
        <div className="flex-center gap-1.5">
          <Image
            src={
              hasdownVoted
                ? "/assets/icons/downvoted.svg"
                : "/assets/icons/downvote.svg"
            }
            width={18}
            height={18}
            alt="downvote"
            className="cursor-pointer"
            onClick={() => handleVote("downvote")}
          />
        </div>
    </div>
  );
};

export default Votes;

4

Answers


  1. React states update are asynchronous, and there could be a delay in updating the isVoting state across renders.

    Just like @hairyhandkerchief23 said you should use a useRef() with a boolean.

    You could try it like this:

    const isVotingRef = useRef(false);
    

    const handleVote = async (action: string) => {
      console.log("isVoting:", isVotingRef.current); 
      if (isVotingRef.current) {  // Use ref's current value
        console.log("Request denied");
        return;
      }
    
      isVotingRef.current = true; // this prevents multiple calling
      try {
        await downvoteAnswer({
          answerId: JSON.parse(itemId),
          userId: JSON.parse(userId),
          hasupVoted,
          hasdownVoted,
          path: pathname,
        });
      } catch (error) {
        console.error("Error in voting:", error);
      } finally {
        isVotingRef.current = false; // reset after finished
      }
    };
    

    Hope this helps

    Login or Signup to reply.
  2. Practically speaking, you should always disable a particular element if you want to avoid having multiple clicks on it.

    The simplest answer that comes to mind is simply wrapping up the image inside a button and disabling it when needed. A little something like:

    <button disabled={isVoting} onClick={() => handleVote("downvote")}>
        <Image
            src={
              hasdownVoted
                ? "/assets/icons/downvoted.svg"
                : "/assets/icons/downvote.svg"
            }
            width={18}
            height={18}
            alt="downvote"
            className="cursor-pointer"
          />
    </button>
    
    Login or Signup to reply.
  3. For this we can use useCallback and try catch

    useCallback Hook: The handleVote function is wrapped in useCallback to ensure it doesn’t get recreated on every render, which could lead to unnecessary re-renders and potential bugs.

    try-finally Block: The try-finally block is used to ensure that setIsVoting(false) is always called, even if the downvoteAnswer function throws an error. This ensures that the voting state is properly reset, allowing for future votes.

    "use client";
    import { downvoteAnswer } from "@/lib/actions/answer.action";
    import Image from "next/image";
    import { usePathname, useRouter } from "next/navigation";
    import { useState, useCallback } from "react";
    
    interface Props {
      type: string;
      itemId: string;
      userId: string;
      upvotes: number;
      hasupVoted: boolean;
      downvotes: number;
      hasdownVoted: boolean;
      authorId: string;
      hasSaved?: boolean;
    }
    
    const Votes = ({
      type,
      itemId,
      userId,
      upvotes,
      hasupVoted,
      authorId,
      downvotes,
      hasdownVoted,
      hasSaved,
    }: Props) => {
      const [isVoting, setIsVoting] = useState(false);
      const pathname = usePathname();
      const router = useRouter();
    
      const handleVote = useCallback(async (action: string) => {
        if (isVoting) {
          console.log("Request denied");
          return;
        }
    
        setIsVoting(true);
        try {
          await downvoteAnswer({
            answerId: JSON.parse(itemId),
            userId: JSON.parse(userId),
            hasupVoted,
            hasdownVoted,
            path: pathname,
          });
        } catch (error) {
          console.error("Vote request failed:", error);
        } finally {
          setIsVoting(false);
        }
      }, [isVoting, itemId, userId, hasupVoted, hasdownVoted, pathname]);
    
      return (
        <div className="flex gap-5">
          <div className="flex-center gap-1.5">
            <Image
              src={
                hasdownVoted
                  ? "/assets/icons/downvoted.svg"
                  : "/assets/icons/downvote.svg"
              }
              width={18}
              height={18}
              alt="downvote"
              className="cursor-pointer"
              onClick={() => handleVote("downvote")}
            />
          </div>
        </div>
      );
    };
    
    export default Votes;
    
    Login or Signup to reply.
  4. You define a ref not directly by a boolean value, but instead by an object:

    /* imports */
    const Votes = (...) => {
       const isVotingRef = useRef({ isVoting: false });
       const handleVote = async (action: string) => {
        console.log("isVoting:", isVotingRef.current.isVoting);
        if (isVotingRef.current.isVoting) {
          console.log("Request denied");
          return;
        }
    
          isVotingRef.current.isVoting = true;
          await downvoteAnswer({
            answerId: JSON.parse(itemId),
            userId: JSON.parse(userId),
            hasupVoted,
            hasdownVoted,
            path: pathname,
          });
          isVotingRef.current.isVoting = false;
        }
      };
      ...
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search