calling useFetch hook inside useEffect - javascript

I found a very interesting hook and I want to use this hook inside a useEffect (it goes against the rules)
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
}, []);
return { response, error };
};
function App() {
const res = useFetch("example.com", {});
useEffect(() => {
// use the hook and make a secondary request
}, [])
what modifications do I need to make in order to support this? AFAIK hooks can´t be called inside useEffect
maybe a new parameter that will setUrl and run it again?

Seems like you want to execute the fetch request in the hook when some state or variable changes.
You cannot conditionally call a hook, nor can you execute it inside another hook accordinng to the rules of hooks mentioned in the documentation.
To do what you want, you can modify your custom hook to accept an array as a dependency that you pass on to the useEffect inside it and it will call the api when any of the dependency changes
const useFetch = (url, options, deps = []) => {
const [response, setResponse] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
url && fetchData();
}, deps);
return { response, error };
};
and use it like
function App() {
const res = useFetch("example.com", {}, [someVariable]);
...
}

You can't call useFetch conditionally nor can it be called in any callback (i.e. a useEffect callback) (see rules of hooks), but you can take advantage of the fact that hooks are called in the same order each render. Do the conditional test and set the URL that is passed to the second useFetch hook. Update the useFetch hook to check for truthy url before making the request.
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
url && fetchData();
}, []);
return { response, error };
};
...
function App() {
const res = useFetch("example.com", {});
let url = "";
if (someCondition) {
let url = ""example2.com"";
}
const res2 = useFetch("example.com", {});

Related

UseEffect async actions

In my useEffect hook i have a few API requests, like:
useEffect(() => {
dispatch(action1());
dispatch(action2());
dispatch(action3());
}, []);
How can i use 'loading' parameter inside hook using async/await function to have loading as true before dispatching first request, and as false after dispatching last request.
Like:
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
dispatch(action1());
dispatch(action2());
dispatch(action3());
setLoading(false);
}, []);
You can do it like this:
const [loading, setLoading] = useState(false);
useEffect(() => {
async function fetchData() {
setLoading(true);
await dispatch(action1());
await dispatch(action2());
await dispatch(action3());
setLoading(false);
}
fetchData()
}, []);
Read more about useEffect and async: React Hook Warnings for async function in useEffect: useEffect function must return a cleanup function or nothing
I always create a custom hook and use that for API call and redux dispatch. have a look this might help.
const useAction = () => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const fetchData = async (params) => {
setLoading(true);
try {
// do something
// await call api
dispatch(action1());
dispatch(action2());
dispatch(action3());
} catch (error) {
} finally {
setLoading(false);
}
};
return { fetchData, loading };
};
// Component
const { fetchData, loading } = useAction();
useEffect(() => {
fetchData(params);
}, []);

React component not re-rendering on URL parameter change when using useEffect hook to fetch data

