Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reactjs and redux - How to prevent excessive api calls from a live-search component?

I have created this live-search component:

class SearchEngine extends Component {
  constructor (props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.handleSearch = this.handleSearch.bind(this);
  }
  handleChange (e) {
      this.props.handleInput(e.target.value); //Redux
  }
  handleSearch (input, token) {
     this.props.handleSearch(input, token) //Redux
  };
  componentWillUpdate(nextProps) {
      if(this.props.input !== nextProps.input){
          this.handleSearch(nextProps.input,  this.props.loginToken);
      }
  }
  render () {
      let data= this.props.result;
      let searchResults = data.map(item=> {
                  return (
                      <div key={item.id}>
                              <h3>{item.title}</h3>
                              <hr />
                              <h4>by {item.artist}</h4>
                              <img alt={item.id} src={item.front_picture} />
                      </div>
                  )
              });
      }
      return (
          <div>
              <input name='input'
                     type='text'
                     placeholder="Search..."
                     value={this.props.input}
                     onChange={this.handleChange} />
              <button onClick={() => this.handleSearch(this.props.input, this.props.loginToken)}>
                  Go
              </button>
              <div className='search_results'>
                  {searchResults}
              </div>
          </div>
      )
}

It is part of a React & Redux app I'm working on and is connected to the Redux store. The thing is that when a user types in a search query, it fires an API call for each of the characters in the input, and creating an excessive API calling, resulting in bugs like showing results of previous queries, not following up with the current search input.

My api call (this.props.handleSearch):

export const handleSearch = (input, loginToken) => {
  const API= `https://.../api/search/vector?query=${input}`;
  }
  return dispatch => {
      fetch(API, {
          headers: {
              'Content-Type': 'application/json',
              'Authorization': loginToken
          }
      }).then(res => {
          if (!res.ok) {
              throw Error(res.statusText);
          }
          return res;
      }).then(res => res.json()).then(data => {
          if(data.length === 0){
              dispatch(handleResult('No items found.'));
          }else{
              dispatch(handleResult(data));
          }
      }).catch((error) => {
          console.log(error);
         });
      }
};

My intention is that it would be a live-search, and update itself based on user input. but I am trying to find a way to wait for the user to finish his input and then apply the changes to prevent the excessive API calling and bugs.

Suggestions?

EDIT:

Here's what worked for me. Thanks to Hammerbot's amazing answer I managed to create my own class of QueueHandler.

export default class QueueHandler {

constructor () { // not passing any "queryFunction" parameter
    this.requesting = false;
    this.stack = [];
}

//instead of an "options" object I pass the api and the token for the "add" function. 
//Using the options object caused errors.

add (api, token) { 
    if (this.stack.length < 2) {
        return new Promise ((resolve, reject) => {
            this.stack.push({
                api,
                token,
                resolve,
                reject
            });
            this.makeQuery()
        })
    }
    return new Promise ((resolve, reject) => {
        this.stack[1] = {
            api,
            token,
            resolve,
            reject
        };
        this.makeQuery()
    })

}

makeQuery () {
    if (! this.stack.length || this.requesting) {
        return null
    }

    this.requesting = true;
// here I call fetch as a default with my api and token
    fetch(this.stack[0].api, {
        headers: {
            'Content-Type': 'application/json',
            'Authorization': this.stack[0].token
        }
    }).then(response => {
        this.stack[0].resolve(response);
        this.requesting = false;
        this.stack.splice(0, 1);
        this.makeQuery()
    }).catch(error => {
        this.stack[0].reject(error);
        this.requesting = false;
        this.stack.splice(0, 1);
        this.makeQuery()
    })
}
}

I made a few changes in order for this to work for me (see comments).

I imported it and assigned a variable:

//searchActions.js file which contains my search related Redux actions

import QueueHandler from '../utils/QueueHandler';

let queue = new QueueHandler();

Then in my original handleSearch function:

