Avoiding React Race condition with AbortController not working

Viewed 712

i am trying to mimic React useEffect race condition and handle that with AbortController. I can never hit the catch block ( i guess ) because the setTimeOut is called post the fetch request. My question is how can i rewrite this code to put fetch inside setTimeout and still be able to use AbortController to cancel the request?

import './App.css';
import {useState,useEffect} from 'react'

function App() {

  const [country, setCountry] = useState('N.A.')
  const [capital, setCapital] = useState('')
 
  
useEffect(() => {      

  const ctrl = new AbortController(); 
  const load = async() =>{
     try
       {
         //debugger
          const response = await fetch(`https://restcountries.eu/rest/v2/capital/${capital}`,
          {signal:ctrl.signal})
          const jsonObj = await response.json()
          setTimeout( ()=> {setCountry(jsonObj[0].name)} , Math.random()*10000)    
       }
    catch(err)
    {
      console.log(err)
    }  
 }
  load();
  
  return () =>{              
              ctrl.abort() 
            };

}, [capital])

  
  return (
    <div>
       <button  onClick={()=>setCapital("Berlin")}  >Berlin</button>      
       <button  onClick={()=>setCapital("Paris")}  >Paris</button>      
       <button  onClick={()=>setCapital("Madrid")}  >Madrid</button>   
       <div> 
         {country}
      </div>  
    </div>
  );
}

export default App;
1 Answers

Hmm... just put that function inside setTimeout calling and don't forget to clean up the timer on unmount (Demo).

import React, { useState, useEffect } from "react";

export default function TestComponent(props) {
  const [country, setCountry] = useState("N.A.");
  const [capital, setCapital] = useState("");
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    const ctrl = new AbortController();
    const timer = setTimeout(async () => {
      try {
        if (!capital) {
          return;
        }
        const response = await fetch(
          `https://restcountries.eu/rest/v2/capital/${capital}`,
          { signal: ctrl.signal }
        );
        // if (!isMounted) return; // can be omitted here
        const jsonObj = await response.json();

        isMounted && setCountry(jsonObj[0].name);
      } catch (err) {
        console.log(err);
        isMounted && setError(err);
      }
    }, Math.random() * 10000);

    return () => {
      clearTimeout(timer);
      isMounted = false;
      ctrl.abort();
    };
  }, [capital]);

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <button onClick={() => setCapital("Berlin")}>Berlin</button>
      <button onClick={() => setCapital("Paris")}>Paris</button>
      <button onClick={() => setCapital("Madrid")}>Madrid</button>
      <div>Country: {error ? <b>{error.toString()}</b> : country}</div>
    </div>
  );
}

Or you can do the same with custom libs (Demo):

import React, { useState } from "react";
import { useAsyncEffect, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CPromise, CanceledError } from "c-promise2";
import cpFetch from "cp-fetch";

export default function TestComponent(props) {
  const [country, setCountry] = useState("N.A.");
  const [capital, setCapital] = useState("");
  const [error, setError] = useState(null);

  const cancel = useAsyncEffect(
    function* () {
      setError(null);
      if (!capital) {
        return;
      }
      yield CPromise.delay(Math.random() * 10000);
      try {
        const response = yield cpFetch(
          `https://restcountries.eu/rest/v2/capital/${capital}`
        ).timeout(props.timeout);

        const jsonObj = yield response.json(); 
        setCountry(jsonObj[0].name);
      } catch (err) {
        CanceledError.rethrow(err, E_REASON_UNMOUNTED);
        console.log(err);
        setError(err);
      }
    },
    [capital]
  );

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <button onClick={() => setCapital("Berlin")}>Berlin</button>
      <button onClick={() => setCapital("Paris")}>Paris</button>
      <button onClick={() => setCapital("Madrid")}>Madrid</button>
      <button onClick={cancel}>Cancel request</button>
      <div>Country: {error ? <b>{error.toString()}</b> : country}</div>
    </div>
  );
}
Related