Here's the issue:
I have a component that is meant to be the same structure for ≈ 25 different items/pages. So, I do what anyone would when trying to use React, and I am passing dynamic URL parameters into my API request (pictured below).
const [{ items, isLoading, isError }] = useDataApi(
`http://localhost:5000/api/teams/topspending/${params.team}`,
[],
params.team);
This is simply using a useEffect component that has been separated for simplicity (pictured below).
const useDataApi = (initialUrl, initialData, effect) => {
console.log("start/top Data API");
const [items, setItems] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
setUrl(initialUrl);
const abortCtrl = new AbortController();
const opts = { signal: abortCtrl.signal };
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
console.log("data loading");
try {
console.log(url, "this should be the new Url");
const result = await axios(url, opts);
setItems(result.data.data);
} catch (error) {
setIsError(true);
}
console.log("data loaded...");
setIsLoading(false);
};
fetchData();
return () => abortCtrl.abort();
}, [effect]);
return [{ items, isLoading, isError }];
};
export default useDataApi;
The task is pretty simple. Upon clicking on a simple navigation link in the navbar to change the URL from http://localhost:5000/api/teams/topspending/Team1 to http://localhost:5000/api/teams/topspending/Team2 I am wishing that the SAME component will re-render after fetching NEW data with the UPDATED URL.
I have tried many things... and I can get the component to update after every URL change, BUT the data fetched is still the OLD data!
(I am using React-Router to route the single component to this link)
Ok, I think there are 2 little issues in your code.
Inside the parent function
This is my main function that is going to use your custom hook. If you see, I don't use interpolation because it is not going to be detected by your custom hook. That is why your initialUrl variable (Your URL) in your custom hook never change.
const App = () => {
const [id, setId] = React.useState(1);
const response = useDataApi(
'https://jsonplaceholder.typicode.com/posts/' + id,
[],
id,
);
return (
<>
<div>My id {id}</div>
<button onClick={() => setId(id + 1)}>Click Me!</button>
</>
);
};
Inside the custom hook
It seems to me that you are misunderstanding the setState function provided by react.
Remember that every time you call the setState function is not synchronous. I mean, if you use setUrl(initialUrl), then in the next line of code your state variable url will not necessarily have the values already updated. To know more about it, you can read this: https://reactjs.org/docs/faq-state.html#when-is-setstate-asynchronous
I would suggest using another variable to call the correct URL and change the variable names of your custom hook. I added some comments to your code //Note:
export const useDataApi = (initialUrl, initialData, effect) => {
console.log("start/top Data API", effect, initialUrl);
const [items, setItems] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
// Note: This not sync function
setUrl(initialUrl);
const abortCtrl = new AbortController();
const opts = { signal: abortCtrl.signal };
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
console.log("data loading");
try {
console.log(url, "this should be the new Url");
// Note: I changed this url variable of your state, to use your initialUrl variable. (this initialUrl parameter should have your UPDATED URL)
const result = await axios(initialUrl, opts);
setItems(result.data);
} catch (error) {
setIsError(true);
}
console.log("data loaded...");
setIsLoading(false);
};
fetchData();
return () => abortCtrl.abort();
}, [effect]);
return [{ items, isLoading, isError }];
};
I Hope, this can help you!.
setState is asynchronous, so there's no guarantee as to when it will be affected before the next render. There's multiple ways to rewrite your code to work more predictably, but with the examples you've provided the easiest solution is to remove the url state altogether and just use initalUrl in your call to axios.
This isn't great.
So another option would be to keep your url state and add a second useEffect that watches url.
eg.
const useDataApi = (initialUrl, initialData, effect) => {
console.log("start/top Data API");
const [items, setItems] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
setUrl(initialUrl);
}, [effect]);
useEffect(() => {
const abortCtrl = new AbortController();
const opts = { signal: abortCtrl.signal };
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
console.log("data loading");
try {
console.log(url, "this should be the new Url");
const result = await axios(url, opts);
setItems(result.data.data);
} catch (error) {
setIsError(true);
}
console.log("data loaded...");
setIsLoading(false);
};
fetchData();
return () => abortCtrl.abort();
}, [url])
return [{ items, isLoading, isError }];
};
export default useDataApi;
Still not great, but does what you're trying to do?

Is there any way where we can call a custom hook inside useeffect in react

const useApiCall = (asyncFunction, body) => {
const [fetching, setFetching] = useState(false);
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [cancel, setCancel] = useState(null);
const [fetched, setFetched] = useState(false);
const execute = useCallback(async cancelPromise => {
try {
setFetching(true);
setResponse(null);
setError(null);
setFetched(false);
setCancel(() => {
return cancelPromise.cancel;
});
let result = await cancelPromise.promise;
setResponse(result);
} catch (ex) {
setError(ex);
} finally {
setFetching(false);
setFetched(true);
}
}, []);
useEffect(() => {
const cancelPromise = body
? makeCancelable(asyncFunction(body))
: makeCancelable(asyncFunction());
execute(cancelPromise);
return () => cancelPromise.cancel();
}, [asyncFunction, body, execute]);
return { fetching, response, error, fetched, cancel };
};
As this hook returns 4 states of an api call i would like to use this hook inside another hook but conditionally.
should I create another parameter that will call the api conditionally?
for example accept another parameter say shouldCall api (bool)
and add a condition above the execute method mentioned in the useEffect?

React higher order function to return hook

