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()