Invalid Hook Call when calling a custom hook - reactjs

I've been trying to solve a problem for 4 days and I've searched and searched a lot! I'm not sure if I'm just searching for the wrong thing or seeing it and am just completely oblivious to it. My problem is that when I call the hook from the main function, it works, but when I call it via an onSubmit function, it fails for invalid hook call. I understand the concept of why the hook would be invalid, but am just completely unsure how to resolve it. I reformatted my hook so that I could initialize it earlier in my code and then call it via the onSubmit, but the problem is that the state token only gets updated on initialization and not whenever it's changed. Thus leading to some API calls with a bad token. Any Help Anyone can offer will be greatly appreciated!
The Setup:
NextJS, React-Form, Redux via Next-Redux-Wrapper, SWR
When ReactForm validates the form, I'd like for it to submit it's data to a custom hook. However, that custom hook fails with the Invalid Hook Call. It does it as soon as I defined a variable for useState.
-- Form Code ---
const { register, errors, handleSubmit, setValue, reset, control } = useForm()
<form onSubmit={ handleSubmit(onSubmit) }>
-- onSubmit Code ---
const onSubmit = (data) => {
const newData = useApiPost(`users/update/${ id }`, submitData)
}
-- Custom Hook --
function useApiPut(resource, params){
const [ data, setData ] = useState(null)
const [ error, setError ] = useState(null)
const { url, data : inData } = rest(resource, params)
//Post data
const { data : resData, error : resError, mutate } = useSWR(
state.user.token ? url : null,
url => axios.request({
url,
method : 'post',
data : inData,
headers : { Authorization : `Bearer ${ state.user.token }` }
})
.then(({ data }) => data),
{ revalidateOnFocus : false }
)
useEffect(() => {
if(resData) {
setData(resData)
}
if(resError) {
setError(resError)
}
}, [ resData, resError ])
return { data, error, mutate }
}

So the answer to this ended up being a few different things.
First, accessing my store value via store.getState() resolved one issue with hooks.
Secondly, moving away from SWR. It is an AWESOME wrapper and capable of a TON, no doubt. I just kept struggling with it and finally gave up. I moved to using Axios Interceptors and it has been amazing. Axios doesn't cache (out of the box) like SWR, but I was utilizing the caching for tokens, so I wasn't really using it to it's full potential. Please note, I'm in no way bashing SWR, it's a really great and extremely powerful library.
Lastly, wrapping the entire set of hooks into one function allowed me to initialize the function at the root of my page and then access the hooks I needed to use later.
So instead of just calling useApiPost() now my hook looks like this:
const useApi = async() => {
const post = async() => {
//Do work here
}
return { post }
}
export default useApi
Then it's called like this inside the main function:
const { post } = useApi()
And it's called like this in the onSubmit Function:
const options = await post(resource, params)

Related

RTK Query response state

I'm trying to convert some Axio code to RTK query and having some trouble. The 'data' response from RTK query doesn't seem to act like useState as I thought.
Original axio code:
const [ importantData, setImportantData ] = useState('');
useEffect(() => {
async function axiosCallToFetchData() {
const response = await axiosAPI.post('/endpoint', { payload });
const { importantData } = await response.data;
setImportantData(importantData);
}
axiosCallToFetchData()
.then((res) => res)
.catch((error) => console.log(error));
}, []);
const objectThatNeedsData.data = importantData;
New RTK Query code
const { data, isSuccess } = useGetImportantDataQuery({ payload });
if(isSuccess){
setImportantData(data.importantData);
}
const objectThatNeedsData.data = importantData;
This however is giving me an infinite render loop. Also if I try to treat the 'data' object as a state object and just throw it into my component as:
const objectThatNeedsData.data = data.importantData;
Then I get an undefined error because it's trying to load the importantData before it's completed. I feel like this should be a simple fix but I'm getting stuck. I've gone through the docs but most examples just use the if statement block to check the status. The API calls are being made atleast with RTK and getting proper responses. Any advice?
Your first problem is that you always call setImportantData during render, without checking if it is necessary - and that will always cause a rerender. If you want to do that you need to check if it is even necessary:
if(isSuccess && importantData != data.importantData){
setImportantData(data.importantData);
}
But as you noticed, that is actually not necessary - there is hardly ever any need to copy stuff into local state when you already have access to it in your component.
But if accessing data.importantData, you need to check if data is there in the first place - you forgot to check for isSuccess here.
if (isSuccess) {
objectThatNeedsData.data = data.importantData;
}
All that said, if objectThatNeedsData is not a new local variable that you are declaring during this render, you probably should not just modify that during the render in general.

