164

I need to perform a Search when user stops typing.I know I am supposed to use setTimeout() . But with Reactjs I cant find how it works. Can someone please tell me how to invoke a method (that will handle Search) when the user stops typing for a few seconds (suppose 5).I cant figure out where to write the code to check that the user has stopped typing.

import React, {Component, PropTypes} from 'react';

export default class SearchBox extends Component {

    state={
      name:" ",
    }

    changeName = (event) => {
        this.setState({name: event.target.value}); 
    }

    sendToParent = () => {
        this.props.searching(this.state.name);
    }

    render() {
        return (
            <div>
                 <input type="text"  placeholder='Enter name you wish to Search.'  onChange={this.changeName} />

            </div>
        );
    }
}   

I want to invoke the sendToParent method when the user stops typing.

Flip
  • 6,233
  • 7
  • 46
  • 75
shinite
  • 1,978
  • 3
  • 12
  • 15
  • Well are they typing in an input element? If so, input elements have a attribute called 'onKeyPress' that gets called every time they press a button while that input is selected. So you could have it so that the timeout is started every time they press a button, but if they press the button again before the timeout has executed it resets the timer. If they haven't typed for X time then the timeout executes a search. – Jayce444 Feb 14 '17 at 03:06
  • If you're referring to the actual code that does the timeout search and 'stopped typing' check, then there's plenty of examples of those online – Jayce444 Feb 14 '17 at 03:08
  • My method is not invoking on using onkeyPress – shinite Feb 14 '17 at 03:26
  • It's case sensitive, 'onkeyPress' won't work. Has to be 'onKeyPress' – Jayce444 Feb 14 '17 at 03:36

22 Answers22

199

Implement using useEffect hook:

function Search() {
  const [searchTerm, setSearchTerm] = useState('')

  useEffect(() => {
    const delayDebounceFn = setTimeout(() => {
      console.log(searchTerm)
      // Send Axios request here
    }, 3000)

    return () => clearTimeout(delayDebounceFn)
  }, [searchTerm])

  return (
    <input
      autoFocus
      type='text'
      autoComplete='off'
      className='live-search-field'
      placeholder='Search here...'
      onChange={(e) => setSearchTerm(e.target.value)}
    />
  )
}
Harshal
  • 7,562
  • 2
  • 30
  • 20
  • 11
    be careful, this will re-render the component – Aljohn Yamaro Nov 25 '20 at 03:15
  • 21
    Simple and work as a charm. This solution need more votes – dungmidside Jan 17 '21 at 08:25
  • 1
    Exactly what I needed. The HTTP executes **a single request** with the input value. Thank you! – Vladimir Jovanović Feb 07 '21 at 00:04
  • 6
    Made a codesandbox in case it's any help https://codesandbox.io/s/search-input-settimeout-wszrv?file=/src/Search.tsx:205-417 – JimmyTheCode Jun 20 '21 at 09:00
  • 3
    pity @AljohnYamaro been ignore. he got a good point there – Alan Yong Jun 23 '21 at 12:41
  • 6
    @AljohnYamaro, can you explain why it gets re-rendered? – Indika K Jul 31 '21 at 12:11
  • the accepted answer didn't work for me. I was creating a custom autosubmit form that waits until textinput typing stops before submitting and this answer worked like a charm! Thank you! – piouson Aug 06 '21 at 12:14
  • 4
    @IndikaK it gets re-rendered because there is an onChange on the element that calls setState every time a key is pressed by the user. – Scott Sep 18 '21 at 05:19
  • 1
    `delayDebounceFn` is a confusing name, it's an identifier. It's typically named `timeoutID` or similar. https://developer.mozilla.org/en-US/docs/Web/API/setTimeout – tokland Mar 03 '22 at 10:35
  • The problem with this solution is that it will call the useEffect function once per keystroke rather than just once when the user stops typing, leading to excessive hits on a server. – toshiomagic Feb 17 '23 at 03:39
111

You can use setTimeout with respect to your code as follows,

state = {
    name: '',
    typing: false,
    typingTimeout: 0
}
changeName = (event) => {
    const self = this;

    if (self.state.typingTimeout) {
       clearTimeout(self.state.typingTimeout);
    }

    self.setState({
       name: event.target.value,
       typing: false,
       typingTimeout: setTimeout(function () {
           self.sendToParent(self.state.name);
         }, 5000)
    });
}

