How to build an auto-suggest field using React and Express

One of my latest projects is a map application that has a search form in it. In this form is a field where you can type in a few letters and it will bring up the cities in the US, along with the state abbreviation concatenated with it. In other words – an autocomplete field of US cities.

I’m using ReactJS for the front end, and ExpressJS for the back. I really like the fact that both ends is written in the same language – so you don’t have to switch context. JavaScript has come a long way – and I must say it has been pretty cool to work with.

The Express Backend

Let’s start with creating the backend. What we’re trying to do is a “mini” API of sorts. Where we can pass in a term, and will bring us results. Express is perfect for our scenario.

I found a couple of good resources that will contain the data that we’ll serve. For the US cities, I found this Gist which has them all, paired with the states their in: https://gist.github.com/Lwdthe1/81818d30d23f012628aac1cdf672627d

But notice that the states are in full form. I like it better when they show the abbreviation. So here is another Gist that does that: https://gist.github.com/mshafrir/2646763

So I create a couple of constants so I can loop through and map them as we during search.

const cityStates = [
    {'city': 'Abbeville', 'state': 'Louisiana'},
    {'city': 'Aberdeen', 'state': 'Maryland'},
    {'city': 'Aberdeen', 'state': 'Mississippi'}
    ...
]
const statesHash = {
    "AL": "Alabama",
    "AK": "Alaska",
    "AS": "American Samoa",
    ...
}

Then we create a function called search – where we accept the request and response from Express. Here we do the query and mapping as shown below:

let search = (req,res)=>{
    let term = req.query.term;
    let result = [];
    cityStates.map((item,index)=>{
        var itemCompare = item.city.toLowerCase().trim();
        var termCompare = term.toLowerCase().trim();
        if(itemCompare.indexOf(termCompare) != -1){
            var out = {};
            out.city = item.city;
            for(hash in statesHash){
                if(statesHash[hash] == item.state){
                    out.state = hash;
                }
            }
            result.push(out);
        }
    })
    res.set({'Content-Type' : 'application/json; charset=UTF-8'});
    res.write(JSON.stringify(result));
    res.end();
}

We return the response object above as a JSON string. Which we can parse easily from our front end. Don’t forget to setup our route in Express:

const express = require('express')
const cities = require('./cities');
express().get('/search-city/', (req, res) => {
    cities.search(req, res);
})

So you see the first parameter in our .get function – is the actual route. This is so that we can do something like below in our endpoint:

/search-city?term=miami

All we need to do is make the “term” dynamic, and we should have different results. Now let’s move forward.

The React Front End

Okay, now we get to the meat of the application. React is a different way of looking at front end development. Everything is inside JavaScript – so there is really no “HTML”. Or at least, no .html files. I’m not going into detail on how that works, I’m just going to assume you know.

Let’s import react and axios.

import React from 'react';
import axios from 'axios';

Axios makes it easy for us to write AJAX calls. Then, let’s create our Form – and its a class component. Let’s call it “Search”. We’re using “Controlled Components” style – which has more flexibility when working with forms.

class Search extends React.Component {
    constructor(props) {
       super(props)
    }
    render (){
        return <form></form>
    }
}

Now that we our form, let’s add our state. We need 2: “near” and “citySuggestions”.

constructor(props) {
   this.state = {
       near : '',
       citySuggestions : []
   };
   this.nearChanged = this.nearChanged.bind(this);
}

Our state “near” – is what we’re putting in our input field. In React’s controlled components, we have to define input field values in state make it the single source of truth. This might be a little cumbersome – especially compared to other frameworks – where you can simply define a “model”.

On top of that, any mutations to the state has to be done through a handler. So let’s create that now:

nearChanged(event){
        var self = this;
        var val = event.target.value;
        this.setState({near:val});
        if(val.length > 3){
            axios.get(EXPRESSURL+'/search-city?term='+val)
               .then(function (response) {
                   self.setState({
                      citySuggestions : response.data
                   })
                })
                .catch(function (error) {
                   console.log(error);
                })
        }else{
            self.setState({
                citySuggestions : []
            })
        }
    }

So our nearChanged function is what mutates our “citySuggestions” state. And this is done through an AJAX call to our Express server. We’re also waiting until the length of the entered text is more than 3 – for a more valid search term. Otherwise, we set the citySuggestions to an empty array.

Now let’s add the actual field inside our form, including and binging onChange and other attributes.

render (){
        return <form>
                    <input
                        name="near"
                        type="text"
                        value={this.state.near}
                        autoComplete="off"
                        onChange={this.nearChanged}
                        >
                    </input>
                    {this.citySuggest()}
                </form>
    }

We also added “this.citySuggest()” above – which is simply a function that return an un-ordered list of items. These items are the suggestions that came back from our AJAX call. Let’s move on to the actual function:

citySuggest(){
     var self = this;
     return (<ul className="citySuggest">
           {this.state.citySuggestions.map(function(item,index){
           return <li onClick={self.cityClicked.bind(self,index)}
                    key={index}>{item.city}</li>
                    })}
              </ul>)
    }

Each list item is simply mapped to each states “citySuggestions”. Also, we have attached a listener to it when it’s clicked. We call this event “cityClicked”. Notice that we use .bind(self,index), where “self” is really “this” – which we need inside our class to use, and “index” is to know which one is actually clicked.

 cityClicked(index){
        var ct = this.state.citySuggestions[index].city;
        var st = this.state.citySuggestions[index].state;
        this.setState({
            citySuggestions : [],
            near : ct + ', ' + st
        })
    }

Our cityClicked() function is quite simple. We simply look for the which citySuggestions item was clicked by the “index” that was passed. It’s actually in this function that we set specific values – such as hidden fields etc. But for now – we’re setting the “near” state as well. This is so our input field will reflect what was clicked from the list.

.citySuggest {
    background: #fff;
    position: fixed;
    width: 100%;
    border: 1px solid #ccc;
    box-shadow: 0 2px 2px rgba(0,0,0,.23);
}
.citySuggest li {
    padding: 10px 15px;
}
.citySuggest li:hover {
    background:#ebebeb;
    cursor: pointer;
}

I added very minimal styling – which I’ll leave open to your taste. But for now, that’s a working input field.

Leave your thoughts below.

1 Comments

Leave a Reply to david Cancel reply