Reputation: 1768
I'm working on a REACT hook component that fetches data using the Fetch API fetch
. Though, I'm facing an issue that I'm not quite sure how to solve it, or better said, I'm not sure if is there any "recommended" way to face this issue.
Take this code in example:
const Fetcher = () => {
[data, setData] = useState([]);
const handleButtonClick = (e) => {
fetch('http://www.myapi.com')
.then(response => response.json())
.then(json => {
const newData = [...data];
newData.push(json);
setData(newData);
});
}
return <button onClick={handleButtonClick}>Click to fetch</button>
}
It's not a working example, but it's pretty clear what is happening: I click a button and I fetch something; that something is ADDED to the current state.
Now, where is the problem? When I wrote const newData = [...data]
I'm considering the data
variable that was available in the moment the fetch
started, NOT the current data
. This means that, if the fetch
takes 1 minute to be executed, in that minute the data
could have been updated some other way, so, when the fetch.then().then()
is called, I override the actual current data
with something that is not correct.
I can give you this scheme to make you understand better:
fetch
is executed with data
equals to [ ]
;fetch
ends (again, with data
equals to [ ]
;data
is saved the new value (e.g. ['foo']
);data
that is an empty array, it save in data
the array ['bar']
;As you can see, at the end of the day, I have an array ['bar']
, when instead it should be ['foo', 'bar']
.
To this issue, I've come up with two solutions:
Keeping a copy of the state in a ref
, and use that in the fetch.then().then()
. Something like this:
const Fetcher = () => {
[data, setData] = useState([]);
const refData = useRef(data);
const handleButtonClick = (e) => {
fetch('http://www.myapi.com')
.then(response => response.json())
.then(json => {
const newData = [...refData.current];
newData.push(json);
setData(newData);
});
}
useEffect(() => {
refData.current = data;
}, [data]);
return <button onClick={handleButtonClick}>Click to fetch</button>
}
Use a temporary variable and an useEffect to work on the latest variable:
const Fetcher = () => {
[data, setData] = useState([]);
[lastFetchedData, setLastFetchedData] = useState();
const handleButtonClick = (e) => {
fetch('http://www.myapi.com')
.then(response => response.json())
.then(json => {
setLastFetchedData(json);
});
}
useEffect(() => {
const newData = [...data];
newData.push(lastFetchedData);
setData(newData);
}, [lastFetchedData]);
return <button onClick={handleButtonClick}>Click to fetch</button>
}
I'm pretty sure that both of them work without creating any major issue, but:
First approach: I don't know, it seems to me it goes against REACT way of thinking. I'm using a ref to maintain the state up-to-date somewhere.. I mean, I don't know if this is OK for REACT "way-of-living";
Second approach: In this case I use only states. What I don't like here is that I do an extra render ANY time some data is fetched. Not big deal, but, you know, if it could be avoided it would be better. I could return an empty page if lastFetchedData
is not null
, but the user would see that empty page for maybe a millisecond. Actually, it wouldn't see the empty page, but probably the page blink due to the two renders;
Upvotes: 1
Views: 998
Reputation: 1074355
The usual solution is to use the callback form of setData
:
const Fetcher = () => {
[data, setData] = useState([]);
const handleButtonClick = (e) => {
fetch('http://www.myapi.com')
.then(response => response.json())
.then(json => {
setData(data => [...data, json]); // <============
});
}
return <button onClick={handleButtonClick}>Click to fetch</button>
}
Side notes:
Your code is falling prey to the footgun in the fetch
API I describe here. You need to check response.ok
before calling json
.
You aren't handling errors from fetch
.
You've forgotten to declare data
and setData
(I recommend using strict mode so this is an error):
So:
const Fetcher = () => {
const [data, setData] = useState([]);
// ^^^^^−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− #3
const handleButtonClick = (e) => {
fetch('http://www.myapi.com')
.then(response => {
if (!response.ok) { // #1
throw new Error("HTTP error " + response.status); // #1
} // #1
return response.json();
})
.then(json => {
setData(data => [...data, json]);
})
.catch(error => { // #2
// Handle/report error here // #2
}); // #2
}
return <button onClick={handleButtonClick}>Click to fetch</button>
}
Upvotes: 2