90

I need the state to change to maintain the string the user is typing. However I want to delay an action until the user stops typing. But I can't quite place my finger on how to do both.

So When the user stops typing I want an action to be triggered, but not before. Any suggestions?

chris
  • 36,115
  • 52
  • 143
  • 252
  • I've tried setting an interval to clear out in the `onChange` but its delaying the string being maintained as well. – chris Oct 30 '18 at 19:43
  • 1
    The concept of debouncing sounds like it is what you are looking for, there are many packages that support this functionality, and it is not difficult to write yourself https://levelup.gitconnected.com/debounce-react-and-redux-code-for-improved-performance-4b8d3c19e305?gi=56159788061e – Benjamin Charais Oct 30 '18 at 20:48
  • @BenjaminCharais yes, **debouncing** is correct. It's a weird term though and easy to forget. Once you know the name answers on SO are easy to find: https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js/28046731#28046731 – icc97 Apr 20 '22 at 15:00
  • I personally prefer this logrocket article though, it has some nice animations to explain the problem and various solutions: https://blog.logrocket.com/how-and-when-to-debounce-or-throttle-in-react/ – icc97 Apr 20 '22 at 15:02

11 Answers11

107

With React Hooks and Function components

To keep the string the user is typing, use the useState hook to store the text the user is typing. Then give that state to the value of the input. Also be sure to use setState on the onChange event handler of the input, otherwise the input value won't change.

To trigger an action only sometime after the user stops typing, you can use the useEffect hook together with setTimeout. In this case, we want to trigger useEffect when the input value changes, so we'll create a useEffect hook and on its dependency array give it the variable with the value of the input. The function given to useEffect should use setTimeout to trigger an action after the delay time that is desired. Also, the function given to useEffect should return a cleanup function that clears the timeout set. This avoids doing actions for input values which are no longer relevant to the user.

Below is a little example of an app that uses the above steps to keep the string the user is typing visible and to show the finished string 500ms after the user stops typing.

function App() {
  const [query, setQuery] = useState("");
  const [displayMessage, setDisplayMessage] = useState("");

  useEffect(() => {
    const timeOutId = setTimeout(() => setDisplayMessage(query), 500);
    return () => clearTimeout(timeOutId);
  }, [query]);

  return (
    <>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <p>{displayMessage}</p>
    </>
  );
}
Cookie
  • 558
  • 8
  • 24
jnforja
  • 1,078
  • 1
  • 9
  • 8
  • Nice idea. Gonna apply it now :) – Arup Rakshit Jan 11 '21 at 15:05
  • 4
    The above idea is good, though I would instead use a library debounce function, not setTimeout, e.g. with Lodash: https://lodash.com/docs/#debounce – Dmitriy Feb 04 '21 at 04:10
  • 1
    Very nice solution to debouncing in React! I'd like to add that if you forget to set the dependencies of the `useEffect()` method, it will be called whenever the state changes (which could happen if you have multiple `` tags with their own listeners), which could be costly, depending on what you're doing (i.e. validation). – micka190 Jul 06 '21 at 20:24
  • How about if a user types "google" and then I trigger an api request to fetch some data for query "google" but user types ".com" too right after .6 seconds so technically api will be triggered 2 times, so how I omit result of first api's request result and take the last api request's result as final output specially when it may possible that first api request can be late or early to delivery data than second api request , – Amit Bravo Aug 14 '21 at 17:37
  • Amit, this article should answer your question. https://joaoforja.com/blog/5-steps-to-perform-a-search-when-user-stops-typing-using-react-+-hooks-in-a-controlled-component/ I wouldn't do it exactly like that nowadays, but it addresses your concerns. – jnforja Aug 15 '21 at 21:32
  • Very helpful solution, works great as well – Valerxx22 Feb 07 '22 at 10:10
  • @Dmitriy yes - it's good to know the name for this process - **debouncing**. Then you can start searching for *why* this code works, for example this [logrocket blog on debouncing and throttleing](https://blog.logrocket.com/how-and-when-to-debounce-or-throttle-in-react/) explains what we're doing nicely – icc97 Apr 20 '22 at 14:57
30

