Search code examples
reactjstypescriptvitereact-selectreact-typescript

How to avoid race condition when fetching APIs that depends on each other in React?


I'm trying to update a select with the react-select custom component for React, due to the race condition that is happening when fetching data, I'm not able to populate the selected value for it. I've looked several SO questions on how to handle fetch calls in useEffect and how to handle them and everything but nothing has worked... Not sure if this is related to a race condition or promises in the fetch API that I'm not handling properly.

AlergiaForm.component.tsx

import Select, { SingleValue} from "react-select";
import {useState, useEffect} from "react";
interface IOption {
  value: string | null;
  label: string;
  icon?: string;
}

interface IProduct { 
  id: number,
  title: string
}

const AlergiaForm = () => {
  const[products, setProducts] = useState(Array<IProduct>);
  const[product, setProduct] = useState<SingleValue<IOption> | null>(null);
  const[isLoading, setLoading] = useState(false);
  const[value, setValue] = useState<string | null | undefined>("");

  //Mapper
  const mapper = ():IOption[] => {
    return products.map(item => {
      return { value: item.id, label: item.title};
    }) as unknown as IOption[];
  }

   useEffect(() => {
    //Get product, like an edit
    const getProduct = () => {
      setLoading(true);
      fetch('https://dummyjson.com/products/6')
      .then(res => res.json())
      .then((data) => {
          //We get the products dropdown values and we are waiting for it... is not even async and I'm waiting on the promise I think?
          getProducts();

          setLoading(false);
          //Logic to "set" the current value, but since it has a race condition, it's not picking it up
          //Works when a hot module refresh happens in Vite because the context is updated already... but not on page "load" or react-router-dom navigation
          const product = data as IProduct;
          const {id} = {...product};
          const selectedProduct = products.find(i => i.id === id) as IProduct;
          const ioption = { value: selectedProduct?.id.toString(), label: selectedProduct?.title } as SingleValue<IOption>;
          setProduct(ioption);
      });
    }

    const getProducts = () => {
      fetch('https://dummyjson.com/products')
      .then(res => res.json())
      .then((data) => {
        const products = data.products as IProduct[];
        setProducts(products);
      });
    }             

    getProduct();

   }, []);

   const handleDropDownChange = (selectedOption: SingleValue<IOption>):void => {
    const { value } = {...selectedOption};
    setValue(value);
}

  return(
    <>
      <Select options={mapper()} value={isLoading ? null : product} onChange={handleDropDownChange} />
      <p>Value selected: {product?.value ?? value}</p>
      <p>Is Loading: {isLoading ? "Loading..." : "Done"}</p>
    </>
  )  
}

  export default AlergiaForm;

App.tsx

import AlergiaForm from './AlergiaForm.component'

function App() {
  return (
    <main>
      <AlergiaForm />
    </main>
  )
}

export default App;

Minimal example with the same code: Example

You will see it does not update the selected value when the product is retrieved, and that's probably because the promise is not done when trying to set the product, but not sure how to avoid that even with waiting for the actual response to get the data and set the values correctly.

Other thing to mention is that I'm using Vite, everytime it reloads the page on a save file the value is selected, so I know the code is working, it's just I guess the issue is the race condition here.

Articles read: https://maxrozen.com/fetching-data-react-with-useeffect
https://overreacted.io/a-complete-guide-to-useeffect/


Solution

  • The product id and the product list is what's needed to fetch, so that should be your states. And since the fetching of those are unrelated, they should be separate useEffects. Once both are fetched, you can find the product. Since the product is completely dependent on two states, it shouldn't be it's own state.

    // output from first useEffect
    const [productId, setProductId] = useState<number>();
    // output from second useEffect
    const [products, setProducts] = useState<Array<IProduct>>([]);
    
    useEffect(() => {
      fetch("https://dummyjson.com/products/6")
        .then((res) => res.json())
        .then((data: IProduct) => {
          setProductId(data.id);
        });
    }, []);
    
    useEffect(() => {
      fetch("https://dummyjson.com/products")
        .then((res) => res.json())
        .then((data: { products: IProducts[] }) => {
          setProducts(products);
        });
    }, []);
    
    // try to find the fetched product from the list of products.
    // `undefined` as long as `products` or `productId` hasn't been fetched.
    const selectedProduct = products.find((i) => i.id === productId);
    // `product` is `undefined` as long as `selectedProduct` is undefined.
    const product: SingleValue<IOption> = selectedProduct && {
      value: selectedProduct.id.toString(),
      label: selectedProduct.title,
    };
    
    // we are loading if we don't have a product
    const isLoading = !product;