export const handleSearch = (input, loginToken) => {
  const API= `https://.../api/search/vector?query=${input}`;
  }
  return dispatch => {
    queue.add(API, loginToken).then...  //queue.add instead of fetch.

Hope this helps anyone!

like image 416
Amiry Avatar asked Nov 11 '17 13:11

Amiry


People also ask

How do I stop continuous api calls in React?

— To avoid too many requests, you can fetch data in delay with await and loop and update data using setTimeout().

How do I stop multiple api calls?

On clicking on tabs you interchange state and you use ngOnInit() method every time when a state is changed to get the data.In this way, you can reduce API calls by using nested routing.


1 Answers

I think that they are several strategies to handle the problem. I'm going to talk about 3 ways here.

The two first ways are "throttling" and "debouncing" your input. There is a very good article here that explains the different techniques: https://css-tricks.com/debouncing-throttling-explained-examples/

Debounce waits a given time to actually execute the function you want to execute. And if in this given time you make the same call, it will wait again this given time to see if you call it again. If you don't, it will execute the function. This is explicated with this image (taken from the article mentioned above):

enter image description here

Throttle executes the function directly, waits a given time for a new call and executes the last call made in this given time. The following schema explains it (taken from this article http://artemdemo.me/blog/throttling-vs-debouncing/):

enter image description here

I was using those first techniques at first but I found some downside to it. The main one was that I could not really control the rendering of my component.

Let's imagine the following function:

function makeApiCall () {
  api.request({
    url: '/api/foo',
    method: 'get'
  }).then(response => {
    // Assign response data to some vars here
  })
}

As you can see, the request uses an asynchronous process that will assign response data later. Now let's imagine two requests, and we always want to use the result of the last request that have been done. (That's what you want in a search input). But the result of the second request comes first before the result of the first request. That will result in your data containing the wrong response:

1. 0ms -> makeApiCall() -> 100ms -> assigns response to data
2. 10ms -> makeApiCall() -> 50ms -> assigns response to data

The solution for that to me was to create some sort of "queue". The behaviour of this queue is:

1 - If we add a task to the queue, the task goes in front of the queue. 2 - If we add a second task to the queue, the task goes in the second position. 3 - If we add a third task to the queue, the task replaces the second.

So there is a maximum of two tasks in the queue. As soon as the first task has ended, the second task is executed etc...

So you always have the same result, and you limit your api calls in function of many parameters. If the user has a slow internet connexion, the first request will take some time to execute, so there won't be a lot of requests.

Here is the code I used for this queue:

export default class HttpQueue {

  constructor (queryFunction) {
    this.requesting = false
    this.stack = []
    this.queryFunction = queryFunction
  }

  add (options) {
    if (this.stack.length < 2) {
      return new Promise ((resolve, reject) => {
        this.stack.push({
          options,
          resolve,
          reject
        })
        this.makeQuery()
      })
    }
    return new Promise ((resolve, reject) => {
      this.stack[1] = {
        options,
        resolve,
        reject
      }
      this.makeQuery()
    })

  }

  makeQuery () {
    if (! this.stack.length || this.requesting) {
      return null
    }

    this.requesting = true

    this.queryFunction(this.stack[0].options).then(response => {
      this.stack[0].resolve(response)
      this.requesting = false
      this.stack.splice(0, 1)
      this.makeQuery()
    }).catch(error => {
      this.stack[0].reject(error)
      this.requesting = false
      this.stack.splice(0, 1)
      this.makeQuery()
    })
  }
}

You can use it like this:

// First, you create a new HttpQueue and tell it what to use to make your api calls. In your case, that would be your "fetch()" function:

let queue = new HttpQueue(fetch)

// Then, you can add calls to the queue, and handle the response as you would have done it before:

queue.add(API, {
    headers: {
        'Content-Type': 'application/json',
        'Authorization': loginToken
    }
}).then(res => {
    if (!res.ok) {
        throw Error(res.statusText);
    }
    return res;
}).then(res => res.json()).then(data => {
    if(data.length === 0){
        dispatch(handleResult('No vinyls found.'));
    }else{
        dispatch(handleResult(data));
    }
}).catch((error) => {
    console.log(error);
   });
}
like image 198
Hammerbot Avatar answered Oct 27 '22 00:10

Hammerbot