loading...

How To Write a Search Component with Suggestions in React

sage911 profile image Stefan Age ・4 min read

Github Repo
This example uses syntax that requires transpiling. See repo for full babel configuration.

Providing search suggestions is a great way to improve user experience. It can save time as well as guide users who are not exactly sure what they are looking for.

With our 'why' identified we can move on to the implementation. But how do we implement suggestions in JavaScript?

As is the case in most problem solving exercises, a good place to start is by asking the right questions:

  • Is there a library that solves what I'm trying to accomplish and should I use it? A quick google search returns options like autocomplete.js, but there is valuable insight to be gained by writing our own.

  • What HTML elements are we operating with? Looks like we could use <form>, <input/>, <ul>.

We've decided to write our own.

What we'll need:

  • A source of information. We're after a collection of values to compare against what our user has entered (we'll source an API response, but you could also use a local array of values).

  • An HTTP client. Allows us to make requests to specific endpoints to GET the data we're looking for. I chose axios since it gives us some additional features over the Fetch API such as automatic parsing of the data it receives.

  • A smart/container component that makes API calls referencing a controlled input.

  • A presentational (stateless functional) component for displaying the results.

Lets start with our container, Search:

import React, { Component } from 'react'

class Search extends Component {
 state = {
   query: '',
 }

 handleInputChange = () => {
   this.setState({
     query: this.search.value
   })
 }

 render() {
   return (
     <form>
       <input
         placeholder="Search for..."
         ref={input => this.search = input}
         onChange={this.handleInputChange}
       />
       <p>{this.state.query}</p>
     </form>
   )
 }
}

export default Search

You'll notice that as you type into the input field, Search re-renders and our input's value is shown below. Thanks to refs we can select the input element and do useful things such as getting its value or invoking DOM events like focus (this.search.focus()).

Next, let's wire in an API. Here we'll use MusicGraph, a database of music information. Grab an API key here.

We'll use axios to create a getInfo method (check your API's docs on how to structure your request URL):

  getInfo = () => {
    axios.get(`${API_URL}?api_key=${API_KEY}&prefix=${this.state.query}&limit=7`)
      .then(({ data }) => {
        this.setState({
          results: data.data
        })
      })
  }

Axios .get returns a promise meaning it does not block the rest of the application from executing while it waits for an API response. Here we are making a request to the MovieGraph API's artist endpoint, using the magic of refs to populate the prefix query parameter. (API_URL and API_KEY are being defined above the class definition, see next snapshot.)

Let's also tweak the handleInputChange method. We don't need to make an API call for every single onChange event, or when the input is cleared.

The full component so far:

import React, { Component } from 'react'
import axios from 'axios'

const { API_KEY } = process.env
const API_URL = 'http://api.musicgraph.com/api/v2/artist/suggest'

class Search extends Component {
  state = {
    query: '',
    results: []
  }

  getInfo = () => {
    axios.get(`${API_URL}?api_key=${API_KEY}&prefix=${this.state.query}&limit=7`)
      .then(({ data }) => {
        this.setState({
          results: data.data // MusicGraph returns an object named data, 
                             // as does axios. So... data.data                             
        })
      })
  }

  handleInputChange = () => {
    this.setState({
      query: this.search.value
    }, () => {
      if (this.state.query && this.state.query.length > 1) {
        if (this.state.query.length % 2 === 0) {
          this.getInfo()
        }
      } 
    })
  }

  render() {
    return (
      <form>
        <input
          placeholder="Search for..."
          ref={input => this.search = input}
          onChange={this.handleInputChange}
        />
        <p>{this.state.query}</p>
      </form>
    )
  }
}

export default Search

If you have React Dev Tools installed in your browser you can watch the state of Search change as the API calls complete:
React Dev Tools

Home stretch. Now to render our results to the DOM.

As mentioned at setup let's create our presentational component, Suggestions.

import React from 'react'

const Suggestions = (props) => {
  const options = props.results.map(r => (
    <li key={r.id}>
      {r.name}
    </li>
  ))
  return <ul>{options}</ul>
}

export default Suggestions

We've setup Suggestions to expect a prop named results.

Let's render our Suggestions component:

import React, { Component } from 'react'
import axios from 'axios'
import Suggestions from 'components/Suggestions'

