Using Opencage Gecoder API with REACT [2nd edition]
Tags: react, javascript, geocoder, opencage
Published: 2023-06-18 17:38:50

Photo by [Kelsey Knight](https://unsplash.com/photos/SFRw5GChoLA) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)

## 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](https://opencagedata.com/) API into a [React](https://reactjs.org/) 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](https://opencagedata.com/users/sign_up)), a [node](https://nodejs.org/) platform with [yarn](https://yarnpkg.com/) or [npm](https://www.npmjs.com/get-npm); and finally your favourite IDE/Text Editor.

Since setting up a build environment for React is not the focus of this tutorial, [vitejs](https://vitejs.dev/) will be used.

Before we start, here is the [source code](https://github.com/tsamaya/opencage-react-guide-v2). And a live version can be found [here](https://glowing-trifle-49c0ea.netlify.app/).

## Setup the environment

```bash
npm create vite@latest opencage-react-app
```

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

```bash
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
```

## Start hacking

### First part

Let’s do the suggested commands above

```bash
cd opencage-react-app
npm install
npm run dev
```

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

![vitejs-app-start](https://raw.githubusercontent.com/tsamaya/opencage-react-guide-v2/master/resources/vitejs-app-start.png)

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

First of all download [opencage svg logo](https://raw.githubusercontent.com/tsamaya/opencage-react-guide-v2/master/resources/opencage-white.svg) 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

```js
import reactLogo from './assets/react.svg'
```

with

```js
import reactLogo from './assets/opencage-white.svg'
```

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](https://bulma.io), 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).

```bash
npm install -S bulma
```

It outputs:

```bash
added 1 package, and audited 30 packages in 2s

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

found 0 vulnerabilities
```

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.

```css
/* ./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);
  }
}
```

Create `./src/Header.jsx` file:

```js
// ./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
```

Edit `./src/main.jsx`, adding

```js
import 'bulma/css/bulma.css'
```

instead of

```js
import './index.css'
```

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

```js
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
```

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

```bash
npm install -S  opencage-api-client leaflet react-leaflet classnames
```

- [opencage-api-client](https://github.com/tsamaya/opencage-api-client) is the client library for Opencage Geocoder API
- [LeafletJS](https://leafletjs.com/) is the well-known web mapping API
- [classnames](https://github.com/JedWatson/classnames) is a javascript utility lib to help build className attributes

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

This is how the app appears right now:

![screenshot-1](https://raw.githubusercontent.com/tsamaya/opencage-react-guide-v2/master/resources/screenshot-1.png)

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](https://raw.githubusercontent.com/tsamaya/opencage-react-guide-v2/master/resources/design-1.png)

Create a file `./src/GeocodingForm.jsx`:

```js
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
```

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

```js
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
```

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

```js
// ./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
```

```js
// ./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
```

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

Edit the `./src/App.jsx` file:

```js
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
```

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

```html
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
```

Here is how the app currently appears:

![screenshot-2](https://raw.githubusercontent.com/tsamaya/opencage-react-guide-v2/master/resources/screenshot-2.png)

### 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:

```js
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
```

Download the pin icon from [marker-icon-red.png](https://github.com/tsamaya/opencage-react-guide-v2/blob/master/public/marker-icon-red.png) and save it to `public/` folder.

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

```css
#map {
  width: auto;
  min-height: 350px;
  height: 40vmin;
}
```

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

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

and with the other results content add the map:

```js
{
  activeTab === MAP_TAB && results.length > 0 && <ResultMap response={props.response} />
}
```

There is now a map in the application:

![screenshot-3](https://raw.githubusercontent.com/tsamaya/opencage-react-guide-v2/master/resources/screenshot-3.png)

## 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](https://mapstodon.space/@arferrand) or [Twitter](https://twitter.com/arferrand) and, once again, if you read this tutorial all the way through, I'm really proud of you.

## Resources

- Opencage Data Geocoder: [https://opencagedata.com/](https://opencagedata.com/)
- Source code repository on GitHub: [https://github.com/tsamaya/opencage-react-guide-v2](https://github.com/tsamaya/opencage-react-guide-v2)
- Demo version on Netlify: [https://glowing-trifle-49c0ea.netlify.app/](https://glowing-trifle-49c0ea.netlify.app/)