skip to Main Content

I have a React component with 3 buttons representing 3 choices the user can make.
If the user clicks a button and then another button I cancel the previous HTTP request using AbortController.signal.abort(), it is really incredibly simple.

The thing is, the method has a loading state in its try {} catch {} finally {} block.
When I cancel the first promise the http request is really cancelled but the second promise starts to run before the first promise reaches its finally scope.. making the loader -> false before the second promise finished.

  1. first click -> first promise runs
  2. another click -> second promise runs
  3. first HTTP is cancelled
  4. new request starts
  5. finally {} block of the first function invoked/runs
  6. finally {} block of the second function invoked/runs

I tried doing it in 2 ways:

First Way


export default function SingleTesterQuestion({
  question,
}: {
  question: SomeType;
}) {
  const controller = useRef(new AbortController());
  const [loading, setLoading] = useState(false);

  const updateQuestionInServerForSpecificId = useCallback(
    async (choice: string) => {
      try {
        // if same question with same id got clicked on
        // different buttons (fast) abort the previous request
        controller.current.abort();
        controller.current = new AbortController();
        setLoading(true);
        await httpMethods.answerSimulationTestQuestion(
          question.id,
          choice,
          controller.current.signal
        );
        // update some store
      } catch (error) {
        //
      } finally {
        setLoading(false);
      }
    },
    [question.id]
  );
  return (
    <Box>
      <Choices updateQuestionInServerForSpecificId={updateQuestionInServerForSpecificId} />
    </Box>
  );
}

Second Way (trying to add some fuction in the middle that uses signal.onabort())


export default function SingleTesterQuestion({
  question,
}: {
  question: SomeType;
}) {
 const controller = useRef<null | AbortController>(null);
  const [loading, setLoading] = useState(false);

  const req = async (choice: string) => {
    try {
      // if same question with same id got clicked on
      // different buttons (fast) abort the previous request
      controller.current = new AbortController();
      setLoading(true);
      await httpMethods.answerSimulationTestQuestion(
        question.id,
        choice,
        controller.current.signal
      );
      // update some store
    } catch (error) {
      //
    } finally {
      controller.current = null;
      setLoading(false);
    }
  };

  const updateQuestionInServerForSpecificId = async (
    choice: string
  ) => {
    if (!controller.current) {
      // no one has yet to create an AbortController
      // make the request here
      await req(choice);
      return;
    }

    // there is already an AbortController out there
    controller.current.signal.onabort = async (x) => {
      await req(choice);
    };

    controller.current.abort();
  };
  return (
    <Box>
      <Choices updateQuestionInServerForSpecificId={updateQuestionInServerForSpecificId} />
    </Box>
  );
}

That’s the method:

  async answerSimulationTestQuestion(
    questionId: string,
    choice: string,
    signal: GenericAbortSignal
  ): Promise<boolean> {
    const { data } = await axios.put<boolean>(
      `url...`,
      { choice },
      { params: { questionId }, signal }
    );
    return data;
  }

Is there anyway to solve this? Why is this happening?

2

Answers


  1. I think this will prevent the state change:

            finally {
            if (!controller.current.signal.aborted) 
               setLoading(false);
            }
    
    Login or Signup to reply.
  2. The AbortController.abort() will trigger the catch block. I’d check there if the error was caused by an 'AbortError' and only set the loading state to false whenever the the error is caused by something different.

    The setLoading(false) in the finally block can be moved to after the await line, for you only want it to do that on a successful request, or an error caused other than by your AbortController.

    const controller = useRef<null | AbortController>(null);
    const [loading, setLoading] = useState(false);
    
    const updateQuestionInServerForSpecificId = useCallback(
      async (choice: string) => {
        try {
          if (controller.current !== null) {
            controller.current.abort();
          }
    
          controller.current = new AbortController();
          setLoading(true);
    
          await httpMethods.answerSimulationTestQuestion(
            question.id,
            choice,
            controller.current.signal
          );
    
          controller.current = null;
          setLoading(false);
        } catch (error) {
          if (error.name !== 'AbortError') {
            setLoading(false);
          }
        }
      },
      [question.id]
    );
    

    Let me know if this works for I haven’t been able to test it.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search