2

So, I have this inputfield, when you type something in it, it makes an API call to our backend, the problem I have is:

Let's say we are typing test, I will make 4 calls out of this:

  • t
  • te
  • tes
  • test

because the call for 't' takes much longer than 'test', the data from 't' will be loaded in at the end. which means I don't get the requested data from 'test'.

enter image description here

My question is, is there any way you can cancel the previous request? ('t', 'te', 'tes') and only let your last call get through? Or is this just optimizing performance of the API speed?

I've already tried with a timeout of half a second but the problem still remains sometimes.

Yorbjörn
  • 356
  • 3
  • 21
  • You've tagged this [tag:axios], so did you look up how to cancel a request in Axios? – jonrsharpe Jul 06 '20 at 07:49
  • as mentioned before, its called "debouncing". basically, you set a timeout for each change (lets say for 500 ms), and at the end of this time out you are checking if the value hasn't changed, and only then the request is going through the API. read more online, i'm sure react has some dobounce mechanism built in. – vlad katz Jul 06 '20 at 07:51
  • @vladkatz It's not, I'll add an answer soon – HMR Jul 06 '20 at 07:57
  • @HMR agreed that what he has requested is not debouncing, but the behavior that he requested is something that debouncing can result in better performance and less API calls – vlad katz Jul 06 '20 at 08:05
  • @vladkatz See answer, debounce will not consistently solve making async request based on user input: `the order the async results resolve may not be the order the user gave the inputs` – HMR Jul 06 '20 at 08:13
  • Sorry for my late response, thanks so much! helped me – Yorbjörn Jul 06 '20 at 08:33

2 Answers2

3

When you make an async call on user input then the order the async results resolve may not be the order the user gave the inputs (so not a debounce issue). When user types s then you make an async call with s, then the user types e and you make an async call with se. There are now 2 async calls unresolved, one with s and one with se.

Say the s call takes a second and the se call takes 10 milliseconds then se resolves first and UI is set to result of se but after that s resolves and the UI is set to result of s. You now have an inconsistent UI.

One way to solve this is with debounce and hope you'll never get async calls that last longer than the debounce time but there is no guarantee. Another way is cancel older requests but that is too much of a pain to implement and not supported by all browsers. The way I show below is just to reject the async promise when a newer request was made. So when user types s and e requests s and se are made but when s resolves after se it will be rejected because it was replaced with a newer request.

const REPLACED = 'REPLACED';
const last = (fn) => {
  const current = { value: {} };
  return (...args) => {
    const now = {};
    current.value = now;
    return Promise.resolve(args)
      .then((args) => fn(...args))
      .then((resolve) =>
        current.value === now
          ? resolve
          : Promise.reject(REPLACED)
      );
  };
};
const later = (value, time) =>
  new Promise((resolve) =>
    setTimeout(() => resolve(value), time)
  );
const apiCall = (value) =>
  //slower when value length is 1
  value.length === 1
    ? later(value, 1000) //takes a second
    : later(value, 100); //takes 100ms
const working = last(apiCall);
const Api = ({ api, title }) => {
  const [value, setValue] = React.useState('');
  const [apiResult, setApiResult] = React.useState('');
  React.useEffect(() => {
    api(value).then((resolve) => {
      console.log(title, 'resolved with', resolve);
      setApiResult(resolve);
    });
  }, [api, title, value]);
  return (
    <div>
      <h1>{title}</h1>
      <h3>api result: {apiResult}</h3>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
    </div>
  );
};
const App = () => (
  <div>
    <Api
      api={apiCall}
      title="Broken (type 2 characters fast)"
    />
    <Api api={working} title="Working" />
  </div>
);
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
HMR
  • 37,593
  • 24
  • 91
  • 160
  • 1
    FWIW, the SWR library already does the same deduplication; it internally holds a timestamp for the last request made and only accepts a response tagged with the same timestamp: https://github.com/vercel/swr/blob/1882cbd410f1dbf8017997af5f3383ad07090346/src/use-swr.ts#L306 – AKX Jul 06 '20 at 08:15
  • @HMR, could you kindly provide some information in the code for what does what? Altough this is what I need, this is all new for me so I don't really understand what it is doing. – Yorbjörn Jul 07 '20 at 08:03
  • @Yorbjörn The `last` function is a [curried function](https://javascript.info/currying-partials) that closes over a function that returns a promise (apiCall) and will call that function but only resolves the promise if it was the promise that came from the last time it was called: `The way I show below is just to reject the async promise when a newer request was made` – HMR Jul 07 '20 at 09:53
  • Okay, I understand the concept for a curried function, on thing that is not really clear for me is how I implement my GET request with this. So let's say I need to get my data from this link ````https://${api_uri}/name/${value}````, where do I put the url in this code, and where can I see the response of this. Sorry if i'm asking dumb questions – Yorbjörn Jul 07 '20 at 10:17
  • @Yorbjörn Say your get request is done by a function named `apiCall` then `last(apiCall)` will return a function will only resolve if it was called last so `lastApiCall = last(apiCall)` and `lastApiCall(url).then...` – HMR Jul 07 '20 at 10:36
  • Could you take a look at what I recreated in codesandbox with your code? I did manage to get my data in my console.log, but my core problem remains. https://codesandbox.io/s/cancel-previous-requests-e9wr7 – Yorbjörn Jul 07 '20 at 11:57
  • @Yorbjörn You are not returning anything in [apiCall](https://codesandbox.io/s/cancel-previous-requests-7k9t8?file=/src/index.js:523-739) – HMR Jul 07 '20 at 13:07
0

Use AbortController of JavaScript. Its is specifically implemented to cancel network requests.

Check this out https://developer.mozilla.org/en-US/docs/Web/API/AbortController

knigalye
  • 956
  • 1
  • 13
  • 19