Perform debounce in React.js

前端 未结 30 1474
礼貌的吻别
礼貌的吻别 2020-11-22 04:11

How do you perform debounce in React.js?

I want to debounce the handleOnChange.

I tried with debounce(this.handleOnChange, 200) but it doesn\'t

相关标签:
30条回答
  • 2020-11-22 04:44

    2019: try hooks + promise debouncing

    This is the most up to date version of how I would solve this problem. I would use:

    • awesome-debounce-promise to debounce the async function
    • use-constant to store that debounced function into the component
    • react-async-hook to get the result into my component

    This is some initial wiring but you are composing primitive blocks on your own, and you can make your own custom hook so that you only need to do this once.

    // Generic reusable hook
    const useDebouncedSearch = (searchFunction) => {
    
      // Handle the input text state
      const [inputText, setInputText] = useState('');
    
      // Debounce the original search async function
      const debouncedSearchFunction = useConstant(() =>
        AwesomeDebouncePromise(searchFunction, 300)
      );
    
      // The async callback is run each time the text changes,
      // but as the search function is debounced, it does not
      // fire a new request on each keystroke
      const searchResults = useAsync(
        async () => {
          if (inputText.length === 0) {
            return [];
          } else {
            return debouncedSearchFunction(inputText);
          }
        },
        [debouncedSearchFunction, inputText]
      );
    
      // Return everything needed for the hook consumer
      return {
        inputText,
        setInputText,
        searchResults,
      };
    };
    

    And then you can use your hook:

    const useSearchStarwarsHero = () => useDebouncedSearch(text => searchStarwarsHeroAsync(text))
    
    const SearchStarwarsHeroExample = () => {
      const { inputText, setInputText, searchResults } = useSearchStarwarsHero();
      return (
        <div>
          <input value={inputText} onChange={e => setInputText(e.target.value)} />
          <div>
            {searchResults.loading && <div>...</div>}
            {searchResults.error && <div>Error: {search.error.message}</div>}
            {searchResults.result && (
              <div>
                <div>Results: {search.result.length}</div>
                <ul>
                  {searchResults.result.map(hero => (
                    <li key={hero.name}>{hero.name}</li>
                  ))}
                </ul>
              </div>
            )}
          </div>
        </div>
      );
    };
    

    You will find this example running here and you should read react-async-hook documentation for more details.


    2018: try promise debouncing

    We often want to debounce API calls to avoid flooding the backend with useless requests.

    In 2018, working with callbacks (Lodash/Underscore) feels bad and error-prone to me. It's easy to encounter boilerplate and concurrency issues due to API calls resolving in an arbitrary order.

    I've created a little library with React in mind to solve your pains: awesome-debounce-promise.

    This should not be more complicated than that:

    const searchAPI = text => fetch('/search?text=' + encodeURIComponent(text));
    
    const searchAPIDebounced = AwesomeDebouncePromise(searchAPI, 500);
    
    class SearchInputAndResults extends React.Component {
      state = {
        text: '',
        results: null,
      };
    
      handleTextChange = async text => {
        this.setState({ text, results: null });
        const result = await searchAPIDebounced(text);
        this.setState({ result });
      };
    }
    

    The debounced function ensures that:

    • API calls will be debounced
    • the debounced function always returns a promise
    • only the last call's returned promise will resolve
    • a single this.setState({ result }); will happen per API call

    Eventually, you may add another trick if your component unmounts:

    componentWillUnmount() {
      this.setState = () => {};
    }
    

    Note that Observables (RxJS) can also be a great fit for debouncing inputs, but it's a more powerful abstraction which may be harder to learn/use correctly.


    < 2017: still want to use callback debouncing?

    The important part here is to create a single debounced (or throttled) function per component instance. You don't want to recreate the debounce (or throttle) function everytime, and you don't want either multiple instances to share the same debounced function.

    I'm not defining a debouncing function in this answer as it's not really relevant, but this answer will work perfectly fine with _.debounce of underscore or lodash, as well as any user-provided debouncing function.


    GOOD IDEA:

    Because debounced functions are stateful, we have to create one debounced function per component instance.

    ES6 (class property): recommended

    class SearchBox extends React.Component {
        method = debounce(() => { 
          ...
        });
    }
    

    ES6 (class constructor)

    class SearchBox extends React.Component {
        constructor(props) {
            super(props);
            this.method = debounce(this.method.bind(this),1000);
        }
        method() { ... }
    }
    

    ES5

    var SearchBox = React.createClass({
        method: function() {...},
        componentWillMount: function() {
           this.method = debounce(this.method.bind(this),100);
        },
    });
    

    See JsFiddle: 3 instances are producing 1 log entry per instance (that makes 3 globally).


    NOT a good idea:

    var SearchBox = React.createClass({
      method: function() {...},
      debouncedMethod: debounce(this.method, 100);
    });
    

    It won't work, because during class description object creation, this is not the object created itself. this.method does not return what you expect because the this context is not the object itself (which actually does not really exist yet BTW as it is just being created).


    NOT a good idea:

    var SearchBox = React.createClass({
      method: function() {...},
      debouncedMethod: function() {
          var debounced = debounce(this.method,100);
          debounced();
      },
    });
    

    This time you are effectively creating a debounced function that calls your this.method. The problem is that you are recreating it on every debouncedMethod call, so the newly created debounce function does not know anything about former calls! You must reuse the same debounced function over time or the debouncing will not happen.


    NOT a good idea:

    var SearchBox = React.createClass({
      debouncedMethod: debounce(function () {...},100),
    });
    

    This is a little bit tricky here.

    All the mounted instances of the class will share the same debounced function, and most often this is not what you want!. See JsFiddle: 3 instances are producting only 1 log entry globally.

    You have to create a debounced function for each component instance, and not a single debounced function at the class level, shared by each component instance.


    Take care of React's event pooling

    This is related because we often want to debounce or throttle DOM events.

    In React, the event objects (i.e., SyntheticEvent) that you receive in callbacks are pooled (this is now documented). This means that after the event callback has be called, the SyntheticEvent you receive will be put back in the pool with empty attributes to reduce the GC pressure.

    So if you access SyntheticEvent properties asynchronously to the original callback (as may be the case if you throttle/debounce), the properties you access may be erased. If you want the event to never be put back in the pool, you can use the persist() method.

    Without persist (default behavior: pooled event)

    onClick = e => {
      alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`);
      setTimeout(() => {
        alert(`async -> hasNativeEvent=${!!e.nativeEvent}`);
      }, 0);
    };
    

    The 2nd (async) will print hasNativeEvent=false because the event properties have been cleaned up.

    With persist

    onClick = e => {
      e.persist();
      alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`);
      setTimeout(() => {
        alert(`async -> hasNativeEvent=${!!e.nativeEvent}`);
      }, 0);
    };
    

    The 2nd (async) will print hasNativeEvent=true because persist allows you to avoid putting the event back in the pool.

    You can test these 2 behaviors here: JsFiddle

    Read Julen's answer for an example of using persist() with a throttle/debounce function.

    0 讨论(0)
  • 2020-11-22 04:44

    If all you need from the event object is to get the DOM input element, the solution is much simpler – just use ref. Note that this requires Underscore:

    class Item extends React.Component {
        constructor(props) {
            super(props);
            this.saveTitle = _.throttle(this.saveTitle.bind(this), 1000);
        }
        saveTitle(){
            let val = this.inputTitle.value;
            // make the ajax call
        }
        render() {
            return <input 
                        ref={ el => this.inputTitle = el } 
                        type="text" 
                        defaultValue={this.props.title} 
                        onChange={this.saveTitle} />
        }
    }
    
    0 讨论(0)
  • 2020-11-22 04:44

    Lots of good info here already, but to be succinct. This works for me...

    import React, {Component} from 'react';
    import _ from 'lodash';
    
    class MyComponent extends Component{
          constructor(props){
            super(props);
            this.handleChange = _.debounce(this.handleChange.bind(this),700);
          }; 
    
    0 讨论(0)
  • 2020-11-22 04:46

    I found this post by Justin Tulk very helpful. After a couple of attempts, in what one would perceive to be the more official way with react/redux, it shows that it fails due to React's synthetic event pooling. His solution then uses some internal state to track the value changed/entered in the input, with a callback right after setState which calls a throttled/debounced redux action that shows some results in realtime.

    import React, {Component} from 'react'
    import TextField from 'material-ui/TextField'
    import { debounce } from 'lodash'
    
    class TableSearch extends Component {
    
      constructor(props){
        super(props)
    
        this.state = {
            value: props.value
        }
    
        this.changeSearch = debounce(this.props.changeSearch, 250)
      }
    
      handleChange = (e) => {
        const val = e.target.value
    
        this.setState({ value: val }, () => {
          this.changeSearch(val)
        })
      }
    
      render() {
    
        return (
            <TextField
                className = {styles.field}
                onChange = {this.handleChange}
                value = {this.props.value}
            />
        )
      }
    }
    
    0 讨论(0)
  • 2020-11-22 04:47

    You can use Lodash debounce https://lodash.com/docs/4.17.5#debounce method. It is simple and effective.

    import * as lodash from lodash;
    
    const update = (input) => {
        // Update the input here.
        console.log(`Input ${input}`);     
    }
    
    const debounceHandleUpdate = lodash.debounce((input) => update(input), 200, {maxWait: 200});
    
    doHandleChange() {
       debounceHandleUpdate(input);
    }
    

    You can also cancel the debounce method by using the below method.

    this.debounceHandleUpdate.cancel();
    

    Hope it helps you. Cheers!!

    0 讨论(0)
  • 2020-11-22 04:50

    With debounce you need to keep the original synthetic event around with event.persist(). Here is working example tested with React 16+.

    import React, { Component } from 'react';
    import debounce from 'lodash/debounce'
    
    class ItemType extends Component {
    
      evntHandler = debounce((e) => {
        console.log(e)
      }, 500);
    
      render() {
        return (
          <div className="form-field-wrap"
          onClick={e => {
            e.persist()
            this.evntHandler(e)
          }}>
            ...
          </div>
        );
      }
    }
    export default ItemType;
    

    With functional component, you can do this -

    const Search = ({ getBooks, query }) => {
    
      const handleOnSubmit = (e) => {
        e.preventDefault();
      }
      const debouncedGetBooks = debounce(query => {
        getBooks(query);
      }, 700);
    
      const onInputChange = e => {
        debouncedGetBooks(e.target.value)
      }
    
      return (
        <div className="search-books">
          <Form className="search-books--form" onSubmit={handleOnSubmit}>
            <Form.Group controlId="formBasicEmail">
              <Form.Control type="text" onChange={onInputChange} placeholder="Harry Potter" />
              <Form.Text className="text-muted">
                Search the world's most comprehensive index of full-text books.
              </Form.Text>
            </Form.Group>
            <Button variant="primary" type="submit">
              Search
            </Button>
          </Form>
        </div>
      )
    }
    

    References - - https://gist.github.com/elijahmanor/08fc6c8468c994c844213e4a4344a709 - https://blog.revathskumar.com/2016/02/reactjs-using-debounce-in-react-components.html

    0 讨论(0)
提交回复
热议问题