Search with axios: cancel previous request when new character

12,490

Solution 1

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

Solution 2

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.

Share:
12,490

Related videos on Youtube

Maxiss
Author by

Maxiss

Updated on August 17, 2022

Comments

  • Maxiss
    Maxiss over 1 year

    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;
                        }
                    );
                }
            }
    
    • Matt Oestreich
      Matt Oestreich about 5 years
      Have you tried increasing the debounce?
  • Maxiss
    Maxiss about 5 years
    Thanks a lot for this veru comprehensive answer, indeed I just moved the countitems into searchItems as well. Much easier to read :)