In this tutorial, we will discuss about integrating Opencage API into a React application.
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 favorite IDE or Text Editor.
We assume you are familiar with JavaScript. In this tutorial, we're going to use some ES6 features like arrow functions
, classes
, let
, and const
statements.
This tutorial is not about setting up a build environment for React, so for the easy use, we will use create-react-app.
As current node version, when writing this guide, is 10.12; I assume you can use npx
as it is available since version 5.2.
$ npx create-react-app opencage-react-app
it outputs :
Creating a new React app in [...]/opencage-react-app.
Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts...
yarn add v1.10.1
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 📃 Building fresh packages...
success Saved lockfile.
success Saved 11 new dependencies.
info Direct dependencies
├─ [email protected]
├─ [email protected]
└─ [email protected]
info All dependencies
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
└─ [email protected]
✨ Done in 79.89s.
Initialized a git repository.
Success! Created opencage-react-app at /Users/tsamaya/work/github/tsamaya/opencage-react-app
Inside that directory, you can run several commands:
yarn start
Starts the development server.
yarn build
Bundles the app into static files for production.
yarn test
Starts the test runner.
yarn eject
Removes this tool and copies build dependencies, configuration files
and scripts into the app directory. If you do this, you can’t go back!
We suggest that you begin by typing:
cd opencage-react-app
yarn start
Happy hacking!
Let's do the suggested commands
$ cd opencage-react-app
$ yarn start
The project is built in development mode and it opens your favorite browser on http://localhost:3000.
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/
folder
Open you IDE or Text Editor with the folder opencage-react-app
.
Edit the file ./src/App.js
replace
import logo from './logo.svg';
with
import logo from './opencage-white.svg';
The app is rebuilt and instead of the atomic react logo, you should now have a Open Cage logo.
use CTRL + C
to stop the development server.
We will now add dependencies to the project.
First the style, you can pick up your favorite CSS framework (flexbox, bootstrap or material UI), for this tutorial I picked up Bulma as it is javascript free, then no react wrapper is needed to keep this tutorial simple and focused only on opencage geocode API integration.
$ yarn add bulma
it outputs
yarn add v1.10.1
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 📃 Building fresh packages...
success Saved lockfile.
success Saved 3 new dependencies.
info Direct dependencies
├─ [email protected]
├─ [email protected]
└─ [email protected]
info All dependencies
├─ [email protected]
├─ [email protected]
└─ [email protected]
✨ Done in 8.61s.
let's create an Header component: duplicate App.js
as Header.js
Rename App.css
into Header.css
. Then edit Header.css
, we will avoid being see sick with the infinite loop animation and place the center text in the header only. The header will be a header not whole view port 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);
}
}
Edit ./src/Header.js
:
// ./src/Header.js
import React, { Component } from 'react';
import logo from './opencage-white.svg';
import './Header.css';
class Header extends Component {
render() {
return (
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
OpenCage <b>Geocoder</b> API.
</p>
</header>
);
}
}
export default Header;
Edit ./src/index.js
, adding
import 'bulma/css/bulma.css';
after
import './index.css';
now edit App.js
, we first use the Header
Component and then we prepare 2 columns.
import React, { Component } from 'react';
import Header from './Header';
class App extends Component {
render() {
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:
$ yarn add opencage-api-client leaflet classnames
- opencage-api-client is the client API 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 start the dev server with $ yarn start
For now the app looks like this
In the first column we will set up the form with the search input parameters. In the second column, we will have the results as multiple tabs, starting with the readable results (formatted address and coordinates), and a second tab with the raw JSON result from the API. As you can see in the following design we will create two main components and GeocodingForm
and GeocodingResults
create a file ./src/GeocodingForm.js
import React, { Component } from 'react';
class GeocodingForm extends Component {
constructor(props) {
super(props);
this.state = {
isLocating: false,
};
this.handleGeoLocation = this.handleGeoLocation.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSubmit = props.onSubmit;
}
handleGeoLocation() {
const geolocation = navigator.geolocation;
const p = new Promise((resolve, reject) => {
if (!geolocation) {
reject(new Error('Not Supported'));
}
this.setState({
isLocating: true,
});
geolocation.getCurrentPosition(
position => {
console.log('Location found');
resolve(position);
},
() => {
console.log('Location : Permission denied');
reject(new Error('Permission denied'));
}
);
});
p.then(location => {
this.setState({
isLocating: false,
});
this.props.onChange(
'query',
location.coords.latitude + ', ' + location.coords.longitude
);
});
}
handleInputChange(event) {
const { target } = event;
const { name } = target;
const value = target.type === 'checkbox' ? target.checked : target.value;
// console.log(name, value);
// this.setState({
// [name]: value,
// });
this.props.onChange(name, value);
}
handleSubmit(event) {
console.log('Form was submitted with state: ', this.state);
event.preventDefault();
}
render() {
const { apikey, isSubmitting, query } = this.props;
const { isLocating } = this.state;
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={this.handleInputChange}
/>
</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={this.handleInputChange}
/>
<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={this.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={this.handleSubmit}
disabled={isLocating || isSubmitting}
>
Geocode
</button>
{/* <!-- ./Button Geocode --> */}
</form>
</div>
);
}
}
export default GeocodingForm;
then create a file ./src/GeocodingResults.js
import React, { Component } from 'react';
import classnames from 'classnames';
import ResultList from './ResultList';
import ResultJSON from './ResultJSON';
const RESULT_TAB = 'RESULT_TAB';
const JSON_TAB = 'JSON_TAB';
class GeocodingResults extends Component {
constructor(props) {
super(props);
this.state = {
activeTab: RESULT_TAB,
};
}
renderTab(title, tab, icon, activeTab) {
return (
<li className={classnames({ 'is-active': activeTab === tab })}>
<a
href="/"
onClick={e => {
e.preventDefault();
this.setState({
activeTab: tab,
});
}}
>
<span className="icon is-small">
<i className={icon} aria-hidden="true" />
</span>
<span>{title}</span>
</a>
</li>
);
}
render() {
const { activeTab } = this.state;
const results = this.props.response.results || [];
return (
<div className="box results">
<div className="tabs is-boxed vh">
<ul>
{this.renderTab('Results', RESULT_TAB, 'fas fa-list-ul', activeTab)}
{results.length > 0 &&
this.renderTab('JSON Result', JSON_TAB, 'fab fa-js', activeTab)}
</ul>
</div>
{/* List of results */}
{activeTab === RESULT_TAB &&
results.length > 0 && <ResultList response={this.props.response} />}
{/* JSON result */}
{activeTab === JSON_TAB &&
results.length > 0 && <ResultJSON response={this.props.response} />}
</div>
);
}
}
export default GeocodingResults;
We need to create files ./src/ResultList.js
and ./src/ResultJSON.js
// ./src/ResultList.js
import React, { Component } from 'react';
class ResultList extends Component {
render() {
const rate = this.props.response.rate || {};
const results = this.props.response.results || [];
return (
<article className="message">
<div className="message-body">
<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';
class ResultJSON extends Component {
render() {
return (
<article className="message">
<div className="message-body">
<pre>{JSON.stringify(this.props.response, null, 2)}</pre>
</div>
</article>
);
}
}
export default ResultJSON;
To finish the first part wire the application with those two main components (GeocodingForm and GeocodingResults)
Edit the ./src/App.js
file, first the imports:
import React, { Component } from 'react';
import Header from './Header';
import GeocodingForm from './GeocodingForm';
import GeocodingResults from './GeocodingResults';
import * as opencage from 'opencage-api-client';
now add a constructor
constructor(props) {
super(props);
this.state = {
query: '',
apikey: '',
isSubmitting: false,
response: {},
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
the App handles input text changes and the submit.
So first add the handleChange
method
handleChange(key, value) {
this.setState({ [key]: value });
}
Followed by the handleSubmit
method
handleSubmit(event) {
event.preventDefault();
this.setState({ isSubmitting: true });
opencage
.geocode({ key: this.state.apikey, q: this.state.query })
.then(response => {
console.log(response);
this.setState({ response, isSubmitting: false });
})
.catch(err => {
console.error(err);
this.setState({ response: {}, isSubmitting: false });
});
}
Last touch for this first part, we add the main components in the render
method:
render() {
return (
<div>
<Header />
<div className="columns">
<div className="column is-one-third-desktop">
<GeocodingForm
apikey={this.state.apikey}
query={this.state.query}
isSubmitting={this.state.isSubmitting}
onSubmit={this.handleSubmit}
onChange={this.handleChange}
/>
</div>
<div className="column">
<GeocodingResults response={this.state.response} />
</div>
</div>
</div>
);
}
Here is what the app now looks like
In this part we will add a map tab
to be continued
Licensed under the MIT License
A copy of the license is available in the repository's LICENSE file.