shinite
shinite

Reputation: 2108

How to start search only when user stops typing?

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.

Upvotes: 177

Views: 208871

Answers (23)

Anas
Anas

Reputation: 1827

I just wrote a custom hook that works exactly like useState hook, it accepts the default state as a first parameter as usual, In addition it accepts a second parameter which is the delay of the state change e.g. useDebouncSearch("some default value or object...etc", 250). Also, this custom hook returns the previous value as the useState does.

Here is the hook, you can just copy it and paste it in a separated .js | .ts file and import it anywhere to use.

import { useState, useEffect } from "react";

export default function useDebouncSearch(defaultState, delay = 350) {
    const [search, setSearch] = useState(defaultState);
    const [searchQuery, setSearchQuery] = useState(defaultState);

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

    const setSearchQueryWithPreviousValue = (value: any) => {
        setSearchQuery((prevValue) => {
            if (typeof value === "function") {
                return value(prevValue);
            }
            return value;
        });
    };

    return [search, setSearchQueryWithPreviousValue];
}

Improved version of the hook that will reset the timeout if the user keep changing before the timeout is finished, so it will be changed only if the user stops writing for the specified timeout:-

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

export default function useDebouncedSearch(defaultState, delay = 350) {
    const [search, setSearch] = useState(defaultState);
    const [searchQuery, setSearchQuery] = useState(defaultState);
    const timeoutRef = useRef(null);

    useEffect(() => {
        if (timeoutRef.current) {
            clearTimeout(timeoutRef.current);
        }

        timeoutRef.current = setTimeout(() => {
            setSearch(searchQuery);
        }, delay);

        return () => {
            if (timeoutRef.current) {
                clearTimeout(timeoutRef.current);
            }
        };
    }, [searchQuery, delay]);

    const setSearchQueryWithPreviousValue = (value) => {
        setSearchQuery((prevValue: any) => {
            if (typeof value === "function") {
                return value(prevValue);
            }
            return value;
        });
    };

    return [search, setSearchQueryWithPreviousValue];
}

Upvotes: 0

Yoosef Palamadathil
Yoosef Palamadathil

Reputation: 79

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()" />

Upvotes: 0

Nishith
Nishith

Reputation: 1118

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'))

Upvotes: 3

GeorgeCodeHub
GeorgeCodeHub

Reputation: 301

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.

Upvotes: 1

Kalana Heshan
Kalana Heshan

Reputation: 16

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]);

Upvotes: 0

shabbir khargon
shabbir khargon

Reputation: 31

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} />

Upvotes: 3

plutomusang
plutomusang

Reputation: 59

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)
        
    }
      
};

Upvotes: 0

ozgrozer
ozgrozer

Reputation: 2042

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'))

Upvotes: 3

Hiren Bhut
Hiren Bhut

Reputation: 1226

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)}/>

Upvotes: 20

Jawwad
Jawwad

Reputation: 11

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>
  );
};

Upvotes: 1

Mina Aziz
Mina Aziz

Reputation: 109

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]);

Upvotes: 7

Ivan Rusli Mcdohl
Ivan Rusli Mcdohl

Reputation: 31

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>
      );
    }

Upvotes: 0

Drunken Daddy
Drunken Daddy

Reputation: 7991

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)}
      />
  );
}

Upvotes: 6

stayingcool
stayingcool

Reputation: 2814

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

Upvotes: 30

infiniteloop
infiniteloop

Reputation: 2212

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}
          />
    )
  }
}

Upvotes: 0

Harshal
Harshal

Reputation: 8310

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)}
    />
  )
}

Upvotes: 227

Akinola Olayinka
Akinola Olayinka

Reputation: 861

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...'}
   />

Upvotes: 20

Amir Ur Rehman
Amir Ur Rehman

Reputation: 670

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

changeName: _.debounce(function (val) {
  console.log(val)                
}, 1000)

Upvotes: -1

Rahul
Rahul

Reputation: 1110

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>

Upvotes: 11

Saba
Saba

Reputation: 3666

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);
}

Upvotes: 114

Ninh Ngo
Ninh Ngo

Reputation: 569

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>
    );
  }
}

Upvotes: 56

Khalid Azam
Khalid Azam

Reputation: 1643

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>
        );
    }
}

Upvotes: 3

jiyinyiyong
jiyinyiyong

Reputation: 4713

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.

Upvotes: -4

Related Questions