implement useFetch react hook to work inside submit function - reactjs

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!

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

Related

My component keeps rerendering while using a path param

I'm trying to use a path param which I fetch from the URL path as an ID for an entity which I'm trying to fetch. I've created a custom data fetching hook which triggers when either the path of the passed params change. I'm use useParams from react router dom to get the ID of the book from the URL.
Below is the component code:
const BookDetails: FC<BookDetailsProps> = () => {
let { bookId } = useParams();
const { response, loading, error } = useApi(`/books/v1/volumes/${bookId}`);
return <Wrapper></Wrapper>;
};
export default BookDetails;
And below is my custom hook:
const useApi = (url: string, params = {}) => {
const [response, setResponse] = useState<AxiosResponse>();
const [error, setError] = useState<AxiosError>();
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await api(url, { params: params });
setResponse(result);
} catch (err: any) {
setError(err);
} finally {
setLoading(false);
}
}, [url, params]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { response, error, loading, fetchData };
};
export default useApi;
Now when I use my API hook with a component which fetches data using the params object everything is fine. The component doesn't rerender and I just get the data, but for some reason when I use it with path params it goes off.
Does anyone know how I should proceed?
I think the issue is that when no params object is passed to the useApi hook
useApi(`/books/v1/volumes/${bookId}`)
The params argument is initialized to a default empty object value.
const useApi = (url: string, params = {}) => {
...
This causes the params variable to be a new object reference anytime the component rerenders for any reason and will retrigger the useEffect hook because fetchData will be a newly recomputed reference.
...
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await api(url, { params: params });
setResponse(result);
} catch (err: any) {
setError(err);
} finally {
setLoading(false);
}
}, [url, params]); // <-- params new reference
useEffect(() => {
fetchData();
}, [fetchData]); // <-- fetchData becomes new reference
...
};
I suggest not initializing param and only provide a fallback value when calling the api function.
Example:
const useApi = (url: string, params) => { // <-- don't initialize
const [response, setResponse] = useState<AxiosResponse>();
const [error, setError] = useState<AxiosError>();
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await api(url, { params: params || {} }); // <-- provide fallback value
setResponse(result);
} catch (err: any) {
setError(err);
} finally {
setLoading(false);
}
}, [url, params]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { response, error, loading, fetchData };
};
export default useApi;
This way params will be an undefined value from render to render and not change shallow reference equality each render.
params gets a new object instance on each call so the dependency array in the useCallback will never be the same between different calls (you haven't pass a params argument to your useApi) and therefore the useEffect will be triggered on each render casing a re-render, hence the infinite rerendering

useEffect dependency causes infinite loop

I created a custom hook which I use in App.js
The custom hook (relevant function is fetchTasks):
export default function useFetch() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [tasks, setTasks] = useState([]);
const fetchTasks = async (url) => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("falied!");
}
const data = await response.json();
const loadedTasks = [];
for (const taskKey in data) {
loadedTasks.push({ id: taskKey, text: data[taskKey].text });
}
setTasks(loadedTasks);
} catch (err) {
console.log(err.message);
}
setLoading(false);
};
return {
loading,
setLoading,
error,
setError,
fetchTasks,
tasks,
};
}
Then in my App.js:
function App() {
const { loading, setLoading, error, setError, fetchTasks, tasks } =
useFetch();
useEffect(() => {
console.log("fetching");
fetchTasks(
"https://.....firebaseio.com/tasks.json"
);
}, []);
My IDE suggests adding the fetchTasks function as a dependency to useEffect. But once I add it, an infinite loop is created. If I omit it from the dependencies as shown in my code, it will work as expected, but I know this is a bad practice. What should I do then?
Because that every time you call useFetch(). fetchTasks function will be re-created. That cause the reference to change at every render then useEffect() will detected that dependency fetchTasks is re-created and execute it again, and make the infinite loop.
So you can leverage useCallback() to memoize your fetchTasks() function so the reference will remains unchanged.
import { useCallback } from 'react'
export default function useFetch() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [tasks, setTasks] = useState([]);
const fetchTasks = useCallback(
async (url) => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("falied!");
}
const data = await response.json();
const loadedTasks = [];
for (const taskKey in data) {
loadedTasks.push({ id: taskKey, text: data[taskKey].text });
}
setTasks(loadedTasks);
} catch (err) {
console.log(err.message);
}
setLoading(false);
};,[])
return {
loading,
setLoading,
error,
setError,
fetchTasks,
tasks,
};
}
function App() {
const { loading, setLoading, error, setError, fetchTasks, tasks } =
useFetch();
useEffect(() => {
console.log("fetching");
fetchTasks(
"https://.....firebaseio.com/tasks.json"
);
}, [fetchTasks]);
instead of return fetchTasks function return this useCallback fetchTasksCallback function from useFetch hook which created only one instance of fetchTasksCallback.
const fetchTasksCallback = useCallback(
(url) => {
fetchTasks(url);
},
[],
);
function App() {
const { loading, setLoading, error, setError, fetchTasksCallback, tasks } =
useFetch();
useEffect(() => {
console.log("fetching");
fetchTasksCallback(
"https://.....firebaseio.com/tasks.json"
);
}, [fetchTasksCallback]);
the problem is this fetchTasks every time create a new instance that way dependency list feels that there is a change and repeats the useEffect code block which causes the infinite loop problem

Access function argument inside function react hooks

I am writing a custom react hook for fetching data from an endpoint. This is what the function looks like
import { useState } from "react";
const useHttp = async (endpoint, method, data) => {
const [loading, setLoading] = useState(false)
const [fetchedData, setfetchedData] = useState(null)
setfetchedData(await fetch.method(endpoint));
return [isLoading, fetchedData]
}
export default useHttp;
As you can see, I want to do a fetch request to whatever method is passed on to the useHttp hook. Please someone point me how to do it?
You cannot pass async functions to React Hooks. You have to useEffect
import { useState, useEffect } from "react";
const useHttp = (endpoint, method, options) => {
const [isLoading, setLoading] = useState(false);
const [fetchedData, setFetchedData] = useState(null);
useEffect(() => {
setLoading(true);
fetch(endpoint, { method, ...options })
.then(data => data.json())
.then((json) => {
// do something with JSON data
setFetchedData(json);
})
.catch((err) => {
// do something with err
})
.finally(() => {
setLoading(false);
});
}, []);
return [isLoading, fetchedData];
};
export default useHttp;
Use useEffect hook to make the HTTP request.
fetch function takes an optional second argument which is an object specifying various options for the HTTP request and one of the options is a method option. Use this method option to specify the request method.
import { useState, useEffect } from "react";
const useHttp = async (endpoint, method, data) => {
const [loading, setLoading] = useState(false);
const [fetchedData, setfetchedData] = useState(null);
useEffect(() => {
setLoading(true);
fetch(endpoint, { method })
.then(res => res.json())
.then(data => {
setLoading(false);
setfetchedData(data);
})
.catch(err => {
setLoading(false);
console.log(err.message);
});
}, []);
return [isLoading, fetchedData];
}
For details on how to specify options for fetch function and different options that can be specified, see using fetch
If you want to use async-await syntax, you can write useEffect hook as:
useEffect(() => {
async function makeRequest() {
setLoading(true);
try {
const response = await fetch(endpoint, { method });
const data = await res.json();
setLoading(false);
setfetchedData(data);
} catch (error) {
setLoading(false);
console.log(err.message);
}
}
makeRequest();
}, []);
hi maybe this help you:
1- call function:
const useHttp = async (url,method,data)=>{
var options = {
method:method,
headers: {
'Content-Type': 'application/json; charset=utf-8;'
}
};
if(method==='POST' && data)
options.body = JSON.stringify(data);
const response = await fetch(url, options);
const rep = await response.json();
console.log(rep);
return rep;
};
in above code first create your request options and then send it by fetch to end point.
2- use it in compoent like below:
setLoading(true);
var rep = await useHttp(...)
setLoading(false);

How can i use useReducer to assign initial state after calling custom Datafetch hook? I keep getting null

I created a custom datafetch hook but when i use the reducer function to set it as initial state it says its null.
Component where i call the custom Hook.
const collection = 'items'
const whereClause = { array: "lists", compare: 'array-contains', value: 'Pantry' }
const res = useDataFetchWhere(collection, whereClause)
const data = res.response
const [state, dispatch] = useReducer(reducer, data)
When I console.log(state) I get null.
My custom data fetch hook
const useDataFetchWhere = (collection, whereClause) => {
const [response, setResponse] = useState(null)
const [error, setError] = useState(null)
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
const fetchData = async () => {
setIsLoading(true)
setError(false)
try {
await db.collection(collection).where(whereClause.array, whereClause.compare, whereClause.value).get()
.then(data => {
setResponse(data.docs.map(doc => ({ ...doc.data(), id: doc.id })))
setIsLoading(false)
console.log('hello where')
})
} catch (error) {
setError(error)
}
}
fetchData()
return function cleanup() {
console.log('cleaned up check')
};
}, [])
return { response, error, isLoading }
}
Is there anything i need to do or call in a different way?
Thanks.
The problem is that useDataFetchWhere does not immediately return the result of the data fetching, but only after a while the request is done and then the setResponse will set the actual data. So you cannot set the response as initial state for the useReducer call.
You need to wait until the request is done before using it's result. You could create an action (e.g. SET_DATA) for the reducer that sets the result once it's there.
You already have the isLoading flag available:
const [state, dispatch] = useReducer(reducer, null);
useEffect(() => {
if (!isLoading) {
const data = res.response;
dispatch({type: 'SET_DATA', data});
}
}, [isLoading]);

