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
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
Start hacking
First part
Let’s do the suggested commands above
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/.
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'
with
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, 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
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
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);
}
}
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
Edit ./src/main.jsx
, adding
import 'bulma/css/bulma.css'
instead of
import './index.css'
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
Now add packages dependencies like the OpenCage API client, LeafletJS, and classnames:
npm install -S opencage-api-client leaflet react-leaflet classnames
- opencage-api-client is the client library for Opencage Geocoder API
- LeafletJS is the well-known web mapping API
- 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:
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.
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
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
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> </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
// ./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:
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>
:
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
Here is how the app currently appears:
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='© <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 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;
}
Back in ./src/GeocodingResuls.jsx
add the tab in the ul
section:
{
results.length > 0 && renderTab('JSON Result', JSON_TAB, 'fab fa-js', activeTab)
}
and with the other results content add the map:
{
activeTab === MAP_TAB && results.length > 0 && <ResultMap response={props.response} />
}
There is now a map in the application:
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
- Opencage Data Geocoder: https://opencagedata.com/
- Source code repository on GitHub: https://github.com/tsamaya/opencage-react-guide-v2
- Demo version on Netlify: https://glowing-trifle-49c0ea.netlify.app/
Top comments (0)