React-map-gl: Mapbox-GL component doesn't update after state change made in the store

Created on 9 Jul 2019  路  9Comments  路  Source: visgl/react-map-gl

I'm learning to use mapbox with react.js.For my little projet i've setup some actions that updated my coordinates.I can log the new state that i get as props through mapStateToProps.But when i try update the existing state,i doesn't work and gives errors.
Here is my code :

import ReactMapGL, { FlyToInterpolator } from "react-map-gl";
import React, { Component } from "react";
import "mapbox-gl/dist/mapbox-gl.css";

//redux side
import { connect } from 'react-redux';

const MAPBOX_TOKEN ="pk.mysecretapikey";

const mapStyle = "mapbox://styles/jlmbaka/cjvf1uy761fo41fp8ksoil15x";

const INITIAL_STATE = {
      height: "100vh",
      width: "100%",
      longitude: 23.071374,
      latitude: -3.6116245,
      zoom: 1.33
  };

const DRC_MAP = {
    longitude: 23.656,
    latitude: -2.88,
    zoom: 4.3,
    curve: 1,
    speed: 0.7,
    easing: t => t * (2 - t)
  }

let count=0;
class Map extends Component{
  state = {
        viewport:INITIAL_STATE,
        country:DRC_MAP
    };


    _goToViewport = args => {
      // console.log("goToViewport() : ", this.state.viewport);
      this._handleViewportChange({
        zoom: 0,
        transitionInterpolator: new FlyToInterpolator(),
        transitionDuration: 3000,
        ...args
      });
    };

    _flyToDRC = () => {
      console.log("flyToDRC - "+count);
      setTimeout(() => {
        this._goToViewport(
          this.state.country
        );
      }, 1000);
    };

    _flyToInfrastructure = ()=>{}

    _handleLoad = () => this._flyToDRC();

    _handleViewportChange = viewport => {
      console.log("_handleViewPortChange() - "+count);
      this.setState({viewport})
    };


     componentWillReceiveProps(nextProps){
       if (nextProps.energy !== null) {
         window.alert("update state");
         const latitude = nextProps.energy[0].geometry.coordinates[0];
         const longitude = nextProps.energy[0].geometry.coordinates[1];
         this.setState({
           viewport: this.state.country
         });
         this.setState({
           country: {
             ...this.state.country,
             latitude,
             longitude
           }
         }, () =>{
            console.log("Actual state : ", this.state);
         } );

       } 
     }

    render(){
      return (
        <ReactMapGL
          mapboxApiAccessToken={MAPBOX_TOKEN}
          mapStyle={mapStyle}
          onViewportChange={this._handleViewportChange}
          onLoad={this._handleLoad}
          {...this.state.viewport}
        />
      );
    };
}


const mapStateToProps = (state) => {
  const { energy } = state; 
  return {
    energy:energy
  }
}

export default connect(mapStateToProps,null)(Map);