Currently, I have a custom fetching data hook written in javascript and it works
import {useState, useEffect} from 'react';
const useApi = apiName => id => {
const [response, setResponse] = useState();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const fetching = async () => {
setLoading(true);
const data = await fetch(`/api/${apiName}${id ? `/${id}` : ""}`)
.then((x) => x.json())
.catch((error) => setError(error));
setResponse(data);
setLoading(false);
};
useEffect(() => {
fetching();
}, [id]);
return { response, loading, error };
};
Then I can use pass in what api I want to call to get the hook. For examples:
const useCustomer = useApi("customer")
const useHello = useApi("hello")
.....
const {response, loading, error} = useCustomer("id_1")
It works fine.
Then, I try to convert to typescript
const useApi = (apiName:string) => (id?:string) => {
const [response, setResponse] = useState({})
.......
}
and eslint complains that
React Hook "useState" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
I would like to know whats wrong with this approach, I know I can have something like:
const useApi = (apiName:string, id?:string) => {}
or disable the eslint(react-hooks/rules-of-hooks)
But just curious about whats the potential problems having higher order function of hook since it actually return the response .
Thanks
When you name you function with prefix hooks, eslint thinks of it as a custom hook according to the general convention. Now that implements useState in a nested function which is why it gives you an error
The best way to write the above code is to not use currying function but pass in the apiName as a param directly
const useApi = (apiName, id) => {
const [response, setResponse] = useState();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const fetching = async () => {
setLoading(true);
const data = await fetch(`/api/${apiName}${id ? `/${id}` : ""}`)
.then((x) => x.json())
.catch((error) => setError(error));
setResponse(data);
setLoading(false);
};
useEffect(() => {
fetching();
}, [id]);
return { response, loading, error };
};
and use it like
.....
const {response, loading, error} = useApi("customer","id_1");
P.S. Hooks are meant to be an alternative to HOC's and there is no point writing a hook if you use it as an HOC itself
There's a much easier way if you don't need the id variable to be in the hook. The reason why you get the warning is because your hooks are in your CB instead of your root function.
Correct Example:
const useApi = (apiName:string) => {
const [response, setResponse] = useState({});
return (id?: string) => {
.......
};
}

How can I return only the set-State from my fetched json data?

im using this Api to get json data.
const FetchEarthquakeData = url => {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(jsonData => setData(jsonData.features))
}, [url]);
return data;
};
The problem is when I use this function like this:
const jsonData = FetchEarthquakeData(url)
console.log(jsonData);
I get following console.logs:
null
Array(17)
So my function FetchEarthquakeData returns the null variable and! the desired api. However if I want to map() over the jsonData, the null value gets mapped. How can I refactor my code so I get only the Array?
I'm not quite sure what useState() and setData() do. But in order to fetch the json data from the API, you can make the function as followed, then you can perform operations on the fetched data.
const url = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_day.geojson"
const FetchEarthquakeData = url => {
return new Promise(resolve => {
fetch(url)
.then(res => res.json())
.then(jsonData => resolve(jsonData.features))
})
}
FetchEarthquakeData(url).then(features => {
console.log(features)
// Do your map() here
})
as per the documentation of react hooks ,Only Call Hooks from React Functions Don’t call Hooks from regular JavaScript functions.
React Function Components -- which are basically just JavaScript Functions being React Components which return JSX (React's Syntax.)
for your requirement you can do as follows in your react component.
idea is to have state hook initialized with empty array then update it once json data available. the fetch logic can be moved inside useeffect .
const SampleComponent=()=>{
const [data,setData] = useState([])
useeffect (){
fetch(url).then((responsedata)=>setData(responseData) ,err=>console.log (err)
}
return (
<>
{
data.map((value,index)=>
<div key=index>{value}<div>
}
</>
)
}
if you find above fetching pattern repetitive in your app thou can use a custom hook for the same.
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
}, []);
return { response, error };
};
in your React component you can use like
const {data,error} = useFetch(url , options)
You have to do it in an async fashion in order to achieve what you need.
I recommend you doing it separately. In case you need the data to load when the component mounts.
Another thing is that your function is a bit confusing.
Let's assume some things:
const Example = () => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const result = await fetch(url);
setData(result.features);
};
fetchData();
}, []);
return (
<div>
{console.log(data)}
{data && <p>{JSON.stringify(data, null, 2)}</p>}
</div>
);
};
I am sure that the way you are doing it is storing the data properly on data, but you can not see it on your console.log. It is a bit tricky.
You can read a bit more here => https://medium.com/#wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3
Adding a bit more of complexity in case you want to handle different states like loading and error.
const Example = () => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await fetch(url);
setData(result.features);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, []);
return (
<div>
{console.log(data)}
{data && <p>{JSON.stringify(data, null, 2)}</p>}
</div>
);
};

Categories

Resources