How to correctly call useFetch function?

I've successfully implemented a useFetch function to call an API Endpoint. It works perfectly if I add code like this to the root of a functional React component like this:
const [{ data, isLoading, isError }] = useFetch(
'http://some_api_endpoint_path'
);
export const useFetch = (url) => {
const [data, setData] = useState();
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const response = await axios.get(url);
setData(response.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
return [{ data, isLoading, isError }];
};
But let's say I want to check if a newly entered username exists, say upon the firing of an onBlur event of an input element. When I've tried implementing this, I get this error:
React Hook "useFetch" is called in function "handleBlur" which is neither a React function component or a custom React Hook function react-hooks/rules-of-hooks
I even tried this approach:
const [isChanged, setIsChanged] = useState(false);
useEffect(() => {
useFetch(
'http://some_api_endpoint_path'
);
}, [isChanged]);
But got the same error.
Then I tried this simplified version, which doesn't do anything useful but I was testing the React Hooks Rules:
useEffect(() => {
useFetch(
'http://some_api_endpoint_path'
);
}, []);
And still I got the same error.
In these last 2 cases especially, I feel that I am following the Rules of Hooks but apparently not!
What is the correct way to call useFetch in such a situation?
I suppose you call useFetch this way, right?
const onBlur = () => {
const [{ data, isLoading, isError }] = useFetch(
'http://some_api_endpoint_path'
);
...
}
If true, this is wrong. Check this link out:
🔴 Do not call in event handlers.
You may implement this way:
// Pass common initial for all fetches.
export const useFetch = (awsConfig, apiRoot, apiPathDefault) => {
const [data, setData] = useState();
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
// Just pass the variables that changes in each new fetch requisition
const fetchData = async (apiPath) => {
setIsError(false);
setIsLoading(true);
try {
const response = await axios.get(apiRoot + apiPath);
setData(response.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
useEffect(() => {
fetchData(apiRoot + apiPathDefault);
}, [awsConfig, apiRoot, apiPathDefault]);
return [{ data, isLoading, isError }, fetchData];
};
And whenever you want to fetch again, you just call fetchData:
const [{ data, isLoading, isError }, fetchData] = useFetch(API_ROOT(), appStore.awsConfig, defaultPath);
const onBlur = () => {
fetchData(newPath);
...
}
I've used the same principle that Apollo team used when created useLazyQuey (open this link and search for useLazyQuery, please). Also, note that I pass all common and immutable variables when I call the hooks and pass just the mutable ones in the single fetch.

Resources