I'm trying to load some more data from an api inside a react component on button click. This data should get merged with the already loaded data. While loading I want to show a spinner. I am using axios and react hook components.
My Code:
const App = props => {
const [data, setData] = useState({ ... });
// ...
function handleLoadMore(uri) {
setData({...data, isLoadingMore: true})
axios
.get(uri)
.then(response => {
setData({
...data,
items: [...data.items, response.data.items]
})
})
.catch(error => {
setData({
...data,
error: 'An error occured'
})
})
.finally(() => {
setData({
...data,
isLoadingMore: false
})
})
}
I expected this to first show the spinner, load the new data, merge it with the pre existing data and show the new list of items, but the new data doesnt get merged. The ajax call returns the correct result, so there is no problem. What's uexpected to me is that if i remove .finally(..)
everything works as intended, even the spinner disappears.
The question then is, how does setData update the state inside promises? In my opinion, leaving out .finally(..)
is not clear at all because isLoadingMore
is never set to false in the code yet it somehow gets updated to false anyway
It's hard to tell because the code is incomplete, but I see two problems (assuming the missing ]
is just a typo in the question):
response.data.items
in the setData
call.click
) that React specifically handles flushing updates for.So (see comments):
const App = props => {
const [data, setData] = useState({ ... });
// ...
function handleLoadMore(uri) {
// *** Use callback
setData(current => ({...current, isLoadingMore: true}));
axios
.get(uri)
.then(response => {
// *** Use callback, spread response.data.items
setData(current => ({
...current,
items: [...current.items, ...response.data.items]
}));
})
.catch(error => {
// *** Use callback
setData(current => ({...current, error: 'An error occured'}));
})
.finally(() => {
// *** Use callback
setData(current => ({...current, isLoadingMore: false}));
});
}
}
But I would combine the clearing of isLoadingMore
with the things above it if you're using combined state like that (more below).
const App = props => {
const [data, setData] = useState({ ... });
// ...
function handleLoadMore(uri) {
setData(current => ({...current, isLoadingMore: true}));
axios
.get(uri)
.then(response => {
setData(current => ({
...current,
items: [...current.items, ...response.data.items],
isLoadingMore: false // ***
}));
})
.catch(error => {
setData(current => ({
...current,
error: 'An error occured',
isLoadingMore: false // ***
}));
});
}
}
But: You should use separate useState
calls for different state items.
const App = props => {
const [items, setItems] = useState([]);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState("");
// ...
function handleLoadMore(uri) {
setLoadingMore(true);
axios
.get(uri)
.then(response => {
setItems(currentItems => [...currentItems, ...response.data.items]);
})
.catch(error => {
setError('An error occured');
})
.finally(() => {
setLoadingMore(false);
});
}
// ...
}
Notice how this lets the updates be more discrete, resulting in less work (not constantly re-spreading all the state items).