Reputation: 385
I'm trying to only call an axios call once per render from an event handler (onClick basically), so I'm using useEffect and inside that useEffect, I'm using useState. Problem is - when onClick is called, I get the following error:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
I understand why I'm getting it, I'm using useState in an event handler - but I don't know what else to do. How else can I handle these variables without useState?
HttpRequest.js
import {useEffect, useState} from 'react'
import axios from 'axios'
export function useAxiosGet(path) {
const [request, setRequest] = useState({
loading: false,
data: null,
error: false
});
useEffect(() => {
setRequest({
loading: true,
data: null,
error: false
});
axios.get(path)
.then(response => {
setRequest({
loading: false,
data: response.data,
error: false
})
})
.catch((err) => {
setRequest({
loading: false,
data: null,
error: true
});
if (err.response) {
console.log(err.response.data);
console.log(err.response.status);
console.log(err.response.headers);
} else if (err.request) {
console.log(err.request);
} else {
console.log('Error', err.message);
}
console.log(err.config);
})
}, [path])
return request
}
RandomItem.js
import React, {useCallback, useEffect, useState} from 'react';
import Item from "../components/Item";
import Loader from "../../shared/components/UI/Loader";
import {useAxiosGet} from "../../shared/hooks/HttpRequest";
import {useLongPress} from 'use-long-press';
function collectItem(item) {
return useAxiosGet('collection')
}
function RandomItem() {
let content = null;
let item;
item = useAxiosGet('collection');
console.log(item);
const callback = useCallback(event => {
console.log("long pressed!");
}, []);
const longPressEvent = useLongPress(callback, {
onStart: event => console.log('Press started'),
onFinish: event => console.log('Long press finished'),
onCancel: event => collectItem(),
//onMove: event => console.log('Detected mouse or touch movement'),
threshold: 500,
captureEvent: true,
cancelOnMovement: false,
detect: 'both',
});
if (item.error === true) {
content = <p>There was an error retrieving a random item.</p>
}
if (item.loading === true) {
content = <Loader/>
}
if (item.data) {
return (
content =
<div {...longPressEvent}>
<Item name={item.data.name} image={item.data.filename} description={item.data.description}/>
</div>
)
}
return (
<div>
{content}
</div>
);
}
export default RandomItem;
It works to load up the first item just fine, but when you try to cancel a long click (Basically the onClick event handler), it spits out the error above.
Upvotes: 0
Views: 735
Reputation: 385
A user in discord provided this solution: https://codesandbox.io/s/cool-frog-9vim0?file=/src/App.js
import { useState, useEffect, useCallback } from "react";
import axios from "axios";
import "./styles.css";
const fetchDataFromApi = () => {
return axios(
`https://jsonplaceholder.typicode.com/todos/${
1 + Math.floor(Math.random() * 10)
}`
).then(({ data }) => data);
};
const MyComponent = () => {
const [data, setData] = useState(undefined);
const [canCall, setCanCall] = useState(true);
const handler = {
onClick: useCallback(() => {
if (canCall) {
setCanCall(false); // This makes it so you can't call more than once per button click
fetchDataFromApi().then((data) => {
setData(data);
setCanCall(true); // Unlock button Click
});
}
}, [canCall]),
onLoad: useCallback(() => {
if (canCall) {
setCanCall(false); // This makes it so you can't call more than once per button click
fetchDataFromApi().then((data) => {
setData(data);
setCanCall(true); // Unlock button Click
});
}
}, [canCall])
};
useEffect(() => {
handler.onLoad(); //initial call
}, []);
return (
<div>
<pre>{JSON.stringify(data, " ", 2)}</pre>
<button disabled={!canCall} onClick={handler.onClick}>
fetch my data!
</button>
</div>
);
};
export default function App() {
return (
<div className="App">
<MyComponent />
</div>
);
}
Upvotes: 0
Reputation: 23705
You need to redo your hook so it would not start loading unconditionally but instead return a callback that might be called to initiate loading at some moment:
const [loadCollections, { isLoading, data, error }] = useLazyAxiosGet('collections');
....
onCancel: loadCollections
I propose to follow approach that Apollo uses when there is useQuery
that starts loading instantly and useLazyQuery
that returns callback to be called later and conditionally. But both share similar API so could be easily replaced without much updates in code.
Just beware that "immediate" and "lazy" version don't just differ by ability to be called conditionally. Say, for "lazy" version you need to decide what will happen on series calls to callback - should next call rely on existing data or reset and send brand new call. For "immediate" version there are no such a dilemma since component will be re-rendered for multiple times per lifetime, so it definitely should not send new requests each time.
Upvotes: 1