Search code examples
javascriptreactjsfetch-apiuse-effectuse-state

React useEffect looping many times when fetching data and setState using fetch API


Why is this triggering fetchData multiple times? The console.log seems to loop almost infinitely? How do I get this to run just once onload and trigger only once when fetchData() is called later? What am I doing wrong here or missing?

const [data, setData] = useState(null);

  let fetchData = React.useCallback(async () => {
    const result = await fetch(`api/data/get`);
    const body = await result.json();
    setData(body);
    console.log(data)
  },[data])

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

Update (Additional Question): How to wait for data to populate before return() the below this now gives the error because it is null at first?: data.map is not a function

return (
    <select>
        {data.map((value, index) => {
            return <option key={index}>{value}</option>
        })}
    </select>
)

Solution

  • 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.