Using Opencage Gecoder API with REACT [2nd edition]

tsamaya

Arnaud Ferrand

Posted on June 18, 2023

Using Opencage Gecoder API with REACT [2nd edition]

Photo by Kelsey Knight on Unsplash

Overview

Previously, the tutorial was built around React classes that extended components. A few years ago, React hooks were released from beta. As a result, it is past time to go over this guide again.

We will continue to explore the integration of the Opencage API into a React application in this updated version of the tutorial.

The prerequisites are, of course, a OpenCage API key, (if you don’t have one, simply use this free registration link), a node platform with yarn or npm; and finally your favourite IDE/Text Editor.

Since setting up a build environment for React is not the focus of this tutorial, vitejs will be used.

Before we start, here is the source code. And a live version can be found here.

Setup the environment

npm create vite@latest opencage-react-app
Enter fullscreen mode Exit fullscreen mode

It runs the interactive mode where we are going to choose the React framework and the JS+swc variant:

Need to install the following packages:
  create-vite@4.2.0
Ok to proceed? (y) y
✔ Select a framework: › React
✔ Select a variant: › JavaScript + SWC

Scaffolding project in /Users/arnaud/projects/tsamaya/opencage-react-app...

Done. Now run:

  cd opencage-react-app
  npm install
  npm run dev
Enter fullscreen mode Exit fullscreen mode

Start hacking

First part

Let’s do the suggested commands above

cd opencage-react-app
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

The project is built in development mode and it opens your favourite browser on http://127.0.0.1:5173/.

vitejs-app-start

The page will automatically reload if you make changes to the code. So let’s do it.

First of all download opencage svg logo and copy it to the src/assets folder.

Open your IDE or Text Editor with the folder opencage-react-app.

Edit the file ./src/App.jsx:

replace

import reactLogo from './assets/react.svg'
Enter fullscreen mode Exit fullscreen mode

with

import reactLogo from './assets/opencage-white.svg'
Enter fullscreen mode Exit fullscreen mode

The app has been rebuilt, and instead of the atomic react logo, you should now see the OpenCage logo revolving. Since it is white on white, this may be challenging, but we will simply fix it later or right now by adding a background-color: #a9a9a9; to the body in the src/index.css file.

Use CTRL + C to stop the development server.

We will now add dependencies to the project.

In order to keep this tutorial straightforward and focused solely on integrating the Opencage Geocode API, I chose Bulma, a javascript-free CSS framework. You can choose your preferred CSS framework for the style first (such as Bootstrap, a Material UI implementation, or Tailwind).

npm install -S bulma
Enter fullscreen mode Exit fullscreen mode

It outputs:

added 1 package, and audited 30 packages in 2s

6 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Enter fullscreen mode Exit fullscreen mode

let’s create a Header component:

App.css should be renamed to Header.css. Then modify Header.css so that we may just place the centre text in the header, avoiding the nauseating infinite loop animation. It will only be a header and not the entire viewport page.

/* ./src/Header.css */
.App {
}

.App-logo {
  animation: App-logo-spin 10s linear;
  height: 40vmin;
}