Also, you need to bind changeName handler function in constructor.

constructor(props) {
   super(props);
   this.changeName = this.changeName.bind(this);
}
Saba
  • 3,418
  • 2
  • 21
  • 34
  • 18
    I'm late to the party, but I would like to point out that you don't need to bind the changeName function when using es6 arrow syntax. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions – MEnf Jan 17 '18 at 18:37
  • 6
    For that same reason, you don't need to set `const self = this`. `this` is already bound to the class scope. – MEnf Jan 17 '18 at 18:39
  • 1
    There is an typo on: ``` state = { ... typingTimeOut: 0 } ``` typingTimeout is written with an capital O. Couldn't edit answer to fix the typo. – Yves Jun 25 '18 at 12:08
  • 3
    is **typing** field of state necessary here? – Dhruv Singhal Feb 22 '19 at 12:09
  • 1
    **typing** field look not necessary. And when u'r using arrow function `changeName`, u no need to bind `this` in constructor. – Duy Hưng Androgyne Tenor Oct 02 '19 at 03:39
55

Another way that worked with me:

class Search extends Component {
  constructor(props){
    super(props);
    this.timeout =  0;
  }

  doSearch(evt){
    var searchText = evt.target.value; // this is the search text
    if(this.timeout) clearTimeout(this.timeout);
    this.timeout = setTimeout(() => {
      //search function
    }, 300);
  }

   render() {
    return (
      <div className="form-group has-feedback">
        <label className="control-label">Any text</label>
        <input ref="searchInput" type="text" onChange={evt => this.doSearch(evt)} />
      </div>
    );
  }
}
Inacio Schweller
  • 1,986
  • 12
  • 22
Ninh Ngo
  • 559
  • 4
  • 2
25

This library (use-debounce) is nice and simple.

Setup

yarn add use-debounce

or

npm i use-debounce --save

Usage sample from documentation

import React, { useState } from 'react';
import { useDebounce } from 'use-debounce';

export default function Input() {
  const [text, setText] = useState('Hello');
  const [value] = useDebounce(text, 1000);

  return (
    <div>
      <input
        defaultValue={'Hello'}
        onChange={(e) => {
          setText(e.target.value);
        }}
      />
      <p>Actual value: {text}</p>
      <p>Debounce value: {value}</p>
    </div>
  );
}

Things that I liked at this moment, things could be different in future!:

  • Easy to setup & use
  • Less Boilerplate code
  • Modest ratings (~1K) and usage (npm - 200K downloads/Week)
  • Supports timeout, MaxWait and other features
stayingcool
  • 2,324
  • 1
  • 21
  • 24
  • 3
    This should be the accepted answer! This answer saved me a lot of time (and code) – 70ny Nov 19 '20 at 19:25
20

I used the debounce function of lodash

onChangeSearchInput = (evt)=> {
    this.debouncedSearch(evt.target.value);
};

debouncedSearch = debounce(function (query) {
    this.setState({query});
}, 1000);

Somewhere in my render method i have this input field

<input
    type='text'
    onChange={this.onChangeSearchInput}
    className='uk-input'
    placeholder={'search by name or email...'}
   />
Akinola Olayinka
  • 841
  • 7
  • 11
17

I have use this custom hook and it's work perfectly no issue still.

export function useSearchDebounce(delay = 350) {
  const [search, setSearch] = useState(null);
  const [searchQuery, setSearchQuery] = useState(null);

  useEffect(() => {
    const delayFn = setTimeout(() => setSearch(searchQuery), delay);
    return () => clearTimeout(delayFn);
  }, [searchQuery, delay]);

  return [search, setSearchQuery];
}

Use in any place like

const [search, setSearch] = useSearchDebounce();

<input onChange={(e) => setSearch(e.target.value)}/>
Hiren Bhut
  • 1,166
  • 1
  • 13
  • 19
11

I think we can do it in a more simpler and cleaner manner, without abrupting the state parameter which calls the complete component life cycle like this:

constructor(props) {
    super(props);

    //Timer
    this.typingTimeout = null;

    //Event
    this.onFieldChange = this.onFieldChange.bind(this);

    //State
    this.state = { searchValue: '' }; 
}   


 /**
 * Called on the change of the textbox.
 * @param  {[Object]} event [Event object.]
 */
