How do I fix "useEffect has a missing dependency" in custom hook - reactjs

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

Related

Invalid Hook Call for custom hook

I have written a function for API calls. I want to reuse this function from a different page.
FetchData.js
export const FetchData = (url, query, variable) => {
const [fetchData, setFetchData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const queryResult = await axios.post(
url, {
query: query,
variables: variable
}
)
const result = queryResult.data.data;
setFetchData(result.mydata)
};
fetchData();
})
return {fetchData, setFetchData}
}
Here is my main page from where I am trying to call the API using the following code
mainPage.js
import { FetchData } from './FetchData'
export const MainPage = props => {
const onClick = (event) => {
const {fetchData, setFetchData} = FetchData(url, query, variable)
console.log(fetchData)
}
}
It is returning the following error -
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
If you need to fetch data on response to an event, you don't need a useEffect.
const useData = (url, query, variable) => {
const [data, setData] = useState([]);
const fetchData = async () => {
const queryResult = await axios.post(url, {
query: query,
variables: variable,
});
setData(queryResult.data.data);
};
return {data, fetchData}
};
export const MainPage = (props) => {
const {data, fetchData} = useData(url, query, variable);
const onClick = (event) => {
fetchData()
};
};
Hooks can't be used inside handler functions.
Do this instead:
import { FetchData } from './FetchData'
export const MainPage = props => {
const {fetchData, setFetchData} = FetchData(url, query, variable)
const onClick = (event) => {
console.log(fetchData)
}
}

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

Calling a custom hook from an onClick by passing it arguments

I created a custom hook to make my api calls with axios.
When I call this hook passing it different parameters, it returns 3 states.
I created a page with a form.
When I submit this form I call a function "onSubmitform"
I would like to be able to execute this custom hook in this function.
How can I do ?
Maybe a custom hook is not suitable in this case?
-- file useAxios.js --
import { useState, useEffect } from "react";
import axios from "axios";
const useAxios = (axiosParams) => {
const [response, setResponse] = useState(undefined);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const handleData = async (params) => {
try {
const result = await axios.request(params);
setResponse(result.data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
handleData(axiosParams);
}, []);
return { response, error, loading };
};
export default useAxios;
-- file Page.js --
import useAxios from "../hooks/useAxios";
function Page() {
const { response } = useAxios();
const onSubmitForm = () => {
// Here I want to call the custom hook by passing it different parameters.
}
}
You can add an option to execute the request manually and avoid the fetch on mount:
const useAxios = (axiosParams, executeOnMount = true) => {
const [response, setResponse] = useState(undefined);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const handleData = async (params) => {
try {
const result = await axios.request(params);
setResponse(result.data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (executeOnMount) handleData(axiosParams);
}, []);
return { response, error, loading, execute: handleData };
};
Then use it:
const { response, execute } = useAxios(undefined, false);
const onSubmitForm = (data) => {
execute(params) // handleData params
}
A hook is a function (returning something or not) which should be called only when the components (re)renders.
Here you want to use it inside a callback responding to an event, which is not the same thing as the component's render.
Maybe you are just looking for a separate, "simple", function? (for example something similar to what you have in your "useEffect")

Should I call a function that returns a promise immediately after setState or in the dependency array of useEffect()

My code (which seems to work ok) looks like this:
import { SplashScreen } from "#capacitor/splash-screen";
const MyComponent = () => {
const [data, setData] = useState()
useEffect(() => {
init()
}, [])
const init = async () => {
const response = await fetch("some_api")
setData(response.data)
await SplashScreen.hide()
}
return (<div>{JSON.stringify(data)}<div>)
}
But I'm wondering if it's better practive to move the await SplashScreen.hide() function call to a useEffect() with the data in the dependency array like this:
import { SplashScreen } from "#capacitor/splash-screen";
const MyComponent = () => {
const [data, setData] = useState()
useEffect(() => {
init()
}, [])
useEffect(() => {
if (data) {
await SplashScreen.hide()
}
}, [data])
const init = async () => {
const response = await fetch("some_api")
setData(response.data)
}
return (<div>{JSON.stringify(data)}<div>)
}
Note: SplashScreen.hide() returns a promise. Which way is better and why?
It depends if you want to call SplashScreen.hide() after the data has been set or not. In the first case it's not guaranteed that the call will be made after the data is set since setState works async. In the second case though, you are explicitly calling SplashScreen.hide() after the state has been updated.
Note, since you're not doing anything with the promise returned from SplashScreen.hide(), it's not necessary to call it with await.

Correct dependency array for useEffect with React hooks

I am using Create-React-App and the (excellent) use-http for a custom useFetch hook. The goal is to make several API calls upon login to an account area:
const [user, setUser] = useState(null)
const [profile, setProfile] = useState(null)
const [posts, setPosts] = useState(null)
const request = useFetch('/')
const initializeAccount = async () => {
try {
const user = await request.get('api/user/')
const profile = await request.get('api/profile/')
const posts = await request.get('api/posts/')
if (user) {
setUser(user.data)
}
if (profile) {
setProfile(profile.data)
}
if (posts) {
setPosts(posts.data)
}
} catch (e) {
console.log('could not initialize account')
}
}
useEffect(() => {
initializeAccount()
return () => console.log('unmount')
})
I have tried using [] as the dependency array, but I get a linting error saying to move initializeAccount to the dependency array. If I add it, the function runs endlessly.
What is the correct way to setup the dependency array so that this function is called one time? Also, what would be the correct way to handle abort of each of the API calls in this scenario?
My man, in order to run useEffect once for api calls, you have to do it like this:
useEffect(() => {
initializeAccount()
return () => console.log('unmount')
},[])
Hope it helps.

Resources