Creating a Live Search Feature in React Using Axios

Dilshod Turobov

Axios is a powerful HTTP client that allows to easily implement Ajax requests in a JavaScript app. We’ve covered the basics of using Axios with React here, so you can read that first if Axios or Axios + React is all new to you.

In this tutorial we’ll be building a live search feature inside a React app with the help of Axios. Our app will allow us to do a simple movie search using the API from themoviedb.org.

This tutorial is divided into 3 section:

Initializing the App

This tutorial assumes that you have some experience using React, so we’ll skip the initializing step to save our valuable time. You can use your any favorite boilerplate, and in this tutorial we’ll simply use Create React App to initialize the app.

Once the app is initialized, let’s add axios to it:

$ yarn add axios or npm i axios

Next, copy the code below to your App component:

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

import Movies from './Movies';

class App extends Component {
  state = {
    movies: null,
    loading: false,
    value: ''
  };

  search = async val => {
    this.setState({ loading: true });
    const res = await axios(
      `https://api.themoviedb.org/3/search/movie?query=${val}&api_key=dbc0a6d62448554c27b6167ef7dabb1b`
    );
    const movies = await res.data.results;

    this.setState({ movies, loading: false });
  };

  onChangeHandler = async e => {
    this.search(e.target.value);
    this.setState({ value: e.target.value });
  };

  get renderMovies() {
    let movies = <h1>There's no movies</h1>;
    if (this.state.movies) {
      movies = <Movies list={this.state.movies} />;
    }

    return movies;
  }

  render() {
    return (
      <div>
        <input
          value={this.state.value}
          onChange={e => this.onChangeHandler(e)}
          placeholder="Type something to search"
        />
        {this.renderMovies}
      </div>
    );
  }
}

export default App;

Note: Movies is just presentational/dumb component and simply renders the data we give it. It does not touch our data.

Input Component

So, we have a controlled input element that calls onChangeHandler method when we type something in. onChangeHandler changes the value property in the state and calls the search method, giving it the input’s value as an argument.

Take the following piece of code from above:

search = async val => {
  this.setState({ loading: true });
  const res = await axios(
    `https://api.themoviedb.org/3/search/movie?query=${val}&api_key=dbc0a6d62448554c27b6167ef7dabb1b`
  );
  const movies = await res.data.results;

  this.setState({ movies, loading: false });
};

In the search method we are making a GET request to our API to get the movies we want. Once we get the results we update the component’s state via setState. And when we change the state via setState, the component re-renders with the changed state.

Simple as that!

Preventing Unnecessary Requests

You may notice that we send requests every time when we update the input. This can lead to an overload of requests, especially when we receive large responses.

To see this problem in action, open the network tab in your browser’s DevTools. Clear the network tab. Type some movie’s name in the input.

Too many requests, as can be seen from DevTools

As you see we’re downloading all the data every time a keystroke happens. To solve this issue let’s create a utils.js file in the src directory:

$ cd src
$ touch utils.js

Copy the following code into utils.js:

import axios from 'axios';

const makeRequestCreator = () => {
  let token;

  return (query) => {
    // Check if we made a request
    if(token){
      // Cancel the previous request before making a new request
      token.cancel()
    }
    // Create a new CancelToken
    token = axios.CancelToken.source()
    try{
      const res = await axios(query, {cancelToken: cancel.token})
      const result = data.data
      return result;
    } catch(error) {
        if(axios.isCancel(error)) {
          // Handle if request was cancelled
          console.log('Request canceled', error.message);
        } else {
          // Handle usual errors
          console.log('Something went wrong: ', error.message)
        }
    }
  }
}

export const search = makeRequestCreator()

Change the App component as well to make use of our new utility function:

...
import { search } from './utils'

class App extends Component {
    .....

    search = async val => {
        this.setState({ loading: true });
        // const res = await axios(
        const res = await search(
          `https://api.themoviedb.org/3/search/movie?query=${val}&api_key=dbc0a6d62448554c27b6167ef7dabb1b`
        );
        const movies = res.results;

        this.setState({ movies, loading: false });
      };
    ...

What’s happening there now?

Axios has so called cancel tokens that allow us to cancel requests.

In makeRequestCreator we create a variable called token. Then with a request, if the token variable exists we call its cancel method to cancel the previous request. Then we assign token a new CancelToken. After that we make a request with the given query and return the result.

If something goes wrong we catch the errors in the catch block and we can check and handle whether a request was cancelled or not.

Let’s see what happens in the network tab now:

Screenshot: much better performance!

As you see we downloaded only one response. Now our users pay for only what they use.

Caching HTTP Requests and Responses

If we type the same text in the input multiple times, again, we make a new request each time.

Let’s fix this. We will change our utility function in utils.js a little bit:

const resources = {};

const makeRequestCreator = () => {
  let cancel;

  return async query => {
    if (cancel) {
      // Cancel the previous request before making a new request
      cancel.cancel();
    }
    // Create a new CancelToken
    cancel = axios.CancelToken.source();
    try {
      if (resources[query]) {
        // Return result if it exists
        return resources[query];
      }
      const res = await axios(query, { cancelToken: cancel.token });

      const result = res.data.results;
      // Store response
      resources[query] = result;

      return result;
    } catch (error) {
      if (axios.isCancel(error)) {
        // Handle if request was cancelled
        console.log('Request canceled', error.message);
      } else {
        // Handle usual errors
        console.log('Something went wrong: ', error.message);
      }
    }
  };
};

Here we created a resources constant which keeps our downloaded responses. And when we are doing a new request we first check if our resources object has a result for this query. If it does, we just return that result. If it doesn’t have a suitable result we make a new request and store the result in resources. Easy enough!

Let’s summarize everything in a few words. Every time when we type something in the input:

  • We cancel the previous request, if any.
  • If we already have a previous result for what we typed we just return it without making a new request.
  • If we don’t have that result we make a new one and store it.

If you’re interested, you can find a Redux version of this app in this repo

Conclusion

Congrats 🎉🎉🎉! We’ve built a live search feature which doesn’t download unnecessary responses as well as caches responses. I hope you’ve learned a thing or two about how to build an efficient live search feature in React with the help of Axios.

Now, our users spend as little traffic data as possible. If you enjoyed this tutorial, please share it out! 😄

You can find final result and source code here for this post in this CodeSandbox.

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#