Invalid Hook Call for custom hook - reactjs

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

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

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

How to write React hooks with called functions?

I have a useQuery() hook:
const useQuery = () => {
const router = useRouter();
const build = (query) => {}
const read = () => {}
}
export default useQuery;
I want to be able to use hook in my code like:
const query = useQuery();
useEffect(()=>{
const oldQuery = query.read();
if(oldQuery.length === 0) query.build(newQuery);
},[])
but every time I try to call query.read() I get error Property 'read' does not exist on type 'void'. It looks like scope issue. How can I fix it?
You need a return statement in useQuery:
const useQuery = () => {
const router = useRouter();
const build = (query) => {}
const read = () => {}
return { router, build, read }
}
You may also want to consider memoizing these functions, so that code which consumes this hook doesn't do unnecessary work because it thinks the functions have changed:
const build = useCallback((query) => {}, []);
const read = useCallback((() => {}, []);

NextJS / next-translate : get lang outside of components

We have a short question for our application (NextJS 11.0.0 + next-translate 1.0.7)
The library contains a function to make an API call (/lib/mylib.js) :
export const getDataExample = async (lang) => {
return fetch(_apiurl_/example/{lang});
};
And my component in react (/components/myComponent.js) call this function with a useEffect:
import { useEffect, useState } from 'react';
import useTranslation from 'next-translate/useTranslation';
import { getDataExample } from '/lib/mylib';
export default function MyComponent() {
const [data, setData] = useState(false);
const { lang } = useTranslation();
useEffect(() => {
const fetchData = async () => {
const response = await getDataExample(lang);
setData(response);
};
fetchData();
}, []);
[...]
}
I don't want to call getDataExample() directly with the lang parameter.
Is it possible to get the current language in the function (/lib/mylib.js) ?
Thank you for your reply !
But now imagine that my library (/lib/mylib.js) is also used to fetch data into a getServerSideProps :
export async function getServerSideProps({ locale }) {
const response = await getDataExample(locale);
[...]
}
React Hooks are not available here, so what do you do ?
You can create your custom hook. This is an example:
const useFetchWithLang = (func) => {
const { lang } = useTranslation()
return useCallback((args) => func({ ...args, lang }), [lang])
}
const fetchDataExample = ({ otherParam, lang }) => {
return { test: 'test1' }
}
const fetchDataExampleWithLang = useFetchWithLang(fetchDataExample)
After for example, you could use it in a useEffect.
useEffect(() => {
const fetchData = async () => {
const response = await fetchDataExampleWithLang({ otherParam: 'test' });
setData(response);
};
fetchData();
}, []);

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

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

Resources