1

I have fairly nonexistent knowledge in react but I'm learning as I go. I learned the basics back in school, with class components (classic React), but now I'm delving into the Hooks API (mainly because I find it easier to learn and manage, although there seems to be more tricks involved regarding async behavior). So my question might seem silly.

I found this thread regarding setState behavior on the same topic, but this is regarding class components.

In my current application, I'm trying to set three different states using an event handler. It seems that the last state is set immediately, whereas the other two states remain undefined for a bit before changing to a real value. I'm using React-Native components for mobile development, so you'll see snippets in the code such as <SafeAreaView>.

export default App = () => {
    const [ destLong, setDestLong ] = useState();
    const [ destLat, setDestLat ] = useState();
    const [ startNav, setStartNav ] = useState(false);
    const [ locations, setLocations ] = useState([
        {
            name: 'IKEA',
            long: '-74.00653395444186',
            lat: '40.68324646680103',
        },
        {
            name: 'JFK Intl. Airport',
            long: '-73.78131423688552',
            lat: '40.66710279890186',
        },
        {
          name: 'Microcenter',
          long: '-74.00516039699959',
          lat: '40.67195933297655',
        }
    ]);

    const startNavigation = (goinglong, goinglat) => {
        setDestLong(goinglong);
        setDestLat(goinglat);
        setStartNav(true);
    }

    return (
        <SafeAreaView style={styles.container}>
          { startNav ? 
            <MapView 
              destLong = {destLong}
              destLat = {destLat}
            />
            :
            <View style={styles.buttonContainer}>
              <ScrollView>
                {
                  locations.map((location, i) => {
                    return(
                      <Card
                        style={styles.card}
                        key={i}
                        title={ location.name }
                        iconName="home"
                        iconType="Entypo"
                        description={ location.long + ", " + location.lat }
                        onPress={() => startNavigation(location.long, location.lat)}
                      />
                    );
                  })
                }
              </ScrollView>
            </View>
          }
        </SafeAreaView>
    );
}

const styles = StyleSheet.create({
    container: {
      flex: 1,
    },
    buttonContainer: {
      width: '100%',
      height: '100%',
      justifyContent: 'center',
      alignItems: 'center'
    },
    logo: {
      width: '50%',
      height: '50%',
      resizeMode: 'contain'
    },
    card: {
      marginBottom: 10,
    }
  });

This throws an error, because MapView is expecting destLong and destLat to render properly. When I console log inside my startNavigation function, it seems that it immediately updates the state for startNav to true onPress, but destLong and destLat remain undefined for a few cycles before being set.

I've tried a different approach like this:

    useEffect(() => {
        setStartNav(true);
    }, [destLong]);

    const startNavigation = (goinglong, goinglat) => {
        setDestLong(goinglong);
        setDestLat(goinglat);
    }

But it just crashes the app (my guess is infinite loop).

I've also tried removing the startNav state altogether and rendering <MapView> on destLong like this

{ destLong ? 
<MapView 
    destLong = {destLong}
    destLat = {destLat}
/>
:
<View style={styles.buttonContainer}>
    ...
</View>
}

But that did not work either.


Which brings me to this question: does the Hooks API respect the order of setState, or is each other carried out asynchronously? From my understanding it's the latter. But then, how do you handle this behavior?