const { API_KEY } = process.env
const API_URL = 'http://api.musicgraph.com/api/v2/artist/suggest'

class Search extends Component {
  state = {
    query: '',
    results: []
  }

  getInfo = () => {
    axios.get(`${API_URL}?api_key=${API_KEY}&prefix=${this.state.query}&limit=7`)
      .then(({ data }) => {
        this.setState({
          results: data.data
        })
      })
  }

  handleInputChange = () => {
    this.setState({
      query: this.search.value
    }, () => {
      if (this.state.query && this.state.query.length > 1) {
        if (this.state.query.length % 2 === 0) {
          this.getInfo()
        }
      } else if (!this.state.query) {
      }
    })
  }

  render() {
    return (
      <form>
        <input
          placeholder="Search for..."
          ref={input => this.search = input}
          onChange={this.handleInputChange}
        />
        <Suggestions results={this.state.results} />
      </form>
    )
  }
}

export default Search

Try it out:

A working autocomplete

It works!

Once routing is setup, we can wrap each result in an anchor/react-router Link component. But that is a topic for another post. Until then, I hope this was helpful to someone!

Discussion

pic
Editor guide
Collapse
abhi4pr profile image
abhi4pr

Sir i got an error (Unhandled Rejection (TypeError): Cannot read property 'data' of undefined).
in this line (results:data.data).

I created my own API.

app.get('/Search',function(req,res){
con.query("select * from webtable where username like '%username%'",
function(err,searchrs){
console.log("Search results:",searchrs);
res.json(searchrs);
})
});

What corrections this API need ?
How to resolve that above error ?

Please answer me soon
Thanks in advance.

Collapse
wbmedia profile image
Antonio Nicasio

out need to check your output response because data is not declared the error says all
Cannot read property 'data' of undefined

Collapse
fabriciohendrix profile image
Fabrício Santos

Hey Stefan, How and where can I use my API_KEY ?

Collapse
sage911 profile image
Stefan Age Author

Hi Fabricio,

You can define your API KEY in your shell instance by running the command:
$ export MGRPH_KEY=yourAPIkey. Just make sure to start the app in the same window.

You can also utilize a .env file.

Lastly, you can add it as part of your start script for example

package.json

"scripts":  {
    "start": "MGRPH_KEY=yourAPI-KEY react-scripts start"
}
Collapse
plaidsanne profile image
Plaidsanne

Hello,

musicgraph seems to be down and not responsive when testing API's. I was wondering is this the case for you as well.

Collapse
killagorillafb profile image
Frankie Barrios

Musicgraph's site is no longer available. Use a different API if still looking at this in 2019.

Collapse
naveensomanna profile image
naveensomanna

sir how to hide that api list

Collapse
sage911 profile image
Stefan Age Author

There are a few ways you could hide the Suggestions component. I think a simple solution would be to create a state attribute called showSuggestions. Then you could show/hide it using React's short circuit syntax. In the render method add { this.state.showSuggestions && <Suggestions results={this.state.results} /> }. Then just use setState whenever you want to change the value of showSuggestions, for example when the user leaves the input field.

Collapse
naveensomanna profile image
naveensomanna

sir i am really confusing about setState Assynchronous function (its keep on calling Api even if setState is not change )

Collapse
naveensomanna profile image
naveensomanna

i hide the results but if i tried again in its shows no results

Collapse
naveensomanna profile image
naveensomanna

thank you sir

Collapse
kobello profile image
Matt Layden

Hey,
How can I get this to work with a Fetch API? I am using react-router and am able to type into the search bar and re-render with results that were fetched by "tags" which is a key in the json. For example, if I type the word "hip" in my search bar the page will re-render with videos that have the tag "hip" in their json.

However, I can't seem to connect this code you have here with what I am using and I think it is because I do not need to fetch from my navbar.

Thanks
Matt

Collapse
sage911 profile image
Stefan Age Author

Hey Matt - Do you have an example or sandbox I could look at? It should work pretty much identically with Fetch.

Collapse
bossajie profile image
Boss A

have you tried this with multiple component like, in the table and on every row, you will put this component with suggestion in react.

Collapse
sage911 profile image
Stefan Age Author

Hi Ajie I haven't tried that personally but don't see why it wouldn't work.