4

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.

Tania Rascia
  • 1,563
  • 17
  • 35
  • Can you check whether debounced function call binds the value properly? I'm not sure the `handleSearchChange` method receives the value properly. – CRayen Aug 30 '18 at 18:33
  • Well, the filtering is technically working, so it must be calling the `isMatch` function. – Tania Rascia Aug 30 '18 at 18:35
  • Hi where are you showing `No data` text ? can you make a codesandbox – Sakhi Mansoor Aug 30 '18 at 18:37
  • No data is just the default response of the table when results are 0. – Tania Rascia Aug 30 '18 at 18:39
  • Sorry, I removed `noResultsMessage`. That shows a display in a dropdown that says "No results found" from Semantic that I'm not using. I hide that whole section with `open={false}`. – Tania Rascia Aug 30 '18 at 18:57
  • Can't you just not display the table when the `this.state.isLoading`? `{!this.state.isLoading &&
    }` This way you can put a spinner etc if it is loading.
    – Akrion Aug 30 '18 at 19:53

1 Answers1

2

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>
    )
  }
}
Akrion
  • 18,117
  • 1
  • 34
  • 54
  • This would cause the entire table to appear and disappear as each search is made. – Tania Rascia Aug 30 '18 at 20:37
  • On each start of a search the user would see a spinner and then when the results come back he would see the table. Isn't this kind of the accepted UX pattern these days? – Akrion Aug 30 '18 at 20:41
  • I don't think so. The entire table appearing and reappearing would be jarring. I would rather the search just not update the table until loading is complete. – Tania Rascia Aug 30 '18 at 21:10
  • Added another option. – Akrion Aug 30 '18 at 23:20
  • This is perfect, @Akrion, thank you. Setting the isLoading to display the old search results is a much smoother user experience. – Tania Rascia Aug 31 '18 at 14:13