react-query: useQuery returns undefined and component does not rerender

I'm playing around with reactQuery in a little demo app you can see in this repo. The app calls this mock API.
I'm stuck on a an issue where I'm using the useQuery hook to call this function in a product API file:
export const getAllProducts = async (): Promise<Product[]> => {
const productEndPoint = 'http://localhost:5000/api/product';
const { data } = await axios.get(productEndPoint);
return data as Array<Product>;
};
In my ProductTable component I then call this function using:
const { data } = useQuery('products', getAllProducts);
I'm finding the call to the API does get made, and the data is returned. but the table in the grid is always empty.
If I debug I'm seeing the data object returned by useQuery is undefined.
The web request does successfully complete and I can see the data being returned in the network tab under requests in the browser.
I'm suspecting its the way the getAllProducts is structured perhaps or an async await issue but can't quite figure it out.
Can anyone suggest where IO may be going wrong please?
Simply use like this
At first data is undefined so mapping undefined data gives you a error so we have to use isLoading and if isLoading is true we wont render or map data till then and after isLoading becomes false then we can render or return data.
export const getAllProducts = async (): Promise<Product[]> => {
const productEndPoint = 'http://localhost:5000/api/product';
const res= await axios.get(productEndPoint);
return res.data as Array<Product>;
};
const { data:products , isLoading } = useQuery('products', getAllProducts);
if(isLoading){
return <FallBackView />
}
return (){
products.map(item => item)
}
I have managed to get this working. For the benefits of others ill share my learnings:
I made a few small changes starting with my api function. Changing the function to the following:
export const getAllProducts = async (): Promise<Product[]> => {
const response = await axios.get(`api/product`, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
return response.data as Product[];
};
I do not de-construct the response of the axios call but rather take the data object from it and return is as an Product[]
Then second thing I then changed was in my ProductTable component. Here I told useQuery which type of response to expect by changing the call to :
const { data } = useQuery<Product[], Error>('products', getAllProducts);
Lastly, a rookie mistake on my part: because I was using a mock api in a docker container running on localhost and calling it using http://localhost:5000/api/product I was getting the all to well known network error:
localhost has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present...
So to get around that for the purpose of this exercise I just added a property to the packages.json file: "proxy":"http://localhost:5000",
This has now successfully allowed fetching of the data as I would expect it.

React Query useQuery hooks raising an error → react-hooks/rules-of-hooks

I have a sample component as follows. I am trying to use React Query to get data from my endpoint(Graphql). I have a custom hook which I have also listed below.
const App =(props)=>{
if (me && me.token) {
let headers = {
Authorization: me.token,
}
const { loading, error, data } = useGQLQuery('getUploads', GetUploadedFiles, headers)
let tableData = []
if (data && data.uploads) {
tableData = data.uploads
}
} else {
message.info("You need to be logged in to manage your assets!")
return;
}
}
Custom Hook →
export const useGQLQuery = (key, query, variables,headers={}, config = {}) => {
let gqlClient = new GraphQLClient(endpoint)
const fetchData = async () => gqlClient.request(query, variables, headers)
return useQuery(key, fetchData, config)
}
I would like to pass the current user's token information in the header.
So far so good.
Now , whenever I am trying to load this compoent, I get the following error
React Hook "useGQLQuery" is called conditionally.
React Hooks must be called in the exact same order in every component render.
Did you accidentally call a React Hook after an early return? react-hooks/rules-of-hooks
I need to pass the current user's token which is why I do this
if (me && me.token) {
let headers = {
Authorization: me.token,
}
const { loading, error, data } = useGQLQuery('getUploads', GetUploadedFiles, headers)
but it is the point where all issue happens.
Can you please let me know how can I do , what I am trying to do here.
Any help with some examples will be a lot of help.
Thanks
You're calling a hook inside an if condition. React cannot allow that. It needs a consistent hook call on every re-render to keep track of changes.
You need to modify your useGQLQuery hook so that it internally handles your logical conditions like me && me.token.
Here's the explanation to why you can't call hooks from if conditions:
https://reactjs.org/docs/hooks-rules.html#explanation

React: Stop hook from being called every re-rendering?

Somewhat new to React and hooks in React. I have a component that calls a communications hook inside of which a call to an API is made with AXIOS and then the JSON response is fed back to the component. The issue I'm having is the component is calling the hook like six times in a row, four of which of course come back with undefined data and then another two times which returns the expected JSON (the same both of those two times).
I did a quick console.log to double check if it was indeed the component calling the hook mulitple times or it was happening inside the hook, and it is the component.
How do I go about only have the hook called only once on demand and not multiple times like it is? Here's the part in question (not including the rest of the code in the widget because it doesn't pertain):
export default function TestWidget() {
//Fetch data from communicator
console.log("called");
const getJSONData = useCommunicatorAPI('https://jsonplaceholder.typicode.com/todos/1');
//Breakdown passed data
const {lastName, alertList, warningList} = getJSONData;
return (
<h1 id="welcomeTitle">Welcome {lastName}!</h1>
);
}
export const useCommunicatorAPI = (requestAPI, requestData) => {
const [{ data, loading, error }, refetch] = useAxios('https://jsonplaceholder.typicode.com/todos/1', []);
console.log("data in Communicator:", data);
return {data};
}
I would use the useEffect hook to do this on mount and whenever any dependencies of the request change (like if the url changed).
Here is what you will want to look at for useEffect
Here is what it might look like:
const [jsonData, setJsonData] = React.useState({})
const url = ...whatver the url is
React.useEffect(() => {
const doFetch = async () => {
const jsonData = await useAxios(url, []);;
setJsonData(jsonData)
}
doFetch();
}, [url])
...use jsonData from the useState
With the above example, the fetch will happen on mount and if the url changes.
Why not just use the hook directly?
export default function TestWidget() {
const [{ data, loading, error }, refetch] =
useAxios('https://jsonplaceholder.typicode.com/todos/1', []);
return (<h1 id="welcomeTitle">Welcome {lastName}!</h1>);
}
the empty array [] makes the hook fire once when called
Try creating a function with async/await where you fetch the data.
Here can you learn about it:
https://javascript.info/async-await

React Hooks: Referencing data that is stored inside context from inside useEffect()

I have a large JSON blob stored inside my Context that I can then make references to using jsonpath (https://www.npmjs.com/package/jsonpath)
How would I go about being able to access the context from inside useEffect() without having to add my context variable as a dependency (the context is updated at other places in the application)?
export default function JsonRpc({ task, dispatch }) {
const { data } = useContext(DataContext);
const [fetchData, setFetchData] = useState(null);
useEffect(() => {
task.keys.forEach(key => {
let val = jp.query(data, key.key)[0];
jp.value(task.payload, key.result_key, val);
});
let newPayload = {
jsonrpc: "2.0",
method: "call",
params: task.payload,
id: "1"
};
const domain = process.env.REACT_APP_WF_SERVER;
let params = {};
if (task.method === "GET") {
params = newPayload;
}
const domain_params =
JSON.parse(localStorage.getItem("domain_params")) || [];
domain_params.forEach(e => {
if (e.domain === domain) {
params[e.param] = e.value;
}
});
setFetchData({ ...task, payload: newPayload, params: params });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [task]);
}
I'm gonna need to post an answer because of code, but I'm not 100% sure about what you need, so I'll build a correct answer with your feedback :)
So, my first idea is: can't you split your effects in two React.useEffect? Something like this:
export default function JsonRpc({ task, dispatch }) {
...
useEffect(() => {
...
setFetchData(...);
}, [task]);
useEffect(() => {
...
}, [data]);
..
}
Now, if my understanding are correct, this is an example of events timeline:
Due to the update on task you will trigger the first useEffect, which can setFetchData();
Due to the update on fetchData, and AXIOS call is made, which updates data (property in the context);
At this, you enter the second useEffect, where you have the updated data, but NO call to setFetchData(), thus no loop;
Then, if you wanted (but couldn't) put data in the dependencies array of your useEffect, I can imagine the two useEffect I wrote have some shared code: you can write a common method called by both useEffects, BUT it's important that the setFetchData() call is outside this common method.
Let me know if you need more elaboration.
thanks for your reply #Jolly! I found a work around:
I moved the data lookup to a state initial calculation:
const [fetchData] = useState(processFetchData(task, data));
then im just making sure i clear the component after the axios call has been made by executing a complete function passed to the component from its parent.
This works for now, but if you have any other suggestions id love to hear them!

Resources