javascriptreactjsreact-class-based-component

Infinite loop while trying to implement debounce user search on input box react


I have the following app at https://github.com/codyc4321/dividends_ui on branch debounce_search_term. The server can be downloaded at https://github.com/codyc4321/stocks_backend. I have the following app which handle higher level information, the execution of the search, and passing data to dumb displays:

App.js

import React from 'react';

import SearchBar from './SearchBar';
import AllDividendsDisplay from './dividend_results_display/AllDividendsDisplay';
import DividendResultsDisplay from './dividend_results_display/DividendResultsDisplay';

import axios from 'axios';


class App extends React.Component {

  state = {
    current_price: '',
    recent_dividend_rate: '',
    current_yield: '',
    dividend_change_1_year: '',
    dividend_change_3_year: '',
    dividend_change_5_year: '',
    dividend_change_10_year: '',
    all_dividends: [],
  }

  onSearchSubmit = async (term) => {

    // clear old data
    // this.setState({
    //   current_price: '',
    //   recent_dividend_rate: '',
    //   current_yield: '',
    //   dividend_change_1_year: '',
    //   dividend_change_3_year: '',
    //   dividend_change_5_year: '',
    //   dividend_change_10_year: '',
    //   all_dividends: [],
    // });

    const host = 'localhost';
    const base_url = 'http://' + host + ':8000'
    console.log(base_url)
    const price_url = base_url + '/dividends/current_price/' + term
    const recent_rate_url = base_url + '/dividends/recent_dividend_rate/' + term
    const yield_url = base_url + '/dividends/current_yield/' + term

    const REQUEST_MAPPER = [
      {
        url: price_url,
        state_key: 'current_price',
        response_key: 'current_price'
      },
      {
        url: recent_rate_url,
        state_key: 'recent_dividend_rate',
        response_key: 'year_dividend_rate'
      },
      {
        url: yield_url,
        state_key: 'current_yield',
        response_key: 'current_yield'
      },

    ];

    REQUEST_MAPPER.map(request_data => {
      axios.get(request_data.url, {})
        .then(response => {
          this.setState({[request_data.state_key]: response.data[request_data.response_key]});
        })
        .catch(err => {
          console.log(err);
        })
    });


    const YEARS = [1, 3, 5, 10];

    YEARS.map(year => {
      const URL = base_url + '/dividends/dividend_yield_change/' + term + '/' + year.toString();
      const OBJECT_KEY = 'dividend_change_' + year.toString() + '_year';
      axios.get(URL, {})
        .then(response => {
          this.setState({[OBJECT_KEY]: response.data['change']});
        })
        .catch(err => {
          console.log(err);
        });
    });


    const all_dividends_url = base_url + '/dividends/all_dividends/' + term + '/3';
    axios.get(all_dividends_url, {})
      .then(response => {
        this.setState({all_dividends: response.data.reverse()});
      })
      .catch(err => {
        console.log(err);
      });
  }

  render() {
    return (
      <div className="ui container" style={{marginTop: '10px'}}>
        <SearchBar onSubmit={this.onSearchSubmit} />
        <DividendResultsDisplay
          current_price={this.state.current_price}
          recent_dividend_rate={this.state.recent_dividend_rate}
          current_yield={this.state.current_yield}
          dividend_change_1_year={this.state.dividend_change_1_year}
          dividend_change_3_year={this.state.dividend_change_3_year}
          dividend_change_5_year={this.state.dividend_change_5_year}
          dividend_change_10_year={this.state.dividend_change_10_year}
          all_dividends={this.state.all_dividends}
        />
      </div>
    )
  }
}


export default App;

I am trying to implement a search that runs after 2 seconds of the user not typing, each time the search term changes (with a 2.5 sec delay of not typing):

SearchBar.js:

import React from 'react';


class SearchBar extends React.Component {
  state = {
    term: 'psec',
    debouncedTerm: 'psec',
    debounceTimerId: 0
  }

  componentDidUpdate(previousProps, previousState) {
    // this.props.onSubmit(this.state.term);
    console.log("new state:");
    console.log(this.state);
    console.log("previous state:");
    console.log(previousState);

    console.log("props:");
    console.log(this.props);
    console.log("previous props");
    console.log(previousProps);

    // this.props.onSubmit(this.state.term);

    const timerId = setTimeout(() => {
      this.setState({debouncedTerm: this.state.term})
    }, 2500);

    this.setState({debounceTimerId: timerId})

    if (this.state.debouncedTerm !== previousState.debouncedTerm) {
      this.props.onSubmit(this.state.term);
    }
  }

  componentWillUnmount() {
    clearTimeout(this.state.timerId);
  }

  onFormSubmit = (event) => {
    event.preventDefault();
    this.props.onSubmit(this.state.term)
  }

  render() {
    return (
      <div className="ui segment">
        <form onSubmit={this.onFormSubmit} className="ui form">
          <div className="field">
            <label>Stock search</label>
            <input
             type="text"
             value={this.state.term}
             onChange={(e) => this.setState({term: e.target.value})}
             ref={ref => ref && ref.focus()}
             onfocus={(e)=>e.currentTarget.setSelectionRange(e.currentTarget.value.length, e.currentTarget.value.length)}
             />
          </div>
        </form>
      </div>
    );
  };
};

export default SearchBar;

I am getting the following infinite loop error:

react-dom.development.js:27292 Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

I am trying to implement this feature which the course implemented in functional components, not classes. I would like to search when the user changes their search term, after two second delay.


Solution

  • Konrad's comment is bang-on - calling setState in your componentDidUpdate callback is causing the infinite loop.

    You need to remove timerId from your state and make it a class variable instead; something like this:

    class SearchBar extends React.Component {
      debounceTimerId = undefined;
    
      state = {
        term: 'psec',
        debouncedTerm: 'psec',
      }
    
      clearDebounceTimer() {
        if (this.debounceTimerId) {
          clearTimeout(this.debounceTimerId);
          this.debounceTimerId = undefined;
        }
      }
    
      componentDidUpdate(previousProps, previousState) {
        // Don't update the timer unless the term has changed
        if (this.state.term !== previousState.term) {
    
          // Make sure you clear the existing timer first!
          this.clearDebounceTimer();
    
          // Updating a class variable won't force a re-render
          this.debounceTimerId = setTimeout(() => {
            this.setState({ debouncedTerm: this.state.term })
          }, 2500);
        };
      }
    }