Search code examples
javascriptreactjstimeoutlodashsemantic-ui-react

Search bar to filter table results in React with timeout


I'm using Semantic React UI Search to filter results of a data table component in React. The table should display all data if search is empty, and display no data or the matching results if search is not empty. My issue is there's always a quick flash of "No data" while you're doing a search.

The original Search code displayed the results as a dropdown, but I modified it to modify the table. Code is below.

class Page extends Component {
  resetComponent = () => this.setState({ isLoading: false, results: [], value: '' })

  handleSearchChange = (e, { value }) => {
    this.setState({ isLoading: true, value })

    setTimeout(() => {
      if (this.state.value.length < 1) return this.resetComponent()

      const re = new RegExp(_.escapeRegExp(this.state.value), 'i')
      const isMatch = result => re.test(result.name)

      this.setState({
        isLoading: false,
        results: _.filter(this.props.users, isMatch),
      })
    }, 200)
  }

  render() {
    const { users } = this.props
    const { value, results } = this.state
    const dataToShow = _.isEmpty(results) && !value ? users : results

    return (
      <Container>
        <Search
          open={false}
          loading={isLoading}
          onSearchChange={_.debounce(this.handleSearchChange, 500, { leading: true })}
          value={value}
          {...this.props}
        />
        <Table data={dataToShow} />
      </Container>
    )
  }
}

I think the const dataToShow = _.isEmpty(results) && !value ? users : results line is what causes it to flash, but I don't know how else to display no results if no match, or all results if empty.

How can I get this timeout/debounce to work properly on the table?

If I do <Table data={results} /> the debounce works, but the table does not display all data on initial load.


Solution

  • What is actually happening is when you set the this.setState({ isLoading: true, value }) the component will re-render since you changed the state. When this happens this line:

    const dataToShow = _.isEmpty(results) && !value ? users : results
    

    would actually show the results - since although the results are empty you do have a value typed. Which is why you get the 'No Data` since you bind to results but they are empty.

    Try this there:

    const dataToShow = _.isEmpty(results) && !value ? users : this.state.isLoading ? users : results
    

    It should continue to show the users when there is value typed and once you are done loading it should change to the results.

    The issue however is (which is why I suggested the easy way out with the spinner) that now you would show results ... then on new search you would go back to the users then go again to the results when done loading.

    I would not display at all the <Table> while this.state.isLoading is true and display some "spinner" if it is ... for example:

        class Page extends Component {
          resetComponent = () => this.setState({ isLoading: false, results: [], value: '' })
        
          handleSearchChange = (e, { value }) => {
            setTimeout(() => {
               this.setState({ isLoading: true, value })
    
               if (this.state.value.length < 1) return this.resetComponent()
               const re = new RegExp(_.escapeRegExp(this.state.value), 'i')
               const isMatch = result => re.test(result.name)
               this.setState({
                  isLoading: false,
                  results: _.filter(this.props.users, isMatch),
               })
            }, 200)
    	  }
    
          render() {
            const { users } = this.props
            const { value, results } = this.state
            const dataToShow = _.isEmpty(results) && !value ? users : results
        
            return (
              <Container>
                <Search
                  open={false}
                  loading={isLoading}
                  onSearchChange={_.debounce(this.handleSearchChange, 500, { leading: true })}
                  value={value}
                  {...this.props}
                />
                {this.state.isLoading && <Spinner />}
                {!this.state.isLoading && <Table data={dataToShow} />}
              </Container>
            )
          }
        }

    But since we disagree on that UX pattern here is another suggestion:

    Keep track of the previous results and keep showing them until the new state change happens with the new results:

    class Page extends Component {
      constructor (props) {
         super(props)
         this.state = {
            isLoading: false,
            results: [],
            oldResults: this.prop.users || [],
            value: ''
         }
       }
    	
      resetComponent = () => this.setState({ isLoading: false, results: [], oldResults: this.prop.users || [], value: '' })
    
      handleSearchChange = (e, { value }) => {
        setTimeout(() => {
        this.setState({ isLoading: true, value })
    
        if (this.state.value.length < 1) return this.resetComponent()
    			 
        const re = new RegExp(_.escapeRegExp(this.state.value), 'i')
        const filteredResults = _.filter(this.props.users, result => re.test(result.name))
           this.setState({
             isLoading: false,
             results: filteredResults,
             oldResults: filteredResults
           })
        }, 200)
      }
    
      render() {
        const { users } = this.props
        const { value, results } = this.state
        const dataToShow = (_.isEmpty(results) && !value) || this.state.isLoading ? oldResults : results
        return (
          <Container>
            <Search
              open={false}
              loading={isLoading}
              onSearchChange={_.debounce(this.handleSearchChange, 500, { leading: true })}
              value={value}
              {...this.props}
            />
    	<Table data={dataToShow} />
          </Container>
        )
      }
    }