Reputation: 393
I have a custom hook named useFetchMyApi
wrapping a fetch call to API endpoint. The function hook accepts a parameter and it is included in the post body. The data array output is dependent on the hook parameter.
On the UI, the App component calls useFetchMyApi
once. A button click handler will call subsequent useFetchMyApi
to update a list on the UI.
My issues:
useFetchMyApi
in the event handler because it violates the rules of hook.useFetchMyApi
?import React, { useState, useEffect, useRef } from "react";
import "./styles.css";
const useFetchMyApi = (location = "") => {
const [items, setItems] = useState([]);
useEffect(() => {
// let's stimulate the API call with a 2 seconds response time
setTimeout(() => setItems([location, Math.random()]), 5000);
}, [location]);
return { items };
};
export default function App() {
const locationSelect = useRef(null);
const { items } = useFetchMyApi("mylocation1");
const onButtonClicked = (event) => {
const selectedLocation = locationSelect.current.value;
// Call api and refresh items by location
// Fix here??
//useFetchMyApi(selectedLocation);
};
return (
<div className="App">
<select ref={locationSelect}>
<option value="location1">Location 1</option>
<option value="location2">Location 2</option>
</select>
<button onClick={onButtonClicked}>Refresh</button>
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
);
}
https://codesandbox.io/s/stupefied-water-1o6mz?file=/src/App.js:0-1055
Upvotes: 11
Views: 11757
Reputation: 202618
- I can't call
useFetchMyApi
in the event handler because it violates the rules of hooks.
Correct, react hooks can only be called from the body of functional components and other react hooks. They can not be conditionally invoked, such as in an asynchronous callback, or in loops.
- How do I refresh the list of items dependent on the response of
useFetchMyApi
?
You've nothing in App
to trigger a rerender to trigger the effect in order to trigger the effect's callback.
You should leverage the useEffect
hook's dependency in your custom useFetchMyApi
hook to refresh the list of items. Add some local component state to represent the "currently" selected location. Use your button's onClick
callback to update the state and trigger a rerender which then will rerun your custom hook.
const useFetchMyApi = (location = "") => {
const [items, setItems] = useState([]);
useEffect(() => {
// let's stimulate the API call with a 2 seconds response time
setTimeout(() => setItems([location, Math.random()]), 2000);
}, [location]); // <-- effect callback triggers when location updates
return { items };
};
function App() {
const locationSelect = useRef(null);
const [location, setLocation] = useState("mylocation1"); // <-- add state
const { items } = useFetchMyApi(location); // <-- pass location value to hook
const onButtonClicked = () => {
const selectedLocation = locationSelect.current.value;
setLocation(selectedLocation); // <-- update state, trigger rerender
};
return (
<div className="App">
<select ref={locationSelect}>
<option value="location1">Location 1</option>
<option value="location2">Location 2</option>
</select>
<button onClick={onButtonClicked}>Refresh</button>
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
);
}
An alternative solution would be to rewrite your custom useFetchMyApi
hook to also return a callback to be invoked on-demand in a click handler to do the fetch and update the list. Here is a simple conversion. Notice now the returned fetch function consumes the location, not the hook call.
const useFetchMyApi = () => {
const [items, setItems] = useState([]);
// custom fetch function consumes location
const locationFetch = (location = "") => {
// let's stimulate the API call with a 2 seconds response time
setTimeout(() => setItems([location, Math.random()]), 2000);
};
return [items, locationFetch]; // <-- return state and fetch function
};
export default function App() {
const locationSelect = useRef(null);
const [items, locationFetch] = useFetchMyApi(); // <-- destructure state and fetch function
useEffect(() => {
locationFetch("mylocation1");
}, []); // <-- initial mounting fetch call
const onButtonClicked = () => {
const selectedLocation = locationSelect.current.value;
locationFetch(selectedLocation); // <-- invoke fetch function
};
return (
<div className="App">
<select ref={locationSelect}>
<option value="location1">Location 1</option>
<option value="location2">Location 2</option>
</select>
<button onClick={onButtonClicked}>Refresh</button>
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
);
}
Upvotes: 18