React Hooks - handling async api call

Viewed 2156

I'm trying to set up a custom hook to handle all api fetches in my project. I can make the call successfully, and store the data, but only on calls after the first one. I am pretty confident that the first call does not return anything because of race conditions and the nature of async.

Here is my hook:

import { useState, useEffect } from 'react'
import api from 'services/api'

const useHopServiceApi = () => {
    const [data, setData] = useState([])
    const [url, setUrl] = useState('')
    const [body, setBody] = useState({})
    const [isLoading, setIsLoading] = useState(false)
    const [isError, setIsError] = useState(false)

    useEffect(() => {
        const fetchData = async () => {
            setIsError(false)
            setIsLoading(true)
            try {
                const result = await api.post(url, body)
                setData(result.d)
            } catch (error) {
                setIsError(true)
            }
            setIsLoading(false)
        }
        if (url && body) {
            fetchData()
        }
    }, [url, body])
    return [{ data, isLoading, isError }, setUrl, setBody]
}

export default useHopServiceApi

and the component it is being called in:

const HomePage = (props) => {
  const [{ data, isLoading, isError }, doFetch, setBody] = useHopServiceApi()

const handleClickSelectInterests = () => {
    setBody({ count: 1 })
    doFetch('/GetPopularCities')
    console.log('data:', data)
  }
}

When I click the button (to trigger the api call), data is logged out as an empty array, but on subsequent clicks, it is filled with the data.

2 Answers

The reason you aren't seeing data after the first click is that you don't have the data yet from the doFetch immediately after it is called.

The second time you click the button to call handleClickSelectInterests data is logged, but it is the data from the first render. You can render the updated data just fine, but you can't log the most up to date thing inside the handleClick function.

Here is an example to show what I mean:

Code sandbox example

Root component:

import React from "react";
import ReactDOM from "react-dom";

import useHopServiceApi from "./useHopServiceApi";
import "./styles.css";

function App() {
  const [{ data, isLoading, isError }, doFetch] = useHopServiceApi();
  console.log("Data in render!", data);
  const handleClick = () => {
    doFetch("/fakeUrl");
    // Data logged below will show the data from the previous request, not the latest
    console.log("Data in handle click!", data);
  };
  return (
    <div className="App">
      {isLoading && <div>Is loading!</div>}
      <button onClick={handleClick}>Simulate fetch!</button>
      {data && <div>Data! {data}</div>}
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Your Hook:

import { useState, useEffect } from "react";
let numClick = 1;

const simulatedFetch = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`Fetch request data for request number ${numClick}`);
      numClick++;
    }, 2000);
  });
};
const useHopServiceApi = () => {
  const [data, setData] = useState([]);
  const [url, setUrl] = useState("");
  const [body, setBody] = useState({});
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);
      try {
        const result = await simulatedFetch();
        console.log("new result!", result);
        setData(result);
        setUrl("");
      } catch (error) {
        setIsError(true);
      }
      setIsLoading(false);
    };
    if (url) {
      fetchData();
    }
  }, [url, body]);
  return [{ data, isLoading, isError }, setUrl, setBody];
};

export default useHopServiceApi;

The example above shows that you can use the most up to date data in the render function, even though you can't log it in the handleClick function

From the docs:

The setState function is used to update the state. It accepts a new state value and enqueues a re-render of the component.

At first click, hook updates are enqued and console.log('data:', data) is reached before state updates are executed, so empty data is logged.

You should place console.log in other places of the function to get a better grasp of sequence of execution

Related