setState hook not updating - though prevState shows correct value

Viewed 48

I'm using the state variable to keep track of my previous state.

The animation works (without index), but when I do

const [index, setIndex] = useState(0)

useEffect(() => {
        animation.addListener((scroll) => {
            setTimeout(function() {
                let position = Math.floor(scroll.value / cardWidth + 0.75);
                if (position >= restaurants.length - 1) position = restaurants.length - 1;
                if (position <= 0) position = 0;

                console.log("position: ", position, "index: ", index)

                if (index !== position) {
                    setIndex(prevState => {
                        console.log("previous: ", prevState, "position: ", position, "index: ", index)
                        return position
                    });
                    const { coordinate } = restaurants[position];
                    mapRef.current.animateCamera({
                        center: {
                            latitude: coordinate.latitude,
                            longitude: coordinate.longitude,
                        },
                         altitude: 2000, // zooms in when viewing specific location
                    }, {duration: 1000}) // not sure if duration is actually doing anything
                }
            }, 3000);
        });
        return function cleanUp(): void { animation.removeAllListeners(); }
    }, []);

The index never updates. I've tried checking the prevState inside the setState function and that updates, but if I print index outside of setIndex -- it doesn't update. How can I fix this? EDIT: Found the problem. The function never stops as it is always listening. How can I refactor my code to end the function so the state is changed?

Output from console

position:  1 index:  0
previous:  1 position:  1 index:  0
position:  1 index:  0
previous:  1 position:  1 index:  0
position:  1 index:  0
previous:  1 position:  1 index:  0
position:  1 index:  0
previous:  2 position:  2 index:  0
position:  2 index:  0
previous:  2 position:  2 index:  0
position:  2 index:  0
previous:  3 position:  3 index:  0
position:  3 index:  0
previous:  3 position:  3 index:  0
position:  3 index:  0

EDIT: Answer That I Found

As many have stated below, useState is asynchronous and will wait for the function to end before updating. The listener is a function that keeps running once it's activated until the next render. Because of this, values only updated when it re-rendered. I decided to switch to useRef as it's synchronous and allowed me to keep the most up-to-date values without re-rendering.

1 Answers

Correct, for a couple of reasons:

  • You create a closure inside addListener capturing the initial value of index, which is set to 0 via useState(0).
  • You supply an empty dependency list for useEffect meaning it will only ever run once when the component mounts

To fix it, you should supply index to the dependency list of useEffect to allow it to re-run thus picking up the newest index.

useEffect(() => { ... }, [index]);

Alternatively, if you find unhooking / rehooking the event handlers is problematic, you can pass a function to setIndex that allows you access to the previous value e.g.

setIndex(oldIndex => {
  let newIndex = oldIndex;
  if (oldIndex != position) {
    // update newIndex 
  }
  return newIndex;
});
Related