3

My goal is to fire a fetch request to get data from an API after some delay. In my particular case, I have an input field where a user can input a post ID. I don't want fire a fetch request on each number entered in an input field. I want to fire a request only after a user stopped entering data for 1 second. Here is my implementation:

import { useCallback, useEffect, useState } from 'react'
import './App.css'

function debounce(fn, ms) {
  let timeoutId
  return function (...args) {
    if (timeoutId) clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      fn(...args)
    }, ms)
  }
}

function App() {
  const [input, setInput] = useState('')

  const fetchData = async () => {
    if (input !== '') {
      let res = await fetch(`https://jsonplaceholder.typicode.com/posts/${input}`)
      let resData = await res.json()
      console.log(resData)
    }
  }

  const fireRequest = useCallback(debounce(fetchData, 1000), [])

  useEffect(() => {
    fireRequest()
  }, [input])

  return (
    <div className='App'>
      <input type='text' onChange={(e) => setInput(e.target.value)} value={input}></input>
    </div>
  )
}

export default App

For some reason, it's not working. There are no errors, and it looks like my data is not being fetched(nothing logs to the console). What am I doing wrong ? Thank you.

Noob
  • 2,247
  • 4
  • 20
  • 31
  • Does this answer your question? [How to start search only when user stops typing?](https://stackoverflow.com/questions/42217121/how-to-start-search-only-when-user-stops-typing) – ggorlen Dec 26 '20 at 17:27

3 Answers3

2

Try this:

useEffect(() => {
    const apiCallTimeoutId = setTimeout(() => {
      fireRequest()
    }, 1000);
    return () => clearTimeout(apiCallTimeoutId);
  }, [input]);

1 sec after the user stops typing the request will be fired.

Sakshi
  • 1,464
  • 2
  • 8
  • 15
  • There will be a batch of request for each character typed after 1 second as per the solution – Sudhanshu Kumar Dec 26 '20 at 15:45
  • no the request will be fired 1 sec after the user stops typing.if you set it to 5000(say) then after 5 secs the user stops typing the request will be made. – Sakshi Dec 26 '20 at 15:47
  • `The setTimeout() function is used to invoke a function or a piece of code after a specified amount of time is completed.` So as the user types something this useEffect will be triggered and after 1 sec fireRequest will be invoked. – Sakshi Dec 26 '20 at 15:51
  • nope ,fireRequest will be invoked after 1 sec only! – Sakshi Dec 26 '20 at 15:55
  • 1 sec for your timeout, the 2nd one for the fireRequest() itself – Sudhanshu Kumar Dec 26 '20 at 15:56
  • then may be you can remove the delay of fireRequest(if that is possible) – Sakshi Dec 26 '20 at 15:56
  • and anyways any api call will cause some delay! – Sakshi Dec 26 '20 at 15:58
  • 1
    Thanks, it seems to be working well, however, I'm not sure if it's the way to go about it. I need to test it a bit more. – Noob Dec 26 '20 at 15:59
1

This answer is on the right track but there's no need to keep const fireRequest = useCallback(debounce(fetchData, 1000), []) as it seems to imply.

Just fire the timeout, then clear it in the useEffect cleanup callback if the component rerenders:

<script type="text/babel" defer>
const {useState, useEffect} = React;

const Example = () => {
  const [input, setInput] = useState("1");
  const [result, setResult] = useState();

  const fetchData = async () => {
    if (input) {
      try {
        const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${input}`);

        if (!res.ok) {
          throw Error(res.status);
        }

        setResult(await res.json());
      }
      catch (err) {
        console.error(err);
      }
    }
  };

  useEffect(() => {
    const timeout = setTimeout(fetchData, 1000);
    return () => clearTimeout(timeout);
  }, [input]);

  return (
    <div>
      <input
        type="number"
        min="1"
        max="99"
        onChange={e => setInput(e.target.value)} 
        value={input}
      />
      {result && <div>{result.title}</div>}
    </div>
  );
};

ReactDOM.createRoot(document.querySelector("#app"))
  .render(<Example />);

</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="app"></div>

Note the try-catch on the fetch call and checking res.ok rather than throwing when res.json() fails.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
1

The main issue here is that you are calling the function only when input != "". Till then input is not set to the desired value and thus the function becomes a no-op. This change will however help

import { useCallback, useEffect, useState } from 'react'
import './App.css'

function debounce(fn, ms) {
  let timeoutId
  return function (...args) {
    if (timeoutId) clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      fn(...args)
    }, ms)
  }
}

export default function App() {
  const [input, setInput] = useState('')

  const handleInputChange = (e) => {
    setInput(e.target.value);
    debouncedFireRequest(e.target.value);
  };

  const debouncedFireRequest = useCallback(
    debounce(async (value) => {
      if (value !== '') {
        let res = await fetch(`https://jsonplaceholder.typicode.com/posts/${value}`)
        let resData = await res.json()
        console.log(resData)
      }
    }, 1000),
    []
  );

  return (
    <div className='App'>
      <input type='text' onChange={handleInputChange} value={input}></input>
    </div>
  );
}
Rajat Jain
  • 116
  • 3
  • ohh, so in my code `useCallback` basically captures the value of `input` as `''` and because I don't supply new value of `input` it just stays `''`. Is it correct ? – Noob Dec 26 '20 at 16:30
  • Yes, bingo. To be on the safe side of things, just check for whether `e.target.value` is empty or not..for that pass that to the toBeDebounced function :) – Rajat Jain Dec 26 '20 at 16:34
  • Thank you very much. So, what is the better way of doing it? Should I just `setTimeout` in `useEffect` and clean it in `return`. Seems to be a much cleaner implementation than this debounce way. Are there any drawbacks? – Noob Dec 26 '20 at 16:40
  • well, this is more intuitive. Calling the method in useEffect, keeping a dependency on input will work too. I prefer my approach. Regarding cleaning up, you must clean up the debounce subscription inside useEffect. If you don't do it, your functionality won't get affectetd. It is just that there will be perf issues later on. It is code debt – Rajat Jain Dec 26 '20 at 16:44