In the useCallback hook you pass data as a dependency and also simultaneously change the value of data inside the callback by calling setData, which means every time the value of data changes fetchData will be reinitialized.
In the useEffect hook fetchData is a dependency which means every time fetchData changes useEffect will be triggered. That is why you get an infinite loop.
Because you want to fetch data once when the component is mounted, I think useCallback is unnecessary here. There is no need to memoize the function fetchData unnecessarily.
Solution
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const result = await fetch(`api/data/get`);
const body = await result.json();
setData(body);
} catch(err) {
// error handling code
}
}
// call the async fetchData function
fetchData()
}, [])
If you want to log the value of data when it changes, you can use another useEffect to do that instead. For example,
useEffect(() => {
console.log(data)
}, [data])
P.S. - Also don't let the promise get unhandled, please use a try...catch block to handle the error. I have updated the solution to include the try..catch block.
Edit - solution for the additional question
There are two possible solutions,
Because you expect the value of data to be an array after the API call you can initialize the value of data as an empty array like,
const [data, setData] = useState([]);
But if for some reason you have to initialize the value of data as null. Here is how you can render the information returned from the API call.
// using short-circuit evaluation
return (
<select>
{data && data.length && data.map((value) => {
return <option key={`select-option-${value}`}>{value}</option>
})}
</select>
)
// using a ternary
return (
<div>
{ data && data.length
? (<select>
{
data.map(value => <option key={`select-option-${value}`}>{value}</option>)
}
</select>
)
: <div>Data is still loading...</div>
}
</div>
)
Do not use indexes as key, use something unique for the key.