Working with React

This article describes how to use the HERE Maps API for JavaScript with React. The target is to demonstrate how to build a React component that displays the map and responds to the actions of the user, be it direct interaction with the map or the other components.

Setup

For the fast setup of the new React application we will use the Create React App environment. It provides a fast way to get started building a new single-page application. Execute the npx runner as below (it requires Node >= 8.10 and npm >= 5.6):

npx create-react-app jsapi-react && cd jsapi-react

The call above produces the scaffolding needed to start the application. The directory structure in the jsapi-react directory looks as follows. The React components reside in the src directory:

my-app
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   ├── ...
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    └── serviceWorker.js
    └── setupTests.js

Note

The latest version of create-react-app uses Webpack 5 and is configured to use the polyfill for the global variables. That default does not work with the HERE Maps API for JavaScript. In order to change the setting eject the react app:

npm run eject

And add the following option to the Webpack configuration object under config/webpack.config.js:

node: {
     global: false
}

The recommended way to use HERE Maps API for JavaScript within this environment is to install maps-api-for-javascript NPM package which is hosted at https://repo.platform.here.com/. Add a registry entry to the NPM configuration by executing the following command:

npm config set @here:registry https://repo.platform.here.com/artifactory/api/npm/maps-api-for-javascript/

After that the package from the @here namespace can be installed as usual:

npm install @here/maps-api-for-javascript --save

At this step the environment setup is complete, all packages needed to build a sample application are installed, and it is possible to start the development server by executing:

npm start

The command above launches the development server with the "hot reload" functionality and opens the application in the browser.

Add a static map component

It is possible to add a static map to the application by creating a component that contains an H.Map instance and renders it to the component's container. In order to do that create a file Map.js in the src directory for the new React class component. This component displays a map in the container that has a width and a height of 300 pixels:

import React from 'react';
import H from "@here/maps-api-for-javascript";

export default class Map extends React.Component {
  constructor(props) {
    super(props);
    // the reference to the container
    this.ref = React.createRef();
    // reference to the map
    this.map = null;
  }

  componentDidMount() {
    if (!this.map) {
      // instantiate a platform, default layers and a map as usual
      const platform = new H.service.Platform({
        apikey: '{YOUR_API_KEY}'
      });
      const layers = platform.createDefaultLayers();
      const map = new H.Map(
        this.ref.current,
        layers.vector.normal.map,
        {
          pixelRatio: window.devicePixelRatio,
          center: {lat: 0, lng: 0},
          zoom: 2,
        },
      );
      this.map = map;
    }
  }

  render() {
    return (
      <div
        style={{ width: '300px', height:'300px' }}
        ref={this.ref}
      />
    )
  }
}

The component above now can be used within the App component, replace the content of th src/App.js with the following code:

import Map from './Map';

function App() {
  return (
    <div>
      <Map />
    </div>
  );
}

export default App;

That will render the static map at the zoom level 2 and 0 latitude and longitude: Static Map component

Resizing the map

In many cases it is desirable that the map occupies the full width and/or height of the component. The H.Map instance does not attempt to deduce when the parent container is resized and the map needs an explicit resize() method call in order to adjust to the new dimensions of the container. To demonstrate how it can be achieved within the component we will use the simple-element-resize-detector. Run the following command from the project's directory:

npm install simple-element-resize-detector --save

In the src/Map.js adjust the import statements by importing the simple-element-resize-detector library:

import React from 'react';
import H from "@here/maps-api-for-javascript";
import onResize from 'simple-element-resize-detector';

Update componentDidMount method with the map.getViewPort().resize() call and adjust the width of the container in the render method:

componentDidMount() {
  if (!this.map) {
    const platform = new H.service.Platform({
      apikey: '{YOUR_API_KEY}'
    });
    const layers = platform.createDefaultLayers();
    const map = new H.Map(
      this.ref.current,
      layers.vector.normal.map,
      {
        pixelRatio: window.devicePixelRatio,
        center: {lat: 0, lng: 0},
        zoom: 2,
      },
    );
    onResize(this.ref.current, () => {
      map.getViewPort().resize();
    });
    this.map = map;
  }
}

render() {
  return (
    <div
      style={{ position: 'relative', width: '100%', height:'300px' }}
      ref={this.ref}
    />
  )
}

