Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

implement useFetch react hook to work inside submit function

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!

like image 787
goldylucks Avatar asked Jul 10 '26 16:07

goldylucks


1 Answers

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()
like image 111
Matin Kajabadi Avatar answered Jul 13 '26 17:07

Matin Kajabadi