AMR
  • 146
  • 2
  • 10
  • Short answer, yes, order is maintained. Have you some asynchronous code prior to enqueuing any state updates? – Drew Reese Nov 16 '21 at 20:47
  • I do have an async geolocation function running in the background actively fetching current location and updating a different state. But then why is startNav state immediately set while the destLong and destLat states remain undefined for a few cycles before setting, even though they’re given explicit values? – AMR Nov 16 '21 at 20:53
  • That is interesting since the code you've shared appears to be completely synchronous. Do you have a working example in an Expo Snack (or can you create one?) that we can inspect and debug live? – Drew Reese Nov 16 '21 at 21:10
  • 1
    Setting a state via useState is actually asynchronous, or rather the state change is enqueued and it will then return its new value after a re-render. This means that there is no guarantee in what order the states will be set. They will fire in order, but they may not be set in the same order. In your case I would use useEffect like this: useEffect(() => { if(destLong && destLat && !startNav) { setStartNav(true); } }, [destLong, destLat, startNav]); const startNavigation = (goinglong, goinglat) => { setDestLong(goinglong); setDestLat(goinglat); } – Tor Raswill Nov 16 '21 at 21:47
  • What, where, and how are you determining that the `destLong` and `destLat` state isn't updating when you expect it to? What are the *actual* symptoms you are seeing? I've seen and worked with other SO questions doing a similar thing as you and it was the geolocation data that returns lat/long coordinates taking a few "ticks"/render cycles before the data comes in and you're able to start computing the distances/etc. – Drew Reese Nov 17 '21 at 16:19
  • Geolocation does take a few ticks before actually returning anything, but that's a different topic. But when I posted the question, aside from geolocation returning current lat and long after a few ticks, the destination coordinates (destLat and destLong), which don't rely on geolocation and are retrieved from hardcore in the location state array, also took two ticks or so before changing the state to the passed value. I determined that by just console logging on startNavigation function, immediately after setting the three states (lines 49-51). – AMR Nov 17 '21 at 16:37

1 Answers1

2

I'm adding my comment here as well since I am unable to add proper formatting to my comment above.

Setting a state via useState is actually asynchronous, or rather the state change is enqueued and it will then return its new value after a re-render. This means that there is no guarantee in what order the states will be set. They will fire in order, but they may not be set in the same order.

You can read more here: https://dev.to/shareef/react-usestate-hook-is-asynchronous-1hia, as well as here https://blog.logrocket.com/a-guide-to-usestate-in-react-ecb9952e406c/#reacthooksupdatestate

In your case I would use useState and useEffect like this:

useEffect(() => {
    if(destLong && destLat && !startNav) {
        setStartNav(true);
    }
}, [destLong, destLat, startNav]);

const startNavigation = (goinglong, goinglat) => {
    setDestLong(goinglong);
    setDestLat(goinglat);
}

With that said, I think you could further simplify your code by omitting the startNav state altogether and update your conditional render:

{ (destLat && destLong) ? 
    <MapView 
      destLong = {destLong}
      destLat = {destLat}
    />
    :
    <View style={styles.buttonContainer}>
      ...
    </View>
  }

The above should have the same effect since you have two states that are undefined to begin with, and when they are both defined you want to render something and use their values.

And if you want to display the options again you can set the states to undefined again by doing setDestLat(undefined) and setDestLong(undefined)

Tor Raswill
  • 149
  • 5
  • Perfect! I wanted to learn more about the useEffect function, this cleared it up. The solutions you gave both worked perfectly. Thank you! :) – AMR Nov 16 '21 at 22:07
  • 2
    Asynchronously processed, yes, indeed they are... you enqueue them and some time later React processes them and rerenders, but the order of the enqueued state updates is maintained. React won't sort or reorder them. I still suspect the asynchronous code OP didn't share is the cause of the delay. I suspect it's also mutating the lat/long data as well, which would explain why the state updates were in order, but the mutation occurs later. Your code appears is skirting around the mutation/issue. It may work, but the explanation is incorrect. – Drew Reese Nov 16 '21 at 22:54
  • 1
    Check this [demo](https://codesandbox.io/s/using-hooks-api-does-react-respect-setstate-order-ubnqf) that enqueues 2 state updates, first to multiply a value by 2, and subtract 1 from it. Notice that starting from 2, the output is ***always*** [2, 3, 5, 9, 17, 33, ....]. If the order of state updates was indeterminate then the output could be [2, 2, 2, 2, 2...] if the order was reversed, or some other output if the order changed each time. This is not the case with enqueued state updates though. The resulting state is very easy/striaghtforward to determine. – Drew Reese Nov 16 '21 at 22:57
  • @DrewReese The problem is my project is in React/React-Native but the map component I'm using is Mapbox which is only available in iOS and Android SDKs, so there's no feasible way to make it run on an Expo snack (need header file to link it). But [here](https://github.com/amrh910/ChickenGo/blob/main/App.js) is the full code on my github. Line 44 ```Geolocation.getCurrentPosition(success, error, options);``` is the only line of relevance that I left out, that I suspect is running async as well. – AMR Nov 17 '21 at 15:17