1

I'm trying to use a geocode locator which returns longitude and latitude from a city name. This is supposed to be set as a state and used to display the city on a map. I can manage to fetch the results just fine, however, when I try to set the state, the state is null.

This is the code for the map component:

import React, { useState, useEffect } from "react";
import MapGL, { GeolocateControl } from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";
import { OpenStreetMapProvider } from "leaflet-geosearch";

const Map = (props) => {
  const [long, setLong] = useState(null);
  const [lat, setLat] = useState(null);

  useEffect(() => {
    getCoords();
  }, []);

  const TOKEN =
    "xxx.xxx.xxxx-xxxxx";

  const [viewport, setViewPort] = useState({
    width: "20%",
    height: 400,
    latitude: 6.122498,
    longitude: 80.112597,
    zoom: 12,
  });

  const _onViewportChange = (viewport) =>
    setViewPort({ ...viewport, transitionDuration: 3000 });

  const getCoords = async () => {
    // setup
    const provider = new OpenStreetMapProvider();

    // search
    const results = await provider.search({ query: props.currentCity });
    console.log(results[0].x)
    console.log(results[0].y)
    setLong({ long: results[0].x });
    setLat({ lat: results[0].y });

    console.log(long);
    console.log(lat)
  };
  return (
    <div style={{ margin: "0 auto" }}>
      <MapGL
        {...viewport}
        mapboxApiAccessToken={TOKEN}
        mapStyle="mapbox://styles/mapbox/dark-v8"
        onViewportChange={_onViewportChange}
      ></MapGL>
    </div>
  );
};

export default Map;

I've preset some values as latitude and longitude and rely on the console. logs, so the app doesn't break.

    console.log(results[0].x)
    console.log(results[0].y)
    setLong({ long: results[0].x });
    setLat({ lat: results[0].y });

    console.log(long);
    console.log(lat)

The first two logs prints the location just great. The last two, prints the initial state of the state, which is: null.

The city name comes from the props, which comes from a different component and it does work as it should.

            <Map currentCity={currentCity}/>

I'm pretty sure it's something stupid but I cannot for my life figure out what. The map is a mapbox, I'm using OpenStreetMapProvider from leaflet.

I've tried different providers but to no success. I'm pretty sure that I'm doing something wrong in my async call. I think that the state sets before the call is finished. I've tried using promises but nothing changes.

Thanks.

Jannemannen
  • 115
  • 1
  • 1
  • 10

3 Answers3

2

Because its async method :

setLong({ long: results[0].x }); // <--- This is async
setLat({ lat: results[0].y }); // <--- This is async

console.log(long); // <--- So,You will not get direct updated value
console.log(lat) // <--- So,You will not get direct updated value

Your dom will be rendered once the value is set,

So don't worry, you will get those value inside return where you put JSX.


You can run the below snippet and check output in HTML and console, Hope that will clear your doubts

const { useState , useEffect } = React;

const App = () => {

  const [lat,setLat] = useState(0);
  const [lang,setLang] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      setLat(42.12412)
      setLang(41.1231)

      console.log("Lat after setLat: " , lat); // <----- Not updated due to async behaviour
      console.log("Lang after setLang: " , lang); // <----- Not updated due to async behaviour
    },2000);
  },[]);

  return (
    <div>
      Lat : {lat} <br/> 
      Lang : {lang}
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('react-root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react-root"></div>
Vivek Doshi
  • 56,649
  • 12
  • 110
  • 122
  • This actually makes alot of sense. Thanks for the answer! – Jannemannen May 11 '20 at 14:35
  • 1
    While it's good to know that rendering happens asynchronously, it's not enough to explain this behavior in function components. Your closure variables from the first render are *never* going to have the values from the second render. If the async thing was all that was causing this you could wrap your console.log in a setTimeout that's long enough and see the new value, but you can not. Try `setTimeout(() => console.log(lat), 10000)`, and it will still log 0. – Nicholas Tower May 11 '20 at 17:42
  • Inside useeffect put 2 settimeout with diff time and check what happens. Hope that will clear your doubt. – Vivek Doshi May 11 '20 at 17:48
  • Agreed, adding in setTimeouts to observe the behavior is an excellent idea. We should add it to your answer. Would you like to do the edit or shall I? – Nicholas Tower May 11 '20 at 18:16
  • Currently I am replying from mobile, so cant edit the code right now, you can or whenever I open my PC I'll do it. – Vivek Doshi May 11 '20 at 18:20
  • 1
    @NicholasTower is right, and it is already well explained in [the duplicate](https://stackoverflow.com/a/58877875/1218980). – Emile Bergeron May 11 '20 at 21:04
1

long and lat are local consts. They can never change, and that's not what setLong and setLat are trying to do. The purpose of setting state is to tell react to rerender the component. When that new render happens, new local variables will be created with the new values.

So everything's working as it should, except that you're put your log statement in a place where it's not useful. If you'd like to see the new values, put the log statement in the body of the component

  const [long, setLong] = useState(null);
  const [lat, setLat] = useState(null);
  console.log('rendering', lat, long);
Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98
  • 2
    @Jannemannen, its nothing about because of `const`, it 's not updated just because of async behavior od execution. even in the react doc they are using `const` , https://reactjs.org/docs/hooks-state.html – Vivek Doshi May 11 '20 at 14:22
  • @VivekDoshi You're correct that set state is sometimes asynchronous, but whether or not its synchronous, settings state does not and can not update local variables from the current render. You are carrying over knowledge that was important to class components into function components, where it makes no difference. In class components, `this.state` will be replaced, and so there's a thing you can check that you might expect to change, but it doesn't due to the async issue. Closure variables do not get changed when a function component sets state so sync vs async is irrelevent. – Nicholas Tower May 11 '20 at 17:23
0

There is no issue with this code, actually the state is getting updated. Setting state is an asynchronous task, it will batch your state update and update the state once at the end.

Execution order by numbering:

setLong({ long: results[0].x });  -->(3)
setLat({ lat: results[0].y });    -->(4)
console.log(long);                -->(1)
console.log(lat)                  -->(2)

Here you are updating the state and try to console it immediately, that's why you are getting null(initial state). Remember SetLong() and SetLate() is async function call.