in the above code : the INITIAL_STATE const has got the values that determine how my map will initially render and DRC_MAP const is how my map will rendered after the animated zoom.ANd i've used those 2 const to initialize my state that are viewport and country.With thhis every works fine and as expected wthout any problem.But in my componeneWillReceiveProps method,i'm cheking if there is some new value for my state like this : if(this.props.energy!==null){ so tru,i make the update so that the zoomed version or actual map becomes the the initial viewport and they new values the target area to show and zoom in the map and then the animation can occur again,and that where nothing is working i cannot size view with animation based on new values.How can i make this work?Thank you

Most helpful comment

Hey @jochri3 is this closer to what you were after?

I didn't fully understand the architecture of your project so we were putting the code in the wrong place.

The solution is to move setViewport controls into a context provider. Then we can inject it anywhere we wish, for example, in SectorDetails.js where the click happens.

Files I changed:

  • MapProvider.jsx - create provider that includes viewport, setViewport and onLoad (for initial animation)
  • _app.js - Wrap app in MapProvider
  • map.js - remove all code we worked on before and just get viewport, setViewport and onLoad from MapContext
  • SectorDetails.jsx - get setViewport method from MapContext. On click, get the coordinates and pass to setViewport.

The main thing to take away is that we have moved the viewport control out of the map component and into a context that anywhere can use. You can use this context in "Routes" for example to move the map the same way.

Advanced note: It's important to note that by doing this, any component using MapProvider will re-render as the map moves, because the viewport value is constantly changing. Not to worry - I have provided a withMap HOC in MapProvider.jsx. By passing the context values as props (instead of using useContext within render) we can wrap the component in React.memo and manually check for prop updates we care about. I have just returned true in this case, but in future if props.God changes, for example, you might want to re-render.

Let me know if this doesn't make sense.

All 9 comments

Your component may be receiving updates from the store before the map is initialised. Also, react-map-gl doesn't take curve, speed or easing parameters for transitions.

Can I suggest using hooks to listen for changes?

import React, {useEffect, useState} from 'react';
import {useSelector} from "react-redux";
import ReactMapGL, {FlyToInterpolator} from 'react-map-gl';

// this is what state gets initialised as
const INITIAL_STATE = {
  height: "100vh",
  width: "100%",
  longitude: 23.071374,
  latitude: -3.6116245,
  zoom: 1.33
};

// this is the state we want to merge when the map has loaded.
// note the transition* parameters.
const DRC_MAP = {
  longitude: 23.656,
  latitude: -2.88,
  zoom: 4.3,
  transitionDuration: 500,
  transitionInterpolator: new FlyToInterpolator(),
  transitionEasing: t => t * (2 - t)
}

const MAPBOX_TOKEN ="pk.mysecretapikey";
const mapStyle = "mapbox://styles/jlmbaka/cjvf1uy761fo41fp8ksoil15x";

// classes are so 2018 - use functional components instead!
export default function Map(){
  // get the value from your redux store
  const {energy} = useSelector(state => state);

  // to use when the map is loaded and to change the viewport
  const [loaded, setLoaded] = useState(false);
  const [country, setCountry] = useState({});
  const [viewport, setViewport] = useState(INITIAL_STATE);

  // this effect will fire whenever energy or loaded changes.
  useEffect(() => {
    // all these checks are to ensure this doesn't fire unnecessarily.
    // you may have to add more in the future.
    if(
      loaded && 
      energy !== null &&
      energy.length > 0 &&
      viewport.longitude !== DRC_MAP.longitude &&
      viewport.latitude !== DRC_MAP.latitude
    ){
      // when loaded = true, energy is available, and we're not already
      // at the DRC_MAP lon/lat, merge the old viewport (including height/width)
      // with the new DRC viewport.
      setViewport(oldViewport => ({
        ...oldViewport,
        ...DRC_MAP
      }))

      // optionally, you can get the lon/lat from energy to use in this transition.
      // it's not clear why you're storing it in the state under country.
      const [longitude, latitude] = energy[0].geometry.coordinates;
      setCountry({longitude, latitude});
    }
  }, [energy, loaded, DRC_MAP]);

  return (
        <ReactMapGL
          mapboxApiAccessToken={MAPBOX_TOKEN}
          mapStyle={mapStyle}
          onViewportChange={nextViewport => setViewport(nextViewport)}
          onLoad={() => setLoaded(true)}
          {...viewport}
        />
  )
}

Note in the above, useEffect will fire three plus times, but that's OK:

  1. on initial load (energy, loaded and DRC_MAP are all initialised)
  2. when energy becomes available (store has been filled with, I am assuming, AJAX content)
  3. when loaded is set to true from the react-map-gl onLoad function.
  4. any time 'energy' updates in the store.

References:

edit: added const [viewport, setViewport] = useState(INITIAL_STATE);, removed this.state and corrected references to loaded.

errrer
err2

I'm getting the first error with isLoading,for that i've understood that the the name of the state wasn't the same as in the hook definition where it's loaded and after change,i got the 2nd error.
In your code you use the following syntax in the return statement this.state.viewport but we are in a functional component and we cannot use this,also nowhere you've defined the viewport peace of state

Spot on! Good pickup, you know we need a hook for viewport. I wrote that directly into github and didn't run it. I've updated my comment.

  1. const [viewport, setViewport] = useState(INITIAL_STATE);
  2. ...this.state.viewport => ...viewport (I copied & pasted from your code, my bad).
  3. corrected vars isLoaded => loaded.
  4. removed longitude, latitude from within setViewport (wasn't initialised yet)
  5. check for specific lon/lat within useEffect, because viewport !== DRC_MAP

If it still doesn't work, put it in a code sandbox and I will see what's wrong.

Thank you very much,but i've even lost the initial behavior

With my code(class based Map component),after page loads there is an intial view and then it zooms to DRCongo map.And all that happen before i press any button.Here is how was before with my code.So what i wanted i that when i click on the button Inga1 or Inga2 it can change the view according to the selected place coordinates
old_behavior

And i've tried what you've suggested(with Hooks),and the behaviour has changed,see what it does now:
new_behavior

My goal is to have the following behavior:
ezgif com-optimize
With that one i used jquery longtime a go before learning react,it was so simple to bind click with viewport which are concepts very different from react states,props concpts

Gotcha, we can do that easily. Please put it into a codesandbox.io and I can have a further look for you.

Here is the link of the project in Sandbox : https://codesandbox.io/s/h61mk
Thank you very much

Hey @jochri3 is this closer to what you were after?

I didn't fully understand the architecture of your project so we were putting the code in the wrong place.

The solution is to move setViewport controls into a context provider. Then we can inject it anywhere we wish, for example, in SectorDetails.js where the click happens.

Files I changed:

  • MapProvider.jsx - create provider that includes viewport, setViewport and onLoad (for initial animation)
  • _app.js - Wrap app in MapProvider
  • map.js - remove all code we worked on before and just get viewport, setViewport and onLoad from MapContext
  • SectorDetails.jsx - get setViewport method from MapContext. On click, get the coordinates and pass to setViewport.

The main thing to take away is that we have moved the viewport control out of the map component and into a context that anywhere can use. You can use this context in "Routes" for example to move the map the same way.

Advanced note: It's important to note that by doing this, any component using MapProvider will re-render as the map moves, because the viewport value is constantly changing. Not to worry - I have provided a withMap HOC in MapProvider.jsx. By passing the context values as props (instead of using useContext within render) we can wrap the component in React.memo and manually check for prop updates we care about. I have just returned true in this case, but in future if props.God changes, for example, you might want to re-render.

Let me know if this doesn't make sense.

Thank you very much @mateiyo,this was the behavior that i wanted

Thanks guys, this is helpful. I would like to do the same with my app. Any way to look at the code?
https://timelinesapp.netlify.app/timeline/5fc21eec73137500159ca01d

Screen Shot 2020-11-28 at 5 37 00 PM (2)

When I click an item on the sidebar, it will zoom into that marker on the mapView. I was thinking of doing this using Context but was unsure. Thanks for confirming. Also, I didn't know about the re-render. It makes sense that when they talk about re-render they usually talk about useMemo hook.

How would you do the opposite behavior though: User clicks on a marker on the mapView and the item is scrollToCenter on the sidebar? Would it be the same, just have as part of the onClick handler on the marker to include a scrollToCenter to the same id? Thanks again. I'll try to implement this today.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

joseomar10 picture joseomar10  路  5Comments

xoddong picture xoddong  路  4Comments

iamvdo picture iamvdo  路  5Comments

cjmyles picture cjmyles  路  3Comments

SethHamilton picture SethHamilton  路  3Comments