the component will assume the width of the enclosing container: Static Map component 100% width

Setting the zoom and center

We want another component to take a user's input and change the zoom level and the center of the map. The MapPosition component (src/MapPosition.js) has three input fields and takes a callback from the parent as props:

import React from 'react';

export default class MapPosition extends React.Component {
  handleOnChange = (ev) => {
    const {
      onChange
    } = this.props;
    // pass the values to the parent component
    onChange(ev.target.name, ev.target.value);
  }

  render() {
    const {
      lat,
      lng,
      zoom
    } = this.props;
    return (
      <>
        <div>
          Zoom:
          <input
            onChange={this.handleOnChange}
            name="zoom"
            type="number"
            value={zoom}
          />
        </div>
        <div>
          Latitude:
          <input
            onChange={this.handleOnChange}
            name="lat"
            type="number"
            value={lat}
          />
        </div>
        <div>
          Longitude:
          <input
            onChange={this.handleOnChange}
            name="lng"
            type="number"
            value={lng}
          />
        </div>
      </>
    )
  }
}

It communicates with the App component which manages the state of the application. The App component accordingly should be updated to store the state, and pass the appropriate props to the child components:

import React from 'react';
import Map from './Map';
import MapPosition from './MapPosition';

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      zoom: 0,
      lat: 0,
      lng: 0
    }
  }

  handleInputChange = (name, value) => {
    this.setState({
      [name]: value
    })
  }

  render() {
    const {
      zoom,
      lat,
      lng
    } = this.state;
    return (
      <div className="App">
        <Map
          lat={lat}
          lng={lng}
          zoom={zoom}
        />
        <MapPosition
          lat={lat}
          lng={lng}
          onChange={this.handleInputChange}
          zoom={zoom}
        />
      </div>
    );
  }
}

The final step is to adjust the Map component by adding the componentDidUpdate method responsible for updating the map view as per the user's input:

  componentDidUpdate() {
    const {
      lat,
      lng,
      zoom
    } = this.props;

    if (this.map) {
      // prevent the unnecessary map updates by debouncing the
      // setZoom and setCenter calls
      clearTimeout(this.timeout);
      this.timeout = setTimeout(() => {
        this.map.setZoom(zoom);
        this.map.setCenter({lat, lng});
      }, 100);
    }
  }

The resulting application can take the input from the user with the help of the MapPosition component, store the state in the App and update the Map as per the user input: Map component with the user input

The interactive map

The application above takes only the input via the MapPosition component, normally users expect the map itself to be interactive. The optimal solution enables the user to input the values directly and see the desired location on the map, as well as interact with the map and see the current coordinates. That can be achieved by adding the mapviewchange listener to the H.Map instance and updating the App state via the callback. In order to achieve that add a method in src/App.js that will be responsible to update the App state:

  handleMapViewChange = (zoom, lat, lng) => {
    this.setState({
      lat,
      lng,
      zoom
    })
  }

And pass it as props to the Map component:

    <Map
      lat={lat}
      lng={lng}
      onMapViewChange={this.handleMapViewChange}
      zoom={zoom}
    />

In the Map component add a handleMapViewChange method. The method receives the map event and calls the onMapViewChange callback:

  handleMapViewChange = (ev) => {
    const {
      onMapViewChange
    } = this.props;
    if (ev.newValue && ev.newValue.lookAt) {
      const lookAt = ev.newValue.lookAt;
      // adjust precision
      const lat = Math.trunc(lookAt.position.lat * 1E7) / 1E7;
      const lng = Math.trunc(lookAt.position.lng * 1E7) / 1E7;
      const zoom = Math.trunc(lookAt.zoom * 1E2) / 1E2;
      onMapViewChange(zoom, lat, lng);
    }
  }

Add the following lines to attach a listener and enable the interactive behaviour after the map instantiation in the componentDidMount:

  // attach the listener
  map.addEventListener('mapviewchange', this.handleMapViewChange);
  // add the interactive behaviour to the map
  new H.mapevents.Behavior(new H.mapevents.MapEvents(map));

Besides that, to avoid adverse effects, it is important to remove the event listeners in the componentWillUnmount:

  componentWillUnmount() {
    if (this.map) {
      this.map.removeEventListener('mapviewchange', this.handleMapViewChange);
    }
  }

results matching ""

    No results matching ""