Nicholas Nanda
Nicholas Nanda

Reputation: 21

React state loop on conditional if

Have some confusion in using the react state, as from my code below, when trying to hit the API, the res2 will be running twice, and res3 will be running 4 times, and so on going deeper on the nested if.

Is there any way to only hit each one of the API chains once, then after hitting each API would update the taskNum state and render it on screen?

function Loading() {
  const [taskNum, setTaskNum] = useState(1);
  const location = useLocation();

  const client = axios.create({
    baseURL: "https://localhost:8000"
  });

  const predictImage = useCallback(async () => {
    try {
      const res1 = await client.post(`/get-box/${location.state.id}`);

      if (res1 && !(res1["data"]["img_id"] === -1)) {
        setTaskNum(2);
        const res2 = await client.post(
          `/get-img/${res1["data"]["img_id"]}`
        );
        console.log("response 2");

        if (res2) {
          setTaskNum(3);
          const res3 = await client.post(
            `/super/${res2["data"]["historyId"]}`
          );
          console.log("response 3");

          if (res3) {
            setTaskNum(4);
            const res4 = await client.post(
              `/easy/${res3["data"]["historyId"]}`
            );
            console.log("response 4");

            if (res4) {
              setTaskNum(5);
              const res5 = await client.post(
                `/get-history/${res4["data"]["historyId"]}`
              );
              console.log("response 5");

              if (res5) navigate("/hasil", { state: res5["data"] });
            }
          }
        }
      }
    } catch (error) {
      console.log(error);
      setTaskNum(0);
    }
  }, []);

  useEffect(() => {
    predictImage();
  }, []);

get description function

const getDescription = (num) => {
  if (num === -1) {
    return "Oh no...";
  }
  if (num === 1) {
    return "Done Uplo...";
  }
  if (num === 2) {
    return "Found the..";
  }
  if (num === 3) {
    return "Plates are ready....";
  }
  if (num === 4) {
    return "Eureka..";
  }
  if (num === 5) {
    return "All Done..";
  }
  return "I'm sorry but..";
};

my render return

  return (
    <div className="container mx-auto mt-16 mb-24">
      <div className="flex flex-col items-center mt-16">
        <ScaleLoader
          color="#3F83F8"
          height={30}
          speedMultiplier={0.8}
          width={8}
        />
        <div className="mt-6 text-xl font-medium">Doing Some Magic!</div>
        <span className="mt-1 text-center font-medium text-gray-500">
          {taskNum}/5 Task
          <br /> {getDescription(taskNum)}
        </span>
      </div>
    </div>
  );
}

I've been trying to nest the setTaskNum on conditional if and function to handle updates, but all of them always hit the API multiple times.

if (res1 && !(res1["data"]["userId"] === -1)) {
  if (taskNum === 1) {
    console.log("setTaskNum(2)");
    setTaskNum(2);
  }
  const res2 = await client.get(`/users/${res1["data"]["userId"]}`);
  console.log("response 2");

EDIT My real API has a long processing time, it is Machine Learning related (inference image)

Live preview of the error see in console

Edit React state loop on conditional if

Upvotes: 2

Views: 108

Answers (3)

Nicholas Nanda
Nicholas Nanda

Reputation: 21

was able to resolve it by myself,

  1. Defining global variable
const defaultPredictRes = {
  res1: null,
  res2: null,
  res3: null,
  res4: null,
  taskNum: null
};
  1. Passing it to the useState()
const [predictRes, setPredictRes] = useState(defaultPredictRes);
  1. Set the useState() within conditional
      if (predictStep === 0) {
        const apiRes1 = await client.get(`...`);
        console.log("res1", apiRes1);
        if (apiRes1.data)
          setPredictRes((curRes) => ({
            ...curRes,
            res1: apiRes1.data,
            taskNum: 1
          }));
      }

      if (predictStep === 1 && predictRes.res1?.userId) {
        const apiRes2 = await client.get(`...`);
        console.log("res2", apiRes2);
        if (apiRes2)
          setPredictRes((curRes) => ({
            ...curRes,
            res2: apiRes2.data,
            taskNum: 2
          }));
      }
  1. Create 2 useEffect(), to compensate multiple API catch at first page load.
  // first render + first initiate, componentDidMount()
  useEffect(() => {
    setPredictRes((curRes) => ({ ...curRes, taskNum: 0 }));
  }, []);

  // next stepper
  useEffect(() => {
    console.log("step", predictRes);
    predictImage(predictRes.taskNum);
  }, [predictRes.taskNum]);

Console Result

enter image description here


Full Code

Edit React state loop on conditional if

Upvotes: 0

Drew Reese
Drew Reese

Reputation: 203587

What I see is the useEffect being run twice basically. I see only two of each log.

enter image description here

This seems to be caused by rendering the app into a React.StrictMode component which double-invokes certain lifecycle methods and functions as a way to help you detect unexpected side-effects. Function component bodies are one of these. What I see missing from the code is any useEffect hook cleanup function to cancel/abort any in-flight network requests.

You should return a cleanup function to do this.

Example:

// Moved outside component to remove it as a dependency
const client = axios.create({
  baseURL: "https://jsonplaceholder.typicode.com"
});

function Loading() {
  const [taskNum, setTaskNum] = useState(1);

  const predictImage = useCallback(async ({ controller }) => { // <-- receive controller
    try {
      console.log("before res 1");

      const res1 = await client.get(`/posts/1`, { signal: controller.signal }); // <-- pass controller signal
      console.log(res1);
      console.log("response 1");

      if (res1) {
        setTaskNum(2);
        console.log("setTaskNum(2)");
        const res2 = await client.get(`/users/${res1["data"]["userId"]}`, {
          signal: controller.signal // <-- pass controller signal
        });
        console.log("response 2");
        console.log(res2);
        if (res2) {
          setTaskNum(3);
          console.log("setTaskNum(3)");
          const res3 = await client.get(`/comments/${res2["data"]["id"]}`, {
            signal: controller.signal // <-- pass controller signal
          });
          console.log("response 3");
          console.log(res3);
        }
      }
    } catch (error) {
      console.log(error);
      setTaskNum(0);
    }
  }, []);

  useEffect(() => {
    const controller = new AbortController(); // <-- create controller

    predictImage({ controller }); // <-- pass controller

    return () => controller.abort(); // <-- return cleanup function to abort
  }, [predictImage]);

  return (
    ...
  );
}

Edit react-state-loop-on-conditional-if

enter image description here

Upvotes: 1

Nice Books
Nice Books

Reputation: 1861

The second parameter to setState() is an optional callback function that will be executed once setState is completed and the component is re-rendered.

https://reactjs.org/docs/react-component.html#setstate

So, using class components (constructor, render etc),

async predictImage() {
    var res1 = await ...;
    if(res1) {
        this.setState({taskNum: 2}, ()=> {
            var res2 = await ...;
            if(res2) {
                this.setState({taksNum: 3}, ()=> {
                    var res3 = await ...;
                    if(res3) {
                        this.setState({taskNum: 4}, ()=>{
                            var res4 = await ...;
                            if(res4) {
                            }
                        );
                     }     
}

Since you're calling predictImage from inside useEffect(..,[]),
you can call it from inside componentDidMount or even rename predictImage to componentDidMount.

If you are using React Router <= 5, useLocation can be replaced with withRouter.
For React Router 6+, see (https://reactrouter.com/en/v6.3.0/faq)

Upvotes: 0

Related Questions