Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Search with axios: cancel previous request when new character

Tags:

vue.js

axios

I have a searchbar, the results are updated on each letter, but when the user types 3 letters quickly for example, the previous requests are not cancelled so there is a ugly delay before he get his results.

I have read this https://github.com/axios/axios#cancellation and maybe I am a bit tired but I struggle very much adding it in my project. It is almost doing the opposite effect, it now takes forever. Do you have any suggetsion or maybe do you recommend a good tutorial so that I could understand this?

<input v-model="query" type="text" class="form-control" placeholder="Runner name or description"
                   aria-label="Runner name or description"
                   aria-describedby="basic-addon2">


watch: {
            query: {
                handler: _.debounce(function () {
                    this.newSearch()
                }, 100)
            }
        },
methods: {
            searchItems() {
                let filters = {
                    q: this.query
                };

                const CancelToken = axios.CancelToken;
                const source = CancelToken.source();

                axios.post('/Items/find', filters, {
                    cancelToken: source.token
                })
                .catch(function(thrown) {
                    if (axios.isCancel(thrown)) {
                        console.log('Request canceled', thrown.message);
                    }
                })
                .then(
                    response => {
                        if(this.Items!=null){
                            response.data.forEach(element => {
                                this.Items.push(element);
                            });
                        }
                        else
                            this.Items=response.data;
                    }
                );
            },
            newSearch(){
                const CancelToken = axios.CancelToken;
                const source = CancelToken.source();
                source.cancel('Cancel previous request');

                this.countItems();
                this.searchItems();
            },
            showMore(){
                this.startindex = this.startindex+this.nbrows;
                this.searchItems();
            },
            countItems(){
                this.countItems=10;
                let filters = {
                    q: this.query
                };
                axios.post('/Items/count', filters).then(
                    response => {
                        this.countItems=response.data;
                    }
                );
            }
        }
like image 224
Maxiss Avatar asked Apr 24 '19 16:04

Maxiss


People also ask

How do I cancel an old API request?

If you want to cancel a Fetch request, you need to use the AbortController API. You can use the constructor to create a new AbortController object. It has a read-only property AbortController.

How does Axios allow cancellation request and request timeout?

If Axios has already sent the request, all cancel does is cause your Axios request to error out and ignore any response the server sends after cancellation.

What is cancel token in Axios?

The axios cancel token API is based on the withdrawn cancelable promises proposal. This API is deprecated since v0.22.0 and shouldn't be used in new projects. You can create a cancel token using the CancelToken. source factory as shown below: const CancelToken = axios.


2 Answers

I was able to get this to work.. The trick was to check if the cancel token existed before kicking off the API call, among other things.. I had to move the CancelToken and cancel variables outside of the Vue object/component..


This example searches GitHub for repositories...

var cancel;
var CancelToken = axios.CancelToken;

new Vue({
  el: "#app",
  data: {
    query: "",
    results: "",
    isLoading: false
  },
  methods: {
    clear() {
      this.isLoading = false;
      this.results = "";
      this.query = "";
    },

    handleSearch: _.debounce(function() {
      this.preApiCall();
    }, 300),

    preApiCall() {
      if (cancel != undefined) {
        cancel();
        console.log("cancelled");
      }
      this.apiCall(this.query);
    },

    apiCall(query) {
      if (query !== "") {
        this.isLoading = true;
        axios({
          method: "get",
          url: "https://api.github.com/search/repositories",
          cancelToken: new CancelToken(function executor(c) {
            cancel = c;
          }),
          params: {
            q: query
          }
        }).then(res => {
          this.results = JSON.parse(JSON.stringify(res.data.items));
          this.isLoading = false;
        }).catch(err => {
          this.results = err.message;
          throw Error(err.message);
          this.isLoading = false;
        });
      } else {
        this.clear();
      }
    }
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

<div id="app">
  <input v-model="query" @keyup.stop="handleSearch" type="text" class="form-control" placeholder="Search">
  <button @click.stop="clear">Clear</button>
  <div v-if="isLoading">Loading...</div>
  <ul v-if="results !== ''">
    <li v-for="(r, index) in results" :key="index">
      {{ r.name }}
    </li>
  </ul>
</div>

[CodePen mirror]


Cancelled requests:

enter image description here

like image 146
Matt Oestreich Avatar answered Sep 23 '22 05:09

Matt Oestreich


The problem is that you're creating a new canceltoken in the newSearch and canceling that instead of the original one. If you save the source on the Vue component you can check in newSearch if it exists and only cancel then. When the post Promise has completed you delete the source again so you can't cancel when it's not needed (or possible).

{
  searchItems() {
      let filters = {
          q: this.query
      };

      const CancelToken = axios.CancelToken;
      this.searchItemsSource = CancelToken.source();

      axios.post('/Items/find', filters, {
          cancelToken: this.searchItemsSource.token
      })
      .catch(function(thrown) {
          if (axios.isCancel(thrown)) {
              console.log('Request canceled', thrown.message);
          }
      })
      .then(
          response => {
              this.searchItemsSource = undefined;
              if(this.Items!=null){
                  response.data.forEach(element => {
                      this.Items.push(element);
                  });
              }
              else
                  this.Items=response.data;
          }
      );
  },
  newSearch(){
      if (this.searchItemsSource) {
          this.searchItemsSource.cancel('Cancel previous request');
      }
      this.countItems();
      this.searchItems();
  },
}

A side note about this code; I'd actually move the canceling of the previous request into the searchItems method since it never makes sense to have two running call's at the same time. It would look more like this:

{
  searchItems() {
      let filters = {
          q: this.query
      };

      if (this.searchItemsSource) {
          this.searchItemsSource.cancel('Cancel previous request');
      }
      const CancelToken = axios.CancelToken;
      this.searchItemsSource = CancelToken.source();

      axios.post('/Items/find', filters, {
          cancelToken: this.searchItemsSource.token
      })
      .catch(function(thrown) {
          if (axios.isCancel(thrown)) {
              console.log('Request canceled', thrown.message);
          }
      })
      .then(
          response => {
              this.searchItemsSource = undefined;
              if(this.Items!=null){
                  response.data.forEach(element => {
                      this.Items.push(element);
                  });
              }
              else
                  this.Items=response.data;
          }
      );
  },
  newSearch(){
      this.countItems();
      this.searchItems();
  },
}

At this point you can ask yourself if the newSearch method is needed at all, you might remove it and move the countItems call into searchItems as well. This all depends on the rest of your code and functionality though.

like image 43
red-X Avatar answered Sep 25 '22 05:09

red-X