If you want customers to buy from you, requiring a shipping or billing address may become part of your ReactJS application. Nearly every database course looks at postal addresses as a good baseline tutorial for how to organize your data model in normal form, but what about on the client-side?
To keep garbage out of your records you may want to take some time to address this with address validation. If you also want to improve the user experience, the HERE Geocoder Autocomplete API may help. This REST service lets you identify an address in fewer keystrokes which can help avoid spelling mistakes by providing suggestions with each character typed. There are also some powerful features like spatial and region filters to improve relevance as well.
Project
For this project, we want to create a simple address form as shown by this mockup.
As you start typing “2168 Sha” the rest of the address should be filled in automatically. We can then check the address with the HERE Geocoder API to be sure we can identify a latitude and longitude value.
Getting Started
Generating 29,542 files for what amounts to a single page form may seem like overkill, but I still recommend starting with create-react-app. The development tools like a local server with live-reloading in a browser are helpful for quick iterations. Let’s get started by running the following:
create-react-app app
cd app
npm install bootstrap axios
npm start
As you can see I’ll also be using Bootstrap as the CSS framework and axios as an HTTP client.
Thinking in React
Following the concepts from the ReactJS tutorial Thinking in React we will follow a few basic steps.
- Break the UI into a component hierarchy
- Build a static version
- Identify the minimal representation of UI state
- Identify where the state should live
- Add inverse data flow
Break the UI into a component hierarchy
Using our mockup from before, it looks like we can break the page up into a number of components.
Build a static version
Starting from the bottom, the AddressItem is a single row item consisting of a label and form element. I’m using ES6 classes and exporting the class for use by other components.
import React, { Component } from 'react';
class AddressItem extends Component {
render() {
return (
<div className="row form-group justify-content-start">
<label className="col-sm-4 col-form-label">{this.props.label}</label>
<div className="col-xl-8">
<input
type="text"
defaultValue={this.props.value}
onChange={this.props.onChange}
className="form-control"
placeholder={this.props.placeholder} />
</div>
</div>
);
}
}
export default AddressItem;
The AddressSuggest and AddressInput components both make use of the AddressItem in their render methods.
// app/src/AddressSuggest.js
class AddressSuggest extends Component {
render() {
return (
<AddressItem label="Address" value={this.props.query} placeholder="start typing" />
);
}
}
You may recognize that I've skipped the import and exports here for brevity but they are still required. You can find those in the full listing from the GitHub repository linked at the end.
// app/src/AddressInput.js
class AddressInput extends Component {
render() {
return (
<div className="card"><div className="card-body">
<AddressItem label="Street" value={this.props.street} placeholder="" readonly="true" />
<AddressItem label="City" value={this.props.city} placeholder="" readonly="true" />
<AddressItem label="State" value={this.props.state} placeholder="" readonly="true" />
<AddressItem label="Postal Code" value={this.props.code} placeholder="" readonly="true" />
<AddressItem label="Country" value={this.props.country} placeholder="" readonly="true" />
</div></div>
);
}
}
Continuing up the hierarchy, the AddressForm combines the whole input form along with submission buttons to do our validation.
// app/src/AddressForm.js
class AddressForm extends Component {
render() {
return (
<div className="container">
<AddressSuggest />
query="4168 S"
/>
<AddressInput
street="4168 Shattuck Ave"
city="Berkeley"
state="CA"
code="94704"
country="USA"
/>
<br/>
<button type="submit" className="btn btn-primary">Check</button>
<button type="submit" className="btn btn-outline-secondary">Clear</button>
</div>
);
}
}
As you can see we just hard-coded some static values in as properties to see how our form will look before we need to deal with any interactive behaviors. Up next, we need to replace some of those properties with state.
Tracking State
Up to this point we’ve only used immutable properties. Now we want to go back and start tracking state. The minimal information we want to track:
- search query entered by the user which changes over time
- postal address can be computed, but can also change over time by user input
The AddressForm is a common ancestor in the hierarchy for these two related components that we want to keep in sync. As the user starts typing text in the AddressSuggest we will query the HERE Geocoder Autocomplete API and update AddressInput.
Looking at the HERE Autocompletion JavaScript Demo we see the required parameters of query, app_id
, and app_code
. Unlike that demo, we will use axios as the HTTP client for making requests. The constant variables APP_ID_HERE and APP_CODE_HERE also need to be defined to be referenced as seen in the code below.
The AddressForm now looks like:
class AddressForm extends Component {
constructor(props) {
super(props);
const address = this.getEmptyAddress();
this.state = {
'address': address,
'query': '',
'locationId': ''
}
this.onQuery = this.onQuery.bind(this);
}
onQuery(evt) {
const query = evt.target.value;
if (!query.length > 0) {
const address = this.getEmptyAddress();
return this.setState({
'address': address,
'query': '',
'locationId': ''
})
}
const self = this;
axios.get('https://autocomplete.geocoder.api.here.com/6.2/suggest.json', {
'params': {
'app_id': APP_ID_HERE,
'app_code': APP_CODE_HERE,
'query': query,
'maxresults': 1,
}}).then(function (response) {
const address = response.data.suggestions[0].address;
const id = response.data.suggestions[0].locationId;
self.setState({
'address': address,
'query': query,
'locationId': id,
});
});
}
render() {
return (
<div class="container">
<AddressSuggest
query={this.state.query}
onChange={this.onQuery}
/>
<AddressInput
street={this.state.address.street}
city={this.state.address.city}
state={this.state.address.state}
postalCode={this.state.address.postalCode}
country={this.state.address.country}
/>
...
);
}
}
The response from the Geocoder Autocomplete includes an array of suggestions. Two valuable pieces of information there include the locationId
if we wanted to do a full geocoder lookup by id to get the latitude and longitude. Included is also an address
block which details the city, country, street, state, and postalCode for display in our form.
Inverse Data Flow
You may have noticed that for our AddressSuggest component we added a onChange={this.onQuery}
. This pushes this method down to lower-level components. Those lower level components need to respond to user input which should be easy now that we’ve passed a reference to this handler as a property as seen in the AddressSuggest component.
return (
<AddressItem
label="Address"
value={this.props.query}
onChange={this.props.onChange}
placeholder="start typing" />
);
It is worth noting that each character typed by the user triggers this event. Since each event fires off a request to the Geocoder Autocomplete service we can quickly rack up many transactions. A final solution may make more efficient use of how these events are handled or display a lookahead of more than one suggestion at a time by changing maxresults=10
.
Validation
Up to this point we’ve helped the user by using their input as suggestions for guessing the correct address with less typing and errors. Once the address is input though, now we want to check it. We need to implement the behavior of our check and clear buttons using the HERE Geocoder.
First, let’s modify our rendered elements to include a result and onClick
event callbacks.
{ result }
<button type="submit" className="btn btn-primary" onClick={this.onCheck}>Check</button>
<button type="submit" className="btn btn-outline-secondary" onClick={this.onClear}>Clear</button>
We also make sure that all of our event handlers are bound in the constructor. This makes sure that this
is an available reference. We then have methods defined for each of these cases.
// User has entered something in address suggest field
this.onQuery = this.onQuery.bind(this);
// User has entered something in address field
this.onAddressChange = this.onAddressChange.bind(this);
// User has clicked the check button
this.onCheck = this.onCheck.bind(this);
// User has clicked the clear button
this.onClear = this.onClear.bind(this);
The clear handler is pretty straightforward, just calling setState()
for returning everything to the initial state as it was when constructor originally runs. The check handler is much more involved. Let’s look at it in a few pieces. This first section is initializing the parameters for the Geocoder service. If we used the Geocoder Autocomplete to find a suitable address, we should already have a LocationId that we can use. If we don’t have that or the user entered text into the various fields – then we’ll construct a search string with whatever details are provided.
onCheck(evt) {
let params = {
'app_id': APP_ID_HERE,
'app_code': APP_CODE_HERE
}
if (this.state.locationId.length > 0) {
params['locationId'] = this.state.locationId;
} else {
params['searchtext'] = this.state.address.street
+ this.state.address.city
+ this.state.address.state
+ this.state.address.postalCode
+ this.state.address.country
}
...
}
With the parameters in place, we again use axios to fetch a response from the geocoder REST API. If we get back a promised response with a matching location we can set the state to the appropriate success or error conditions.
onCheck(evt) {
...
const self = this;
axios.get('https://geocoder.api.here.com/6.2/geocode.json',
{ 'params': params }
).then(function(response) {
const view = response.data.Response.View
if (view.length > 0 && view[0].Result.length > 0) {
const location = view[0].Result[0].Location;
self.setState({
'isChecked': 'true',
'coords': {
'lat': location.DisplayPosition.Latitude,
'lon': location.DisplayPosition.Longitude
},
'address': {
'street': location.Address.HouseNumber + ' ' + location.Address.Street,
'city': location.Address.City,
'state': location.Address.State,
'postalCode': location.Address.PostalCode,
'country': location.Address.Country
}});
} else {
self.setState(
'isChecked': true,
'coords': null
);
}
})
...
}
Getting a latitude and longitude alone is not the best indicator so it is also important to look at the MatchLevel, MatchType, and MatchQuality to evaluate whether the address is valid.
"Result": [
{
"Relevance": 1,
"MatchLevel": "houseNumber",
"MatchQuality": {
"State": 1,
"City": 1,
"Street": [
1
],
"HouseNumber": 1
},
"MatchType": "pointAddress",
...
You can learn more about the address details like this from the Geocoder Search Response documentation.
For the full source listing, please review the GitHub repository HERE-react-address-validation.
Summary
This demonstration looked into how to think about creating components with React so that we could build a smart address form with the HERE Geocoder Autocomplete API . We also did some additional validation using the HERE Geocoder API to retrieve a latitude and longitude to help us test our match quality. Street address validation for deliverability can be a complex subject but hope this gets you started.
There are many other ways to do client-side address validation which haven’t been covered here but hope that seeing how to use a Geocoder as a component can be valuable for better user experience. If you use other libraries like Formik, react-final-form or informed and have other ideas about how to do address validation with React let me know in the comments.
Top comments (0)