1

I am trying to use the debounce function to avoid multiple calls when I'm typing and searching something in a datatable. What I am doing now is in the input

onChange={(e) => {
            const delayedQuery = useCallback(debounce(this.handleSearch(e.target.value), 500));
            return delayedQuery
          }}

where handeSearch is

handleSearch(filter) {
      service.getData(filter).subscribe((data) => {console.log(data)})
    }

but I have this error TypeError: Expected a function . The service is working but the debounce nope. It writes character by character at the same time I'm typing and that's not correct.

Atlas91
  • 5,754
  • 17
  • 69
  • 141
  • 1
    It appears that `this.handleSearch(e.target.value)` doesn't produce a function. Should it? Or should you transform this to a function reference? – VLAZ Nov 13 '20 at 11:46
  • well the handleSearch is the function i call when I type in the input that fires the service with the new data actually. Isn't good? – Atlas91 Nov 13 '20 at 11:47
  • 1
    In addition to what @VLAZ said, you won't be able to use `React.useCallback` inside a callback – ypahalajani Nov 13 '20 at 11:50
  • 2
    `debounce` takes a *function reference* as a parameter. However, what you provide is `this.handleSearch(e.target.value)` - the result of *calling a method*. The result of calling this particular method is `undefined`, as you don't have an explicit `return` statement. So, you either need to change `handleSearch` to return a function or you change that function call to a callback. – VLAZ Nov 13 '20 at 11:50
  • @End.Game You can't use `useCallback` hook because you can't use any hooks in class components. – HMR Nov 13 '20 at 12:39

1 Answers1

1

You can't use hooks in class components or in event handler functions and you have to pass a function to lodash debounce.

Also; if you try to read target.value asynchronously from an event you'll get a this synthetic event is reused for performance reasons error.

In chat I assume you use rxjs from(promise) so you still have a problem that when a user types and you fire async request they may resolve in a different order.

See below for a broken example (type in hai quickly and wait 2 seconds it resolves to ha)

//simple debounce implementation
const debounce = (fn, delay = 50) => {
  let time;
  return (...args) => {
    clearTimeout(time);
    time = setTimeout(() => fn(...args), delay);
  };
};
//service where you can subscribe to
const service = (() => {
  let listeners = [];
  const later = (value, time = 50) =>
    new Promise((r) =>
      //if search is 2 characters long it'll resolve much later
      setTimeout(
        () => r(value),
        value.length === 2 ? 2000 : time
      )
    );
  //subscribe returns an unsubscribe function
  const subScribe = (fn) => {
    listeners.push(fn);
    //return the Subscriber object
    //https://rxjs-dev.firebaseapp.com/api/index/class/Subscriber
    return {
      unsubscribe: () =>
        (listeners = listeners.filter(
          (listener) => listener !== fn
        )),
    };
  };
  //notify all listeners
  const notify = (value) =>
    listeners.forEach((listener) => listener(value));
  return {
    getData: (value) => ({
      subScribe: (fn) => {
        const unsub = subScribe(fn);
        later(value).then((value) => notify(value));
        return unsub;
      },
    }),
  };
})();

class App extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = { asyncSearchResult: '' };
    //use handleSearch to create a debounced version of it
    this.handleSearchDebounced = debounce(
      this.handleSearch
    ).bind(this); //you need to bind it to guearantee correct context
  }
  rendered = 0;
  handleSearch = (value) =>
    service
      .getData(value)
      .subScribe((value) =>
        this.setState({ asyncSearchResult: value })
      );
  render() {
    return (
      <div>
        <input
          type="text"
          // pass value to the handler, not the event
          onChange={(e) =>
            this.handleSearchDebounced(e.target.value)
          }
        />
        async result: {this.state.asyncSearchResult}
      </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>

Here is the solution:

//simple debounce implementation
const debounce = (fn, delay = 50) => {
  let time;
  return (...args) => {
    clearTimeout(time);
    time = setTimeout(() => fn(...args), delay);
  };
};
//service where you can subscribe to
const service = (() => {
  let listeners = [];
  const later = (value, time = 50) =>
    new Promise((r) =>
      //if search is 2 characters long it'll resolve much later
      setTimeout(
        () => r(value),
        value.length === 2 ? 2000 : time
      )
    );
  //subscribe returns an unsubscribe function
  const subScribe = (fn) => {
    listeners.push(fn);
    //return the Subscriber object
    //https://rxjs-dev.firebaseapp.com/api/index/class/Subscriber
    return {
      unsubscribe: () =>
        (listeners = listeners.filter(
          (listener) => listener !== fn
        )),
    };
  };
  //notify all listeners
  const notify = (value) =>
    listeners.forEach((listener) => listener(value));
  return {
    getData: (value) => ({
      subScribe: (fn) => {
        const unsub = subScribe(fn);
        later(value).then((value) => notify(value));
        return unsub;
      },
    }),
  };
})();
const latestGetData = (value) => {
  const shared = {};
  return {
    subScribe: (fn) => {
      const current = {};
      shared.current = current;
      const subscriber = service
        .getData(value)
        .subScribe((result) => {
          //only call subscriber of latest resolved
          if (shared.current === current) {
            fn(result);
          }
          subscriber.unsubscribe();
        });
    },
  };
};
class App extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = { asyncSearchResult: '' };
    //use handleSearch to create a debounced version of it
    this.handleSearchDebounced = debounce(
      this.handleSearch
    ).bind(this); //you need to bind it to guearantee correct context
  }
  rendered = 0;
  handleSearch = (value) =>
    latestGetData(value).subScribe((value) =>
      this.setState({ asyncSearchResult: value })
    );
  render() {
    return (
      <div>
        <input
          type="text"
          // pass value to the handler, not the event
          onChange={(e) =>
            this.handleSearchDebounced(e.target.value)
          }
        />
        async result: {this.state.asyncSearchResult}
      </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
  • Thank you. I got you and now my code is working well. I'm sorry but I'm new in react – Atlas91 Nov 13 '20 at 13:16
  • Nope. It's a postgres. @HMR tell me if I'm wrong, if I don't use bind(this) handleSearch could have the this undefined for example? – Atlas91 Nov 13 '20 at 13:19
  • I don't know how's the backend actually. I have an Unexpected token in rendered = 0;. Is it possibile? – Atlas91 Nov 13 '20 at 13:31
  • Also in handleSearch = (value) in the = symbol. – Atlas91 Nov 13 '20 at 13:32
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/224527/discussion-between-hmr-and-end-game). – HMR Nov 13 '20 at 13:35
  • @End.Game Updated answer to solve [this problem](https://stackoverflow.com/a/62751846/1641941) and added a fake rxjs observable that you use in your code that will unsubscribe once the promise resolves. – HMR Nov 13 '20 at 14:52