I am facing a problem with custom hook. I can't access states in the function. For example: data, error, loading
It is showing an error: "loading is not defined". I know that variables is out of scope but I want to use loading, error.
export const useLikeTrack = track => {
const { addFavoriteTrack } = useTrackContext();
const [success, setSuccess] = useState(false)
const likeTrack = (params) => {
const { data, error, loading } = useAxios({
axiosInstance: myApiAxiosInstance,
url: `tracks/${track["id"]}/likes`,
method: "POST"
});
}
useEffect(() => {
if (!loading && data) {
addFavoriteTrack(track);
setSuccess(true);
}
}, [loading, data]);
return { loading, error, success, likeTrack };
};
export default function TrackItem({ track }) {
const {success, loading, error, likeTrack} = useLikeTrack(track.id)
return (
<div className="flex">
<button className="" onClick={likeTrack}>Like
</button>
</div>
);
}
Can you help me fix it ? I am using useAxios from this: https://github.com/angelle-sw/use-axios-client
if (!loading && data) {
There is no loading nor data declared in that scope. They are inside another function called likeTrack()
To be more specific:
const likeTrack = (params) => {
const { data, error, loading } = useAxios({
// ...
}
}
those vars (data, error, loading) are not accessible outside of that function
As #Aprillion suggested in the comments, you should use the normal axios package to create a request in a handler, like so:
import axios from "axios";
...
const [loading, setLoading] = useState(false);
const [data, setData] = useState();
const likeTrack = (params) => {
setLoading(true);
myApiAxiosInstance.post(`tracks/${track["id"]}/likes`)
.then(response => {
setLoading(false);
setData(response.data)
})
}
...
However, if you really want to use a hook, try out the axios-hooks library instead. Beside the fact that it has way more recent npm-downloads than your package, you can call an axios request manually which is basically what you need here.
First, install the library with npm install axios axios-hooks
Then adjust your code like so:
import axios from "axios";
...
const [{ data, error, loading }, execute] = useAxios({
axiosInstance: myApiAxiosInstance,
url: `tracks/${track["id"]}/likes`,
method: "POST"
},
{
manual: true // This is important, otherwise your request would be fired automatically after your component mounted
});
const likeTrack = (params) => {
execute(); // execute the request manually
}
...
Note how I wrapped the useAxios return value with an array and added execute at the end. With execute() you can trigger the request manually. Also, don't forget to configure your request to only fire manually, as in the example.
source: https://github.com/simoneb/axios-hooks#example
It looks like you're passing track.id instead of track to your custom hook.
Related
I'm trying to use a hook inside of a useEffect call to run only once (and load some data).
I keep getting the error that I can't do that (even though I've done the exact same thing in another app, not sure why 1 works and the other doesn't), and I understand I may be breaking the Rules of Hooks... so, what do I do instead? My goal was to offload all the CRUD operation logic into a simple hook.
Here's MenuItem, the component trying to use the hook to get the data.
const MenuItem = () => {
const [ID, setID] = useState<number | null>(null);
const [menu, setMenu] = useState<Item[]>([]);
const { getMenu, retrievedData } = useMenu();
//gets menu items using menu-hook
useEffect(() => {
getMenu();
}, []);
//if menu is retrieved, setMenu to retrieved data
useEffect(() => {
if (retrievedData.length) setMenu(retrievedData);
}, []);
//onClick of menu item, displays menu item description
const itemHandler = (item: Item) => {
if (ID === null || ID !== item._id) {
setID(item._id);
} else {
setID(null);
}
};
return ...
};
And here's getMenu, the custom hook that handles the logic and data retrieval.
const useMenu = () => {
const backendURL: string = 'https://localhost:3001/api/menu';
const [retrievedData, setRetrievedData] = useState<Item[]>([]);
const getMenu = async () => {
await axios
.get(backendURL)
.then((fetchedData) => {
setRetrievedData(fetchedData.data.menu);
})
.catch((error: Error) => {
console.log(error);
setRetrievedData([]);
});
};
return { getMenu, retrievedData };
};
export default useMenu;
And finally here's the error.
Invalid hook call. Hooks can only be called inside of the body of a function component.
I'd like to add I'm also using Typescript which isn't complaining right now.
There's a few things you can do to improve this code, which might help in future. You're right that you're breaking the rule of hooks, but there's no need to! If you move the fetch out of the hook (there's no need to redefine it on every render) then it's valid not to have it in the deps array because it's a constant.
I'd also make your useMenu hook take care of all the details of loading / returning the loaded value for you.
const fetchMenu = async () => {
const backendURL: string = 'https://localhost:3001/api/menu';
try {
const { data } = await axios.get(backendURL);
return data.menu;
} catch (error: AxiosError) {
console.log(error);
return [];
};
}
export const useMenu = () => {
const [items, setItems] = useState<Item[]>([]);
useEffect(() => {
fetchMenu.then(result => setItems(result);
}, []);
return items;
};
Now you can consume your hook:
const MenuItem = () => {
const [ID, setID] = useState<number | null>(null);
// Now this will automatically be an empty array while loading, and
// the actual menu items once loaded.
const menu = useMenu();
// --- 8< ---
return ...
};
A couple of other things -
Try to avoid default exports, because default exports are terrible.
There are a lot of packages you can use to make your life easier here! react-query is a good one to look at as it will manage all the lifecycle/state management around external data
Alternatively, check out react-use, a collection of custom hooks that help deal with lots of common situations like this one. You could use the useAsync hook to simplify your useMenu hook above:
const backendURL: string = 'https://localhost:3001/api/menu';
const useMenu = () => useAsync(async () => {
const { data } = await axios.get(backendURL);
return data.menu;
});
And now to consume that hook:
const MenuItem = () => {
const { value: menu, loading, error } = useMenu();
if (loading) {
return <LoadingIndicator />;
}
if (error) {
return <>The menu could not be loaded</>;
}
return ...
};
As well as being able to display a loading indicator while the hook is fetching, useAsync will not give you a memory leak warning if your component unmounts before the async function has finished loading (which the code above does not handle).
After working on this project for some time I've also found another solution that is clean and I believe doesn't break the rule of hooks. This requires me to set up a custom http hook that uses a sendRequest function to handle app wide requests. Let me make this clear, THIS IS NOT A SIMPLE SOLUTION, I am indeed adding complexity, but I believe it helps since I'll be making multiple different kinds of requests in the app.
This is the sendRequest function. Note the useCallback hook to prevent unnecessary rerenders
const sendRequest = useCallback(
async (url: string, method = 'GET', body = null, headers = {}) => {
setIsLoading(true);
const httpAbortCtrl = new AbortController();
activeHttpRequests.current.push(httpAbortCtrl);
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal,
});
const responseData = await response.json();
activeHttpRequests.current = activeHttpRequests.current.filter(
(reqCtrl) => reqCtrl !== httpAbortCtrl
);
if (!response.ok) throw new Error(responseData.message);
setIsLoading(false);
return responseData;
} catch (error: any) {
setError(error);
setIsLoading(false);
throw error;
}
},
[]
);
Here's the new useMenu hook, note I don't need to return getMenu as every time sendRequest is used in my app, getMenu will automatically be called.
export const useMenu = () => {
const { sendRequest } = useHttpClient();
const [menu, setMenu] = useState<MenuItem[]>([]);
const [message, setMessage] = useState<string>('');
useEffect(() => {
const getMenu = async () => {
try {
const responseData = await sendRequest(`${config.api}/menu`);
setMenu(responseData.menu);
setMessage(responseData.message);
} catch (error) {}
};
getMenu();
}, [sendRequest]);
return { menu, message };
};
Good luck
Context
All of my components need to fetch data.
How I fetch
Therefore I use a custom hook which fetches the data using the useEffect hook and axios. The hook returns data or if loading or on error false. The data is an object with mostly an array of objects.
How I render
I render my data conditional with an ternary (?) or the use of the short circuit (&&) operator.
Question
How can I destructure my data dependent if my useFetch hook is returning false or the data in a way i can reuse the logic or an minimal implementation to the receiving component?
What I have tried
moving the destructuring assignment into an if statement like in return. Issue: "undefined" errors => data was not available yet
moving attempt 1 to function. Issue: function does not return variables (return statement does not work either)
//issue
fuction Receiver() {
const query = headerQuery();
const data = useFetch(query);
const loaded = data.data //either ```false``` or object with ```data```
/*
The following part should be in an easy condition or passed to an combined logic but I just dont get it
destructuring assignment varies from component to component
ex:
const {
site,
data: {
subMenu: {
description,
article_galleries,
image: {
caption,
image: [{url}],
},
},
},
} = data;
*/
return loaded?(
<RichLink
title={title}
text={teaserForText}
link={link}
key={id}
></RichLink>):<Loading />
(
//for context
import axios from "axios";
import {
useHistory
} from "react-router-dom";
import {
useEffect,
useState
} from "react";
function useFetch(query) {
const [data, setData] = useState(false);
const [site, setSite] = useState(""); // = title
const history = useHistory();
useEffect(() => {
axios({
url: "http://localhost:1337/graphql",
method: "post",
data: {
query: query,
},
})
.then((res) => {
const result = res.data.data;
setData(result);
if (result === null) {
history.push("/Error404");
}
setSite(Object.keys(result)[0]);
})
.catch((error) => {
console.log(error, "error");
history.push("/Error");
});
}, [query, history, setData, setSite]);
return {
data: data,
site: site
};
}
export default useFetch;
)
You can return the error, data and your loading states from your hook. Then the component implementing the hooks can destructure all of these and do things depending upon the result. Example:
const useAsync = () => {
// I prefer status to be idle, pending, resolved and rejected.
// where pending status is loading.
const [status, setStatus] = useState('idle')
const [data, setData] = useState([])
const [error, setError] = useState(null)
useEffect(() => {
setStatus('pending')
axios.get('/').then(resp => {
setStatus('resolved')
setData(resp.data)
}).catch(err => {
setStatus('rejected') // you can handle error boundary
setError(err)
})
}, []}
return {status, data, error}
}
Component implementing this hook
const App = () => {
const {data, status, error} = useAsync()
if(status === 'idle'){
// do something
}else if(status === 'pending'){
return <Loader />
}else if(status === 'resolved'){
return <YourComponent data ={data} />
}else{
return <div role='alert'>something went wrong {error.message}</div>
}
}
the hooks can be enhanced more with the use of dynamic api functions.
My fetch hook:
import { useEffect, useState } from "react";
export const useOurApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [fetchedData, setFetchedData] = useState(initialData);
useEffect(() => {
let unmounted = false;
const handleFetchResponse = response => {
if (unmounted) return initialData;
setHasError(!response.ok);
setIsLoading(false);
return response.ok && response.json ? response.json() : initialData;
};
const fetchData = () => {
setIsLoading(true);
return fetch(url, { credentials: 'include' })
.then(handleFetchResponse)
.catch(handleFetchResponse);
};
if (initialUrl && !unmounted)
fetchData().then(data => !unmounted && setFetchedData(data));
return () => {
unmounted = true;
};
}, [url]);
return { isLoading, hasError, setUrl, data: fetchedData };
};
I call this hook in a function like so:
//states
const [assignments, setAssignments] = useState([])
const [submissions, setSubmissions] = useState([])
const [bulk_edit, setBulk_edit] = useState(false)
const [ENDPOINT_URL, set_ENDPOINT_URL] = useState('https://jsonplaceholder.typicode.com/comments?postId=1')
let url = 'https://jsonplaceholder.typicode.com/comments?postId=1';
const { data, isLoading, hasError } = useOurApi(ENDPOINT_URL, []);
My question is how can I call this instance of userOurAPI with a different URL. I have tried calling it within a function where I need it but we can't call hooks within functions, so I am not sure how to pass it new url to get new data. I don't want to have many instances of userOurAPI because that is not DRY. Or is this not possible? New to hooks, so go easy on me!
In order to change the URL such that the component updates and fetches new data, you create a set function that changes the URL and you make sure that the useEffect() is run again on the change of URL. Return your setter function for URL so that you can use it outside of the first instance of your hook. In my code, you will see that I return a setUrl, I can use that to update fetch! Silly of me not to notice, but hopefully this will help someone.
You could do it the way you chose to, but there are other ways of working around such a problem.
One other way would be to always re-fetch whenever the URL changes, without an explicit setter returned from the hook. This would look something like this:
export const useOurApi = (url, initialData) => { // URL passed directly through removes the need for a specific internal url `useState`
// const [url, setUrl] = useState(initialUrl); // No longer used
// ...
useEffect(() => {
// Handle fetch
}, [url]);
return { isLoading, hasError, data: fetchedData }; // No more `setUrl`
};
This may not always be what you want though, sometimes you may not want to re-fetch all the data on every url change, for example if the URL is empty, you may not want to update the url. In that case you could just add a useEffect to the useOurApi custom hook to update the internal url and re-fetch:
export const useOurApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
// ...
useEffect(() => {
// Handle fetch
}, [url]);
useEffect(() => {
// ... do some permutation to the URL or validate it
setUrl(initialUrl);
}, [initialUrl]);
return { isLoading, hasError, data: fetchedData }; // No more `setUrl`
};
If you still sometimes want to re-fetch the data, unrelated to the URL, you could output some function from the hook to trigger the data fetching. Something like this:
export const useOurApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [fetchedData, setFetchedData] = useState(initialData);
const refetch = useCallback(() => {
// Your fetch logic
}, [url]);
useEffect(() => {
refetch(); // In case you still want to automatically refetch the data on url change
}, [url]);
return { isLoading, hasError, refetch, data: fetchedData };
};
Now you can call refetch whenever you want to trigger the re-fetching. You may still want to be able to internally change the url, but this gives you another a bit more flexible access to the fetching and when it occurs.
you confuse the difference between simple function and function component
Function Component are not just simple function. It means that the have to return a component or a html tag
I think you should turn four function to simple function like so
export const useOurApi = (initialUrl, initialData) => {
let url = initialUrl, fetchedData = initialData,
isLoading= true, hasError = false, unmounted = false;
const handleFetchResponse = response => {
if (unmounted) return initialData;
hasError = !response.ok;
isLoading = false;
return response.ok && response.json ? response.json() : initialData;
};
const fetchData = () => {
isLoading = true;
return fetch(url, { credentials: 'include' })
.then(handleFetchResponse)
.catch(handleFetchResponse);
};
if (initialUrl && !unmounted)
fetchData().then(data => {
if(!unmounted) fetchedData =data;
unmounted = true;
});
return { isLoading, hasError, url, data: fetchedData };
};
I'm using axios-hooks in my react project. I have a problem that whenever I re-render the component, the backend is called and at the beginning, the same endpoint is called twice.
I'm using it in a following way:
import useMyHook from '../../hooks/useMyHook ';
export default function MyComponent() {
const { getData } = useMyHook (category);
...
<Button onClick={getData}...
}
**getData is called to refresh the data (so it's normal that the backend is called again here)
export default function useMyHook(category) {
const { language, contextData, dispatch } = useAppContext();
const config = {... url, headers, params ...};
const opts = { manual: false };
const [{ data: myData, loading, error }, reFetch] = useAxios(config, opts);
useEffect(() => {
if (_.isEmpty(contextData) && !_.isEmpty(myData)) {
dispatch({ type: DATA_LOADED, payload: myData});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [myData]);
const getData = () => {
dispatch({ type: DATA_RESET});
reFetch();
};
return { loading, error, getData};
}
Is there something wrong with my implementation?
PS. I've seen that useAxios has
useAxios(){...}, [stringifiedConfig]) and stringifiedConfig=JSON.stringify(config)
and in my understanding, it shouldn't re-call the backend if the config doesn't change.
Basically, the problem is because of state dispatching.
Whenever we change the state, the component that is calling axios-hooks is unmounted and mounted again so we do a second call.
The workaround is to check if the value is not undefined inside useEffect of the component and then call the axios-hook and disable the automatic call {manual: true} in useAxios.
I'm using a custom hook to get pull some data in from an API for use across a set of React function components. However, esLint throws up a lovely warning:
React Hook useEffect has a missing dependency: 'fetchFromAPI'. Either
include it or remove the dependency array.
I didn't think it's a dependency, as it's inside useFetch() itself. I need to do it as I'm using await. What am I doing wrong? Is it ok to just turn off the warning for this line? Or is there a more canonical syntax I should be using?
function useFetch (url) {
const [data, setData] = useState(null);
async function fetchFromAPI() {
const json = await( await fetch(url) ).json();
setData(json);
}
useEffect(() => {fetchFromAPI()},[url]);
return data;
};
export {
useFetch
};
I suggest you to define async function inside useEffect itself:
function useFetch (url) {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchFromAPI() {
const json = await( await fetch(url) ).json();
setData(json);
}
fetchFromAPI()
},[url]);
return data;
};
You can take a look at valid example from doc faqs which uses async function inside useEffect itself:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// By moving this function inside the effect,
// we can clearly see the values it uses.
async function fetchProduct() {
const response = await fetch('http://myapi/product' + productId);
const json = await response.json();
setProduct(json);
}
fetchProduct();
}, [productId]); // ✅ Valid because our effect only uses productId
// ...
}
Declare it outside your custom effect passing url as parameter and return the json to be setted inside useEffect
async function fetchFromAPI(url) {
const json = await( await fetch(url) ).json();
return json
}
function useFetch (url) {
const [data, setData] = useState(null);
useEffect(() => {
setData(fetchFromAPI(url))
},[url]);
return data;
};
Or directly inside useEffect
function useFetch (url) {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchFromAPI() {
const json = await( await fetch(url) ).json();
return json
}
setData(fetchFromAPI())
},[url]);
return data;
};
Just move your function inside the useEffect, and everything will be fine:
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchFromAPI() {
const json = await (await fetch(url)).json();
setData(json);
}
fetchFromAPI();
}, [url]);
return data;
}
export { useFetch };
https://codesandbox.io/s/clever-cdn-8g0me
async function fetchFromAPI(url) {
return ( await fetch(url) ).json();
}
function useFetch (url) {
const [data, setData] = useState(null);
useEffect(() => {
fetchFromAPI(url).then(setData);
}, [url, setData, fetchFromAPI]);
return data;
};
export {
useFetch
};
You can change a little and then extract fetchFromAPI, for not being created every time useFetch calls, also it's good for single responsibility.
If you understand this code very well of course you can either turn off linting for this current line or you can add the rest setData and fetchFromAPI params. And exactly in this order. Because on re-render params comparison start from first param to last, and it's better place most changed param in first place, for not checking not changed param every time the next one is changed, so if first changed, useEffect don't need to check the others and call passed function earlier