abc
abc

Reputation: 125

Using useState of complex object not working as expected in ReactJS

I have a function component and I am declaring a useState for a complex object like this:

    const [order, setOrder] = useState<IMasterState>({
        DataInterface: null,
        ErrorMsg: "",
        IsRetrieving: true,
        RetrievingMsg: "Fetching your order status..."
    });

I now try to set the state of the order by calling setOrder in a useEffect like this:

    useEffect(() => {
        (async function() {
            let dh = new DataInterface("some string");
            let errMsg = "";
            
            // Get the sales order.
            try
            {
                await dh.FetchOrder();
            }
            catch(error: any)
            {
                errMsg = error;
            };
            
            setOrder(salesOrder => ({...salesOrder, IsRetrieving: false, ErrorMsg: errMsg, DataInterface: dh}));
        })();
    }, []);

As is, this seems to work fine. However, I have a setInterval object that changes the screen message while order.IsRetrieving is true:

    const [fetchCheckerCounter, setFetchCheckerCount] = useState<number>(0);

    const statusFetcherWatcher = setInterval(() => {
        if (order.IsRetrieving)
        {
            if (fetchCheckerCounter === 1)
            {
                setOrder(salesOrder => ({...salesOrder, RetrievingMsg: "This can take a few seconds..."}));
            }
            else if (fetchCheckerCounter === 2)
            {
                setOrder(salesOrder => ({...salesOrder, RetrievingMsg: "Almost there!.."}));
            }

            setFetchCheckerCount(fetchCheckerCounter + 1);
        }
        else
        {
            // Remove timer.
            clearInterval(statusFetcherWatcher);
        }
    }, 7000);

The issue is that order.IsRetrieving is always true for that code block, even though it does change to false, and my website changes to reflect that, even showing the data from dh.FetchOrder(). That means my timer goes on an infinite loop in the background.

So am I setting the state of order correctly? It's incredibly difficult to find a definite answer on the net, since all the answers are invariably about adding a new item to an array.

Upvotes: 2

Views: 727

Answers (1)

Drew Reese
Drew Reese

Reputation: 202605

Issues

  1. You are setting the interval as an unintentional side-effect in the function body.
  2. You have closed over the initial order.isRetreiving state value in the interval callback.

Solution

Use a mounting useEffect to start the interval and use a React ref to cache the state value when it updates so the current value can be accessed in asynchronous callbacks.

const [order, setOrder] = useState<IMasterState>({
  DataInterface: null,
  ErrorMsg: "",
  IsRetrieving: true,
  RetrievingMsg: "Fetching your order status..."
});

const orderRef = useRef(order);

useEffect(() => {
  orderRef.current = order;
}, [order]);

useEffect(() => {
  const statusFetcherWatcher = setInterval(() => {
    if (orderRef.current.IsRetrieving) {
      if (fetchCheckerCounter === 1) {
        setOrder(salesOrder => ({
          ...salesOrder,
          RetrievingMsg: "This can take a few seconds...",
        }));
      } else if (fetchCheckerCounter === 2) {
        setOrder(salesOrder => ({
          ...salesOrder,
          RetrievingMsg: "Almost there!..",
        }));
      }

      setFetchCheckerCount(counter => counter + 1);
    } else {
      // Remove timer.
      clearInterval(statusFetcherWatcher);
    }
  }, 7000);

  return () => clearInterval(statusFetcherWatcher);
}, []);

Upvotes: 2

Related Questions