3

I have a lot of react experience but I'm new to hooks.
I have the following useFetch hook that I modified after this useAsync hook:

import { useState, useEffect, useCallback } from 'react'

export default function useFetch(url, options, { immediate }) {
  const [data, setData] = useState(null)
  const [error, setError] = useState(null)
  const [isPending, setIsPending] = useState(false)

  const executeFetch = useCallback(async () => {
    setIsPending(true)
    setData(null)
    setError(null)
    await fetch(url, options)
      .then((response) => response.json())
      .then((response) => setData(response))
      .catch((err) => setError(err))
      .finally(() => setIsPending(false))
    return { data, error, isPending }
  }, [url, options, data, error, isPending])

  useEffect(() => {
    if (immediate) {
      executeFetch()
    }
  }, [executeFetch, immediate])
  return { data, error, isPending, executeFetch }
}

My problem is I want to use it inside a submit function, and hooks don't work inside other functions, like so (reduced version of the code for brevity):

export default function SignupModal({ closeModal }) {
  const { executeFetch } = useFetch(url, {options},
    { immediate: false }
  )

  async function handleSubmit(evt) {
    evt.preventDefault()
    const { data, error, isPending } = await executeFetch()
  }
  ...
}

currently I'm intentionaly throwing an error in the call, but the error variable remains null.

What am I missing here? Is this even possible with hooks?

Thanks in advance!

1 Answer 1

7

React hook can only be used in the body of your component not inside another function. executeFetch itself is returning { data, error, isPending } and this makes it a nested hook so you can't use it inside your handleSubmit.

useFetch is already returning { data, error, isPending, executeFetch } so executeFetch doesn't need to return again. You can access all these data from the useFetch hook. When you call executeFetch data in your component, data, error and isPending will be updated by setState which will cause your hook to return a new set of values for any of these values that get updated.

export default function useFetch(url, options, { immediate }) {
  const [data, setData] = useState(null)
  const [error, setError] = useState(null)
  const [isPending, setIsPending] = useState(false)

  const executeFetch = useCallback(async () => {
    setIsPending(true)
    setData(null)
    setError(null)
    await fetch(url, options)
      .then((response) => response.json())
      .then((response) => setData(response))
      .catch((err) => setError(err))
      .finally(() => setIsPending(false))
  }, [url, options, data, error, isPending])

  useEffect(() => {
    if (immediate) {
      executeFetch()
    }
  }, [executeFetch, immediate])
  return { data, error, isPending, executeFetch }
}

export default function SignupModal({ closeModal }) {
  const { executeFetch, data, error, isPending } = useFetch(url, {options},
    { immediate: false }
  )

  async function handleSubmit(evt) {
    evt.preventDefault()
    await executeFetch()
  }    
  ...
  // Example in your return function
  {error != null && <Error />}
  <Button state={isPending ? 'processing' : 'normal'}

}

Updated based on the comment

If you need to have an access to data or error inside your handleSubmit function, you will need to return the promise's response/error in your hook so then you should be able to access data/error inside your handleSubmit as well.

Also I recommend to pass options or any other variable data that are subject to change before user triggers handleSubmit to the executeFetch as an argument so executeFetch can always get the latest data.

CodeSandBox Example 1

CodeSandBox Example 2

const useFetch = url => {
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);
  const [data, setData] = useState(null);

  const executeFetch = useCallback(
    // Here you will access to the latest updated options. 
    async ({ options }) => {
      setIsPending(true);
      setError(null);
        return await fetch(url, options)
        .then(response => response.json())
        .then(response => {
          setData(response); 
          return response;
        })
        .catch(err => {
          setError(err.message)
          return err;
        })
        .finally(() => setIsPending(false));
    },
    [url, setIsPending, setError]
  );
  return { data, error, isPending, executeFetch }
};

const { data, executeFetch, error, isPending } = useFetch("URL");
  
  const handleSubmit = useCallback(async (event) => {
    event.preventDefault();
    // I am passing hardcoded { id: 1 } as an argument. This can 
    // be a value from the state ~ user's input depending on your
    // application's logic.
    await executeFetch({ id: 1 }).then(response => {
      // Here you will access to 
      // data or error from promise. 
      console.log('RESPONSE: ', response);
    })
  }, [executeFetch]);

Another recommendations is to not pass a boolean to trigger executeFetch immediately inside your hook, it's up to the caller to decide whether to run the executeFetch immediately or not.

const { executeFetch, ... } = useFetch(....);
// you can call it immediately after setting the hook if you ever needed
await executeFetch()
Sign up to request clarification or add additional context in comments.

4 Comments

I tried that, but I have code inside handleSubmit after executeFetch, and that code needs access to the data and error variables, but they don't update
@goldylucks updated the answer based on your comment.
Thank you for taking the time :) My problem is that inside the handle submit function, I cant access what the useFetch hook returns. I forked your sandbox and added a console log of data. Notice that it only shows once as null.
@goldylucks when you are already inside handleSubmit you can't access what the useFetch hook returns in main function. When you click the second time data from the hook is not null anymore because it holds previous data. You can access the data or error inside handleSubmit using .then() or you can pass two callbacks onSuccess and onError to executeFetch and then access data or error inside handleSubmit directly from executeFetch like codesandbox.io/s/react-hooks-useref-z6n2k?file=/src/index.js

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.