onFieldChange(event) {
    // Clears the previously set timer.
    clearTimeout(this.typingTimeout);

    // Reset the timer, to make the http call after 475MS (this.callSearch is a method which will call the search API. Don't forget to bind it in constructor.)
    this.typingTimeout = setTimeout(this.callSearch, 475);

    // Setting value of the search box to a state.
    this.setState({ [event.target.name]: event.target.value });
}


<div className="block-header">
     <input
           type="text"
           name="searchValue"
           value={this.state.searchValue}
           placeholder="User Name or Email"
           onChange={this.onFieldChange}
     />
</div>
Rahul
  • 1,070
  • 3
  • 21
  • 47
7

you can use react hooks useEffect with the use of setTimeOut function since it always return the timer id and you could easily clear the timer with that id as follows

export const Search = () => {
const [term, setTerm] = useState();
const [results, setResult] = useState([]);

useEffect(() => {
    const searchWiki = async () => {
        const { data } = await axios.get('https://en.wikipedia.org/w/api.php', {
            params: {
                srsearch: term,
            },
        });

        setResult(data.query.search);
    };
    const timerId = setTimeout(() => {
        searchWiki();
     // make a request after 1 second since there's no typing 
    }, 1000);

    return () => {
        clearTimeout(timerId);
    };
}, [term]);
Mena Aziz
  • 99
  • 1
  • 2
6

How about a custom hook?

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

export default function useSearchInputState(searchHandler) {
  
  // to prevent calling the handler on component mount
  const didMountRef = useRef(false);

  const [searchValue, setSearchValue] = useState(null);

  useEffect(() => {
    let delayDebounceFn;

    if (didMountRef.current) {
      delayDebounceFn = setTimeout(searchHandler, 600)
    } else {
      didMountRef.current = true;
    }

    return () => clearTimeout(delayDebounceFn);
  }, [searchValue]); // eslint-disable-line react-hooks/exhaustive-deps

  return [searchValue, setSearchValue];

}

Usage:

function MyComponent(props) {

  const [searchValue, setSearchValue] = useSearchInputState(() => {
    resetData(searchValue ?? null, selectedFilterPos); // replace with your code
  });

  return (
    <input className="Search"
           onChange={e => setSearchValue(e?.target?.value ?? null)}
      />
  );
}
Drunken Daddy
  • 7,326
  • 14
  • 70
  • 104
3

you can just use the debounce from lodash or simulate using setTimeout.

import React, {Component, PropTypes} from 'react';

export default class SearchBox extends Component {
    constructor(props){
       super(props);
       this.state={ name:" "}
       this.timeout =  null;

    }

    changeName = (event) => {
        clearTimeout(timeout);
         if(timeout){
           setTimeout((event)=> this.setState({name: event.target.value}), 200)
         }
    }

    sendToParent = () => {
        this.props.searching(this.state.name);
    }

    render() {
        return (
            <div>
                 <input type="text"  placeholder='Enter name you wish to Search.'  onChange={this.changeName} />

            </div>
        );
    }
}
Khalid Azam
  • 1,615
  • 19
  • 17
3

I made my own custom component like this.

import React, { useState, useEffect } from 'react'

const InputDebounce = props => {
  const { onChange, ...otherProps } = props

  const [inputTimeout, setInputTimeout] = useState(null)

  useEffect(() => () => clearTimeout(inputTimeout), [inputTimeout])

  const inputOnChange = value => {
    if (inputTimeout) clearTimeout(inputTimeout)
    setInputTimeout(
      setTimeout(() => {
        if (onChange) onChange(value)
      }, 1000)
    )
  }

  return (
    <input
      {...otherProps}
      onChange={e => inputOnChange(e.target.value)}
    />
  )
}

export default InputDebounce

And using anywhere like this.

import React from 'react'
import ReactDOM from 'react-dom'

import InputDebounce from './InputDebounce'

const App = () => {
  const usernameOnChange = value => {
    console.log(value)
  }

  return (
    <div>
      <InputDebounce
        type='text'
        name='username'
        placeholder='Username'
        onChange={usernameOnChange}
      />
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
ozgrozer
  • 1,824
  • 1
  • 23
  • 35
2

The code below works well for me :

const [filter, setFilter] = useState()

useEffect(() => {
    const search = setTimeout(() => {
        getList()
        //Your search query and it will run the function after 3secs from user stops typing
    }, 3000);
    return () => clearTimeout(search)
}, [filter])

and add HTML like this:

<input type="text" onInput={(e) => setFilter(e.target.value)} value={filter} />
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jul 23 '22 at 00:42
1

For React hooks:

First we'll define a component

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

const SearchInputText = ({ value, name, placeholder, onChange }) => {
  // state for keepign search text 
  const [searchText, setSearchText] = useState(value);
  // state for keeping the timeout
  const [searchTextTimeout, setSearchTextTimeout] = useState(null);

  // handler for form submit (pressing enter without waiting for setimeout to trigger)
  const handleSubmit = (e) => {
    e.preventDefault();
    // clear timeout as it'll that would be triggered
    if (searchTextTimeout) {
      clearTimeout(searchTextTimeout);
    }
    onChange(searchText);
  };

  // onChange handler
  const handleOnChange = (e) => {
  // cancelling previous timeouts
    if (searchTextTimeout) {
      clearTimeout(searchTextTimeout);
    }
    // first update the input text as user type
    setSearchText(e.target.value);
    // initialize a setimeout by wrapping in our searchTextTimeout so that we can clear it out using clearTimeout
    setSearchTextTimeout(
      setTimeout(() => {
        onChange(searchText);
        // timeout is 2500ms, change it to less or more.
      }, 2500),
    );
  };

  // making sure that we clear the timeout if/when the component unmount
  useEffect(() => {
    return () => clearTimeout(searchTextTimeout);
  }, [searchTextTimeout]);

  return (
    <form onSubmit={handleSubmit}>
      <input
        name={name}
        placeholder={placeholder}
        type="text"
        value={searchText}
        onChange={handleOnChange}
      />
    </form>
  );
};

export default SearchInputText;

Usage:

const Parent = () => {
  const handleChange = (e) => {
    // your implementation here
  };
  return (
    <div>
      <SortSearchInput name="search" placeholder="Enter Search" onChange={handleChange} />
    </div>
  );
};
Jawwad
  • 11
  • 4
  • Perhaps you could add some more comments to educate the person asking the question. – murb Jun 21 '21 at 09:45
1

Here is an approach using functional components and the useRef hook.

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

function Search() {
  const [searchTerm, setSearchTerm] = React.useState("");

  const inputRef = useRef<any>()
  
  useEffect(() => {
    let timer: NodeJS.Timeout | null = null

    const sendData = () => {
      // If the user keeps on typing then the timeout is cleared and restarted
      if(timer) clearTimeout(timer)

      timer = setTimeout(() => {
        setSearchTerm(inputRef.current.value)
      }, 3000)
    }

    const element = inputRef.current;
    // Set listener and start timeout
    element.addEventListener('keyup', sendData);

    return () => {
      // Remove listener wwhen unmounting
      element.removeEventListener('keyup', sendData);
    };
  }, []);

  return (
    <div>
      <input
        ref={inputRef}
        autoFocus
        type="text"
        autoComplete="off"
        className="live-search-field"
        placeholder="Search here..."
        
      />
      <p>searchTerm: {searchTerm}</p>
    </div>
  );
}

export default Search;

This approach avoids unnecessary re-renders and utilizes event listeners to handle the search submission when user stops typing.

GeorgeCodeHub
  • 131
  • 1
  • 10
0

Here's a working component template with some useful parameters to get your started.

import React, { Component } from 'react'

const initialState = { results: [], value: '' }

export default class SearchBox extends Component {
  state = initialState
  timeout = null
  search_url = "https://example.com/search?q="
  min_query_length = 2
  timeout_duration = 300

  handleSearchChange = (e) => {
    let value = e.target.value
    clearTimeout(this.timeout);
    if (value.length < 1) {
        return this.setState(initialState) 
    } else {
        this.setState({ value })
        if (value.length>=this.min_query_length) {    
            this.timeout = setTimeout(this.search, this.timeout_duration);
        }
    }
  }

  search = () => {
    // assuming your results are returned as JSON
    fetch(`${this.search_url}${this.state.value}`)
    .then(res => res.json())
    .then(data => {
        this.setState({
            results: data,
        })
    })
  }

  render() {
    return (
          <input
            onChange={this.handleSearchChange}
          />
    )
  }
}

infiniteloop
  • 2,112
  • 2
  • 18
  • 21
0

using react hooks, modified from @anoNewb's answer. With additions:

  • prevent multiple triggers when there's still timer running
  • add on Form Submit event

codesandbox

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

    export default function App() {
      const [search, setSearch] = useState("");
      const [searchTimeout, setSearchTimeout] = useState(null);

      useEffect(() => {
        if (searchTimeout) {
          clearTimeout(searchTimeout);
        }

        setSearchTimeout(
          setTimeout(() => {
            loadUsers();
          }, 1000),
        );

        return () => clearTimeout(searchTimeout);
      }, [search]);

      const loadUsers = () => {
        console.log("axios call with query: ", search);
      };

      return (
        <div className="App">
          <form
            onSubmit={(e) => {
              e.preventDefault();
              if (searchTimeout) {
                clearTimeout(searchTimeout);
              }
              loadUsers();
            }}
          >
            <input
              onChange={(e) => {
                setSearch(e.target.value);
              }}
            />
          </form>
        </div>
      );
    }
0

The code below works for me.

const[isReady, setReady]  = useState(true);
const onSearchSet =(event:React.ChangeEvent<HTMLInputElement>) => { 

    setCriteria(event.target.value);
    if(isReady) {
        setReady(false);
        const delayDebounceFn = setTimeout(() => {
            // Send Axios request here
            
            props.returnCall(props.RDropID, sortCriteria, event.target.value);

            setReady(true);
          }, 1000)
        
    }
      
};
plutomusang
  • 59
  • 1
  • 6
0

Can I use this code with Saga? It will help send the latest request. The time on the set time out can be changed. In my case, I used 600ms.

  const dispatch = useDispatch();
  const [searchText, setSearchText] = useState('');

  useEffect(() => {
    const sendSearchRequest = setTimeout(() => {
      if (searchText && searchText.length > 2) {
        dispatch(sendRequestToSaga(searchText));
      }
    }, 600);
    return () => clearTimeout(sendSearchRequest);
  }, [searchText]);
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jul 23 '22 at 00:08
0

This is much easier now with useEffect and does not need any library

import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom'

const FuncDemo = () => {
  const [searchStr, setSearchStr] = useState('')

  useEffect(() => {
   const makeApiCall = async () => {
   try {
      // your axios call
    } catch (e) {
    
    }
   }

    const triggerCall = setTimeout(() => {
      makeApiCall()
    }, 500)

    return () => clearTimeout(triggerCall)
  }, [searchStr])


  return (
    <input 
      name='search'
      onChange={e => setSearchString(e.target.value)}
    />
  )
}

ReactDOM.render(<FuncDemo/>, document.getElementById('root'))
Nishith
  • 928
  • 9
  • 13
0
function debounce(func, timeout = 300){
 let timer;
 return (...args) => {
   clearTimeout(timer);
   timer = setTimeout(() => { func.apply(this, args); }, timeout);
 };
}

function search(){
 console.log('search');
}

const processChange = debounce(() => search());

It can be used in input

<input type="text" onkeyup="processChange()" />
-1

User lodash javascript library and use [_.debounce][1]

changeName: _.debounce(function (val) {
  console.log(val)                
}, 1000)
Amir Ur Rehman
  • 649
  • 1
  • 9
  • 28
-3

Problem of Typeahead library https://twitter.github.io/typeahead.js/

Since the case here is simple, I can use a quick and dirty solution:

onChange: (event) ->
  if @_timeoutTask?
    clearTimeout @_timeoutTask

  @_timeoutTask = setTimeout (=>
    @sendToParent event.target.value
    clearTimeout @_timeoutTask
  ), 5000

In this way, the task will be triggered 5s after input event. If new event happens, the old task will be cancelled and a new task is scheduled, then it's another 5s to wait.

The difference in React is the where to store the computation state like _timeoutTask. The file scope, the component state, or the component instance.

Since _timeoutTask is component level, it should be be store globally. And it does not affect rendering, so not in component state too. So I suggest attaching it to component instance directly.

jiyinyiyong
  • 4,586
  • 7
  • 45
  • 88