简体   繁体   中英

React not updating state with setState correctly inside promises

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):

  1. You're not spreading out response.data.items in the setData call.
  2. You're using existing state to set new state, but not doing it with the callback version of the state setter. The documentation is inconsistent about this, but you should use the callback version when setting state based on existing state unless you know you only update state in certain specific event handlers (like 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).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM