Home > Back-end >  React custom fetch hook is one step behind
React custom fetch hook is one step behind

Time:11-23

I am creating my custom fetch hook for both get and post data. I was following official React docs for creating custom fetch hooks, but it looks like my hook-returned state variables are one step behind behind due to useState asynchronous behaviour. Here is my custom useMutation hook

export const useMutationAwait = (url, options) => {
  const [body, setBody] = React.useState({});
  const [data, setData] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [isLoading, setIsLoading] = React.useState(false);

  React.useEffect(() => {
    const fetchData = async () => {
      setError(null);
      setIsLoading(true);
      console.log("...fetching");
      try {
        const response = await axiosInstance.post(url, body, options);
        setData(response.status);
      } catch (error) {
        console.error(error.response.data);
        setError(error.response.data);
      }
      setIsLoading(false);
    };
    fetchData();
  }, [body]);

  return [{ data, isLoading, error }, setBody];
};

And I am using it in my component like this (simplified) - when user presses register button I want to be able immediately decide if my post was successful or not and according to that either navigate user to another screen or display fetch error.

const [email, setEmail] = React.useState('');
const [password, setPassword] React.useState('');
const [{ data: mutData, error: mutError }, sendPost] =
    useMutationAwait("/auth/pre-signup");
  
const registerUser = async () => {
    sendPost({
      email,
      password,
    }); ---> here I want to evaluate the result but when I log data and error, the results come after second log (at first they are both null as initialised in hook)

Is this even correct approach that I am trying to achieve? Basically I want to create some generic function for data fetching and for data mutating and I thought hooks could be the way.

CodePudding user response:

Your approach isn't wrong, but the code you're sharing seams to be incomplete or maybe outdated? Calling sendPost just update some state inside your custom hook but assuming calling it will return a promise (your POST request) you should simply use async-await and wrap it with a try-catch statement.

export const useMutationAwait = (url, options) => {
  const sendPost = async (body) => {
    // submit logic here & return request promise
  }
}
const registerUser = async () => {
  try {
    const result = await sendPost({ login, password });
    // do something on success
  } catch (err) {
    // error handling
  }
}

Some recommendations, since you're implementing your custom hook, you could implement one that only fetch fetch data and another that only submit requests (POST). Doing this you have more liberty since some pages will only have GET while others will have POST or PUT. Basically when implementing a hook try making it very specific to one solution.

CodePudding user response:

You're absolutely correct for mentioning the asynchronous nature of state updates, as that is the root of the problem you're facing.

What is happening is as follows:

  • You are updating the state by using sendPost inside of a function.
  • React state updates are function scoped. This means that React runs all setState() calls it finds in a particular function only after the function is finished running. A quote from this question:

React batches state updates that occur in event handlers and lifecycle methods. Thus, if you update state multiple times in a handler, React will wait for event handling to finish before re-rendering.

  • So setBody() in your example is running after you try to handle the response, which is why it is one step behind.

Solution

In the hook, I would create handlers which have access to the data and error variables. They take a callback (like useEffect does) and calls it with the variable only if it is fresh. Once it is done handling, it sets it back to null.

export const useMutationAwait = (url, options) => {
  const [body, setBody] = React.useState({});
  const [data, setData] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [isLoading, setIsLoading] = React.useState(false);

  const handleData = (callback) => {
    if (data){
      callback(data);
      setData(null);
    }
  }

  const handleError = (callback) => {
    if (error){
      callback(error);
      setError(null);
    }
  }
    
  React.useEffect(() => {
    const fetchData = async () => {
      setError(null);
      setIsLoading(true);
      console.log("...fetching");
      try {
        const response = await axiosInstance.post(url, body, options);
        setData(response.status);
      } catch (error) {
        console.error(error.response.data);
        setError(error.response.data);
      }
      setIsLoading(false);
    };
    fetchData();
  }, [body]);

  return [{ data, handleData, isLoading, error, handleError }, setBody];
};

We now register the handlers when the component is rendered, and everytime data or error changes:

const [
  { 
    data: mutData, 
    handleData: handleMutData,
    error: mutError,
    handleError: handleMutError
  }, sendPost] = useMutationAwait("/auth/pre-signup");

handleMutData((data) => {
  // If you get here, data is fresh.
});

handleMutError((error) => {
  // If you get here, error is fresh.
});
  
const registerUser = async () => {
    sendPost({
      email,
      password,
    });

Just as before, every time the data changes the component which called the hook will also update. But now, every time it updates it calls the handleData or handleError function which in turn runs our custom handler with the new fresh data.

I hope this helped, let me know if you're still having issues.

  • Related