Sounds you are going to need to use setTimeout to start a timer as soon as the user enters text. If the user enters another character, restart the timer. If the user does not type again before the timer completes, it will fire an action that toggles the checkbox:

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      text: '',
      checked: false
    };
    this.timer = null;
  }
  
  componentDidUpdate (prevProps, prevState) {
    if(prevState.text !== this.state.text) {
      this.handleCheck();
    }
  }
  
  onChange = e => {
    this.setState({
      text: e.target.value
    });
  };
  
  handleCheck = () => {
    // Clears running timer and starts a new one each time the user types
    clearTimeout(this.timer);
    this.timer = setTimeout(() => {
      this.toggleCheck();
    }, 1000);
  }
  
  toggleCheck = () => {
    this.setState( prevState => ({ checked: !prevState.checked }));
  }
  
  render () {
    return (
      <div>
        <input value={this.state.text} onChange={this.onChange} placeholder="Start typing..." /><br/>
        <label>
          <input type="checkbox" checked={this.state.checked} onChange={this.toggleCheck} />
          Toggle checkbox after user stops typing for 1 second
        </label>
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Chase DeAnda
  • 15,963
  • 3
  • 30
  • 41
20

One way to do this would be to have your onChange handler execute two functions:

  • Function for immediately updating state
  • Debounced function

Example code:

import debounce from 'lodash.debounce';

class Foo extends React.Component {
  constructor() {
    super()

    this.state = {
      value: ''
    }

    // Delay action 2 seconds
    this.onChangeDebounced = debounce(this.onChangeDebounced, 2000)
  }

  handleInputChange = (e: Event) => {
    // Immediately update the state
    this.setState({
      value: e.target.value
    })

    // Execute the debounced onChange method
    this.onChangeDebounced(e)
  }

  onChangeDebounced = (e: Event) => {
    // Delayed logic goes here
  }

  render() {
    return (
      <input onChange={this.handleInputChange} value={this.state.value} />
    )
  }
}
richardgirges
  • 1,452
  • 1
  • 12
  • 17
  • 3
    Great answer. I tried the above answer first but it call the logic number of time to text change. In my case, i am calling an api on change event so i wanted to call it once the user enter the text instead of calling api on each change. Your answer is the perfect solution. Thanks – Salman Lone Dec 11 '19 at 05:52
  • the 'debounce' reference is very useful to give this situation a proper name – icc97 Apr 20 '22 at 14:28
16

With React Hooks and Function components

const [timer, setTimer] = useState(null);

function changeDelay(change) {
    if (timer) {
      clearTimeout(timer);
      setTimer(null);
    }
    setTimer(
      setTimeout(() => {
        console.log(change);
      }, 3000)
    );
}

In input

<input type="text" onChange={(e) => { changeDelay(e.target.value); }} />
Edwin Vergara
  • 161
  • 1
  • 3
7

With React Hooks - useRef

const timer = useRef(null)
    
useEffect(() => {
    
    clearTimeout(timer.current)
    timer.current = setTimeout(() => {
     // your logic
    },1000)
    
},[value])
Java bee
  • 2,522
  • 1
  • 12
  • 25
Dor Ben Itzhak
  • 285
  • 2
  • 5
3

Call every state update except the first time:

Actually, I have the same issue but a little setTimeout could help me with a check ref for the first time mount:

import React, {useState, useEffect, useRef} from "react";

const Search = () => {
    const filterRef = useRef(); // use ref to call the API call all time except first time
    const [serpQuery, setSerpQuery] = useState('');

    useEffect(() => {
        let delayTimeOutFunction;

        if(!filterRef.current) {
            filterRef.current = true;

        } else { // componentDidMount equivalent
            delayTimeOutFunction = setTimeout(() => {
                console.log('call api: ', serpQuery)
            }, 700); // denounce delay
        }
        return () => clearTimeout(delayTimeOutFunction);
    }, [serpQuery]);

    return (
      <input value={serpQuery} onChange={e => setSerpQuery(e.target.value)} />
    );
};
AmerllicA
  • 29,059
  • 15
  • 130
  • 154
3

You can build a custom hook specifically for this purpose and use it just like the useState hook. This is more like an extension of jnforja's answer

import { useEffect, useState } from "react";
const useDebounce = (initialValue = "", delay) => {
  const [actualValue, setActualValue] = useState(initialValue);
  const [debounceValue, setDebounceValue] = useState(initialValue);
  useEffect(() => {
    const debounceId = setTimeout(() => setDebounceValue(actualValue), delay);
    return () => clearTimeout(debounceId);
  }, [actualValue, delay]);
  return [debounceValue, setActualValue];
};

export default useDebounce;

And use it just like the useState hook with the delay value

const [value, setValue] = useDebounce('',1000)

You can also check this article, explaining the implementation if you want.

Karthik Raja
  • 101
  • 1
  • 1
  • 6
1

You can debounce on the onChange event (if the user is typing the onchange event will not execute)

Warning - Keep in mind that creating functions on render is a bad practice. I did it in order to illustrate the solution. A more safe solution is to use a class Component that creates the debounced handler on its constructor.

class DebouncedInput extends React.Component {
  constructor() {
    super();

    // Creating the debouncedOnChange to avoid performance issues

    this._debouncedOnChange = _.debounce(
      this.props.onChange, 
      this.props.delay
    );
  }

  render () {
    const { onChange, delay, ...rest } = this.props;
    return (
      <input onChange={this._debouncedOnChange} {..rest} />
    )
  }
}

Example below

function DebouncedInput (props) {
  const { onChange, delay = 300, ...rest } = props;
 
  
  return (
    <input 
      {...rest}
      onChange={ _.debounce(onChange, delay)}
    />
  )
}

function App() {
  return (
    <div>
      <DebouncedInput 
        type="text"
        placeholder="enter"
        delay={2000}
        onChange={() => console.log('changing')}
      />
    </div>
  )
}

ReactDOM.render(
  <App/>,
  document.querySelector('#app')
);
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.2/umd/react.production.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.2/umd/react-dom.production.min.js"></script>
</head>
<body>
 <div id="app"></div>
</body>
</html>
AngelSalazar
  • 3,080
  • 1
  • 16
  • 22
  • This is not quite what OP is looking for. This will prevent the user from typing too fast, but will not call a function after x seconds of inactivity. – Chase DeAnda Oct 30 '18 at 21:09
  • Ok, then I wonder why you use a simple debounce function on your answer. weird. – AngelSalazar Oct 30 '18 at 23:30
  • In this solution you don't use the value of input anyhow. Thus the input is rendered as uncontrolled component which allows you to do the trick. If you make it controlled it will stop working. So the only way to keep using your solution is to work with the input as uncontolled component which isn't recommended in general case. – Stalinko Dec 07 '20 at 08:35
0

You can use debounce and throttle of lodash library for delaying to call change handler function, the following code is based on debounce. The same code can be used for the throttle function. Debounce: delays invoking function until after X milliseconds Throttle: invokes function at most once per every X milliseconds

Sample code:

import React,{useEffect, useState, useMemo} from "react"
import debounce from "lodash.debounce";

export default function App() {
  const [search, setSearch] = useState("");
  const handleChangeSearch = ({ target }) => {
    setSearch(target.value);    
  };
  const debouncedChangeHandler = useMemo(
    () => debounce(handleChangeSearch, 500),
    []
  );

  useEffect(() => {
    return () => {
      debouncedChangeHandler.cancel();
    }
  }, []);

  return (
    <div className="App">      
      <label > Search:
      <input sx={{ display: { xs: "none", md: "block" } }}
        onChange={debouncedChangeHandler}
        name="search"
        type="text"
        placeholder="search..."
      />
      </label >
    </div>
  );
}
Mohammad Momtaz
  • 545
  • 6
  • 12
0

I have created npm package for this matter, you can use provided hook to get both immediate and delayed values.

https://www.npmjs.com/package/use-delayed-search

0
let reqDelay = useRef();

const yourMethod = () => {
  clearTimeout(reqDelay.current);

  reqDelay.current = setTimeout(() => {
    // do your fetch or whatever you want here
  }, 1000)
}

This way will prevent to re-render

Markus Ethur
  • 76
  • 1
  • 1
  • 6