if else statement inside useEffect doesn't work as expected. else is executed before the code inside the if is executed completely. I've commented out what I'm expecting in the code. someData prop is the data fetched from the server using the fetch API in the parent component.
import { useState, useEffect } from "react";
const App = ({ someData }) => {
const [text, setText] = useState(0)
const [lastNumber, setLastNumber] = useState(0)
useEffect(() => {
if (!someData) {
(async () => {
const res = await fetch(`https://xxx-xxx......`);
const myJson = await res.json();
const _lastNumber = myJson.number // 1000
setLastNumber(_lastNumber)
console.log(lastNumber) // 1000, appear in the console second
})();
} else {
console.log(lastNumber) // 0, appear in the console first
setText(
lastNumber - someData.number // 0 - 100 (I expect "1000 - 100".)
);
}
}, [someData, lastNumber]);
if (!someData) {
return (
<div>
<h1>Loading</h1>
</div>
)
} else {
return (
<div>
<h1>{text /* -100 (I expect "900".) */}</h1>
</div>
);
}
};
export default App;
If I write code like this using some tricks, it takes a long time to load but works as I want.
import { useState, useEffect } from "react";
const App = ({ someData }) => {
const [text, setText] = useState(0)
const [lastNumber, setLastNumber] = useState(0)
useEffect(() => {
if (!someData) {
(async () => {
const res = await fetch(`https://xxx-xxx......`);
const myJson = await res.json();
const _lastNumber = myJson.number // 1000
setLastNumber(_lastNumber)
console.log(lastNumber)
})();
} else {
if (lastNumber !== 0) {
console.log(lastNumber)
setText(
lastNumber - someData.number
);
}
}
}, [someData, lastNumber]);
if (!someData) {
return (
<div>
<h1>Loading</h1>
</div>
)
} else {
return (
<div>
<h1>{text /* 900 */}</h1>
</div>
);
}
};
export default App;
But I don't know why it works the way that I want and if it's a good way to do it. Please let me know the better way.
You need to move effect in dedicated async function, in order to await part of the execution. In your current solution your effect is synchronous and IIFE you declared will be executed synchronously thus can cause unexpected behaviour of effect.
Rewrite to this(in order to await code execution):
const doSomething = async () => {
if (!someData) {
const res = await fetch(`https://xxx-xxx......`); // NOW YOU CODE WILL WAIT
const myJson = await res.json();
const _lastNumber = myJson.number; // 1000
setLastNumber(_lastNumber);
console.log(lastNumber); // YOU CAN NOT EXPECT TO READ NEW VALUE IMMEDIATELY, THIS WILL LOG OLD VALUE FOR lastNumber
} else {
console.log(lastNumber); // 0, appear in the console first
setText(
lastNumber - someData.number // 0 - 100 (I expect "1000 - 100".)
);
}
};
useEffect(() => {
doSomething();
}, [someData, lastNumber]);
Also, in console.log(lastNumber) part you can not expect to log latest value, one that was set in line before, because state update is async and state is accessed from closure meaning you will access new value from lastNumber but only after rerender.
Yeah! Async functions... )) Read this article for explanations.
The issue here is that the first argument of useEffect is supposed to be a function that returns either nothing (undefined) or a function (to clean up side effects). But an async function returns a Promise, which can't be called as a function! It's simply not what the useEffect hook expects for its first argument.
enum LoadingStatusEnum {
INACTIVE = -1,
ACTIVE = 1,
ERROR = 0,
}
export default function Example({someData}): JSX.Element {
const [text, setText] = useState(0);
const [lastNumber, setLastNumber] = useState(0);
const [loadingStatus, setLoadingStatus] = useState<LoadingStatusEnum>(LoadingStatusEnum.INACTIVE);
useEffect(() => {
const fetchData = async () => {
setLoadingStatus(LoadingStatusEnum.ACTIVE);
const response = await fetch('API action here');
const json = await response.json();
setLastNumber(json.number);
};
if (!someData && loadingStatus !== LoadingStatusEnum.ACTIVE) {
fetchData()
.then(() => {
setLoadingStatus(LoadingStatusEnum.INACTIVE);
})
.catch(() => {
setLoadingStatus(LoadingStatusEnum.ERROR);
});
} else {
setText(lastNumber - someData.number);
}
}, [someData, lastNumber]);
const renderTitle = () => {
let title = String(text);
if (loadingStatus === LoadingStatusEnum.ACTIVE) {
title = 'Loading';
}
if (loadingStatus === LoadingStatusEnum.ERROR) {
title = 'Error';
}
return title;
};
return (
<div>
<h1>
{renderTitle()}
</h1>
</div>
);
}