2

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.

adammartiska
  • 35
  • 1
  • 4
  • can you show us how you're trying to "evalutate" the data? are you not simply missing `await sendPost(...)` then console log data again – azium Nov 22 '21 at 20:00

2 Answers2

0

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.

Gabriel Brito
  • 1,003
  • 2
  • 16
  • 26
  • Yeah I tried this approach but I wanted to have all the variables { data, isLoading, error, [and maybe mutation function?] = useMutation(endpoint, body) returned in the hook call. – adammartiska Nov 22 '21 at 21:00
0

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.

Sam McElligott
  • 306
  • 1
  • 4
  • 1
    Thank you this really helped! And for this purpose isn't it better to write some generic functions for GET/POST and process the data in the returned response of this resolved / rejected promise? – adammartiska Nov 22 '21 at 20:55
  • 1
    In a situation like that it is up to the developer. Use whichever you find the most comfortable/easiest to use it's just personal preference. – Sam McElligott Nov 22 '21 at 21:09