.App-header {
  text-align: center;
  background-color: #20b282;
  min-height: 20vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
Enter fullscreen mode Exit fullscreen mode

Create ./src/Header.jsx file:

// ./src/Header.jsx
import React from 'react'
import logo from './assets/opencage-white.svg'
import './Header.css'

function Header() {
  return (
    <header className="App-header">
      <img src={logo} className="App-logo" alt="logo" />
      <p>
        OpenCage <b>Geocoding</b> API
      </p>
    </header>
  )
}

export default Header
Enter fullscreen mode Exit fullscreen mode

Edit ./src/main.jsx, adding

import 'bulma/css/bulma.css'
Enter fullscreen mode Exit fullscreen mode

instead of

import './index.css'
Enter fullscreen mode Exit fullscreen mode

Now edit App.jsx, we first use the Header Component and then we prepare 2 columns.

import React from 'react'
import Header from './Header'
function App() {
  return (
    <div>
      <Header />
      <div className="columns">
        <div className="column">1</div>
        <div className="column">2</div>
      </div>
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Now add packages dependencies like the OpenCage API client, LeafletJS, and classnames:

npm install -S  opencage-api-client leaflet react-leaflet classnames
Enter fullscreen mode Exit fullscreen mode

We can re-start the dev server with npm run dev

This is how the app appears right now:

screenshot-1

We will build up the form using the search input parameters in the first column. The results will appear as multiple tabs in the second column, with the first tab being the readable results (formatted address and coordinates) and the second tab containing the raw JSON result from the API. GeocodingForm and GeocodingResults are the two key components we will develop, as you can see in the following design.

design

Create a file ./src/GeocodingForm.jsx:

import React, { useState } from 'react'
import './GeocodingForm.css'

function GeocodingForm(props) {
  const [isLocating, setIsLocating] = useState(false)
  const [apikey, setApiKey] = useState('')
  const [query, setQuery] = useState('')

  function handleGeoLocation() {
    const geolocation = navigator.geolocation
    const p = new Promise((resolve, reject) => {
      if (!geolocation) {
        reject(new Error('Not Supported'))
      }
      setIsLocating(true)

      geolocation.getCurrentPosition(
        (position) => {
          console.log('Location found')
          resolve(position)
        },
        () => {
          console.log('Location : Permission denied')
          reject(new Error('Permission denied'))
        }
      )
    })
    p.then((location) => {
      setIsLocating(false)
      setQuery(location.coords.latitude + ', ' + location.coords.longitude)
    })
  }

  function handleSubmit(event) {
    console.log('Form was submitted with query: ', apikey, query)
    props.onSubmit(apikey, query)
  }

  const { isSubmitting } = props

  return (
    <div className="box form">
      <form
        onSubmit={(e) => {
          e.preventDefault()
        }}
      >
        {/* <!-- API KEY --> */}
        <div className="field">
          <label className="label">API key</label>
          <div className="control has-icons-left">
            <span className="icon is-small is-left">
              <i className="fas fa-lock" />
            </span>
            <input
              name="apikey"
              className="input"
              type="text"
              placeholder="YOUR-API-KEY"
              value={apikey}
              onChange={(e) => setApiKey(e.target.value)}
            />
          </div>
          <div className="help">
            Your OpenCage Geocoder API Key (
            <a href="https://opencagedata.com/users/sign_up">register</a>
            ).
          </div>
        </div>
        {/* <!-- ./API KEY --> */}

        {/* <!-- Query --> */}
        <div className="field">
          <label className="label">Address or Coordinates</label>
          <div className="control has-icons-left">
            <span className="icon is-small is-left">
              <i className="fas fa-map-marked-alt" />
            </span>
            <input
              name="query"
              className="input"
              type="text"
              placeholder="location"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
            />
            <div className="help">
              Address, place name
              <br />
              Coordinates as <code>latitude, longitude</code> or <code>y, x</code>.
            </div>
          </div>
        </div>
        {/* <!-- ./Query --> */}

        <div className="field">
          <label className="label">Show my location</label>
          <div className="control" onClick={handleGeoLocation}>
            {!isLocating && (
              <button className="button is-static">
                <span className="icon is-small">
                  <i className="fas fa-location-arrow" />
                </span>
              </button>
            )}
            {isLocating && (
              <button className="button is-static">
                <span className="icon is-small">
                  <i className="fas fa-spinner fa-pulse" />
                </span>
              </button>
            )}
          </div>
        </div>

        {/* <!-- Button Geocode --> */}
        <button
          className="button is-success"
          onClick={handleSubmit}
          disabled={isLocating || isSubmitting}
        >
          Geocode
        </button>
        {/* <!-- ./Button Geocode --> */}
      </form>
    </div>
  )
}

export default GeocodingForm
Enter fullscreen mode Exit fullscreen mode

Then create a file ./src/GeocodingResults.jsx:

import React, { useState } from 'react'
import classnames from 'classnames'

import ResultList from './ResultList'
import ResultJSON from './ResultJSON'

import './GeocodingResults.css'

const RESULT_TAB = 'RESULT_TAB'
const JSON_TAB = 'JSON_TAB'

function GeocodingResults(props) {
  const [activeTab, setActiveTab] = useState(RESULT_TAB)

  function renderTab(title, tab, icon, activeTab) {
    return (
      <li className={classnames({ 'is-active': activeTab === tab })}>
        <a
          href="/"
          onClick={(e) => {
            e.preventDefault()
            setActiveTab(tab)
          }}
        >
          <span className="icon is-small">
            <i className={icon} aria-hidden="true" />
          </span>
          <span>{title}</span>
        </a>
      </li>
    )
  }

  const results = props.response.results || []

  return (
    <div className="box results">
      <div className="tabs is-boxed vh">
        <ul>
          {renderTab('Results', RESULT_TAB, 'fas fa-list-ul', activeTab)}
          {results.length > 0 && renderTab('JSON Result', JSON_TAB, 'fab fa-js', activeTab)}
        </ul>
      </div>

      {/* List of results */}
      {activeTab === RESULT_TAB && results.length > 0 && <ResultList response={props.response} />}
      {/* JSON result */}
      {activeTab === JSON_TAB && results.length > 0 && <ResultJSON response={props.response} />}
    </div>
  )
}

export default GeocodingResults
Enter fullscreen mode Exit fullscreen mode

We need to create files ./src/ResultList.jsx and ./src/ResultJSON.jsx:

// ./src/ResultList.jsx
import React from 'react'

function ResultList(props) {
  const rate = props.response.rate || {}
  const results = props.response.results || []

  return (
    <article className="message">
      <div className="message-body">
        <p>
          Remaining {rate.remaining} out of {rate.limit} requests
        </p>
        <p>&nbsp;</p>
        <ol>
          {results.map((result, index) => {
            return (
              <li key={index}>
                {result.annotations.flag} {result.formatted}
                <br />
                <code>
                  {result.geometry.lat} {result.geometry.lng}
                </code>
              </li>
            )
          })}
        </ol>
      </div>
    </article>
  )
}

export default ResultList
Enter fullscreen mode Exit fullscreen mode
// ./src/ResultJSON.js
import React, { Component } from 'react'

import './ResultJSON.css'

function ResultJSON(props) {
  return (
    <article className="message">
      <div className="message-body">
        <pre>{JSON.stringify(props.response, null, 2)}</pre>
      </div>
    </article>
  )
}

export default ResultJSON
Enter fullscreen mode Exit fullscreen mode

Wire the application with those two key components (GeocodingForm and GeocodingResults) to complete the first stage:

Edit the ./src/App.jsx file:

import React, { useState } from 'react'
import Header from './Header'
import GeocodingForm from './GeocodingForm'
import GeocodingResults from './GeocodingResults'

import * as opencage from 'opencage-api-client'

function App() {
  const [isSubmitting, setIsSubmitting] = useState(false)
  const [response, setResponse] = useState({})
  return (
    <div>
      <Header />
      <div className="columns">
        <div className="column is-one-third-desktop">
          <GeocodingForm
            isSubmitting={isSubmitting}
            onSubmit={(apikey, query) => {
              setIsSubmitting(true)
              console.log(apikey, query)
              opencage
                .geocode({ key: apikey, q: query })
                .then((response) => {
                  console.log(response)
                  setResponse(response)
                })
                .catch((err) => {
                  console.error(err)
                  setResponse({})
                })
                .finally(() => {
                  setIsSubmitting(false)
                })
            }}
          />
        </div>
        <div className="column">
          <GeocodingResults response={response} />
        </div>
      </div>
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

To add the fontawesome icons, edit the project's root file index.html, and add the following line beneath div id="root">/div>:

<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
Enter fullscreen mode Exit fullscreen mode

Here is how the app currently appears:

screenshot-2

Second part

In this second stage, we'll update the results area to include a map tab.

First, let’s create a ./src/ResultMap.js file:

import React, { useEffect, useRef } from 'react'
import { MapContainer, Marker, Popup, TileLayer, FeatureGroup } from 'react-leaflet'

// import Leaflet's CSS
import 'leaflet/dist/leaflet.css'
import './ResultMap.css'

const redIcon = L.icon({
  iconUrl: 'marker-icon-red.png',
  iconSize: [25, 41], // size of the icon
  iconAnchor: [12, 40], // point of the icon which will correspond to marker's location
})

function ResultMap(props) {
  const mapRef = useRef(null)
  const groupRef = useRef(null)

  const position = [40, 0]

  useEffect(() => {
    const map = mapRef.current
    const group = groupRef.current
    if (map && group) {
      map.fitBounds(group.getBounds())
    }
  }, [props])

  return (
    <MapContainer ref={mapRef} id="map" center={position} zoom={2}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <FeatureGroup ref={groupRef}>
        {props.response.results &&
          props.response.results.map((e, i) => (
            <Marker key={i} position={e.geometry}>
              <Popup>
                {i + 1} - {e.formatted}
              </Popup>
            </Marker>
          ))}
      </FeatureGroup>
    </MapContainer>
  )
}

export default ResultMap
Enter fullscreen mode Exit fullscreen mode

Download the pin icon from marker-icon-red.png and save it to public/ folder.

As the map needs a height, we create a ./src/ResultMap.css file:

#map {
  width: auto;
  min-height: 350px;
  height: 40vmin;
}
Enter fullscreen mode Exit fullscreen mode

Back in ./src/GeocodingResuls.jsx add the tab in the ul section:

{
  results.length > 0 && renderTab('JSON Result', JSON_TAB, 'fab fa-js', activeTab)
}
Enter fullscreen mode Exit fullscreen mode

and with the other results content add the map:

{
  activeTab === MAP_TAB && results.length > 0 && <ResultMap response={props.response} />
}
Enter fullscreen mode Exit fullscreen mode

There is now a map in the application:

screenshot-3

The end

I sincerely hope you found this to be useful. If it was, kindly let me know so that I can create more articles similar to this one. You may always contact me on Mastodon or Twitter and, once again, if you read this tutorial all the way through, I'm really proud of you.

Resources

💖 💪 🙅 🚩
tsamaya
Arnaud Ferrand

Posted on June 18, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related