skip to Main Content

I’m encountering a problem with a React component where using setState seems to cause unexpected behavior. Specifically, when I update the state with setState, the dropdown in my Select component (from BaseUI) closes and the input value is cleared, even though I’m only intending to update the state with a new value from the input field.

Expected behavior:

I expect the dropdown to stay open and the input value to remain as I type into the Select component’s input field. I’m trying to make a spinner in the dropdown menu and also limit the number of options in the dropdown menu.

Actual behavior:

Every time I type in the Select component’s input field, the dropdown closes and the input value clears. This happens immediately after setState is called in handleInputChange.

Additional context:

I’m using Redux form to manage form state.
The Select component is from BaseUI.

Question:

What could be causing setState to behave this way, and how can I ensure that updating any states doesn’t interfere with the Select component’s dropdown and input field?

// @noflow
import React, { Component } from 'react';
import { reduxForm, Field } from 'redux-form';
import { compose, bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import get from 'lodash/get';
import validate from '../pipelineValidation';
import { Button, Linkify, Spinner } from '@uber/phase-ui'; 
import {
  validateKafkaSource,
  validateKafkaVolume,
} from '../../../reducers/pipeline';
import { Select } from 'baseui/select';
import { StyledLink } from 'baseui/link';
import { Label2 } from 'baseui/typography';

class KafkaPipelineSource extends Component {
  constructor(props) {
    super(props);
    this.state = {
      kafkaTopicLists: [],
      loading: false, // Track loading state for spinner
    };
  }

  handleSelectChange = ({ value }) => {
    const selectedLabel = value.length > 0 ? value[0].label : '';
    const { change } = this.props;
    change('kafkaTopic', selectedLabel); // Update form value with selected label
  };

  handleInputChange = event => {
    const inputValue = event.target.value.toLowerCase(); // Get input value and convert to lowercase
    // Update state only if input value changes to prevent unnecessary re-renders
    this.setState({ loading: true }, () => {
      // Simulate asynchronous filtering (replace with actual logic)
      setTimeout(() => {
        const filteredOptions = this.props.kafkaTopicList
          .filter(option => option.toLowerCase().includes(inputValue))
          .slice(0, 100)
          .map(value => ({
            id: value,
            label: value,
          }));
        this.setState({
          kafkaTopicLists: filteredOptions,
          loading: false, // Set loading state to false after filtering
        });
      }, 300); // Simulating a delay for filtering
    });
  };

  validateKafkaTopic = () => {
    const { kafkaTopic, validateKafkaSource } = this.props;
    validateKafkaSource({
      datasetName: kafkaTopic,
    });
  };

  validateKafkaVolume = () => {
    const { kafkaTopic, validateKafkaVolume } = this.props;
    validateKafkaVolume({
      datasetName: kafkaTopic,
    });
  };

  render() {
    const {
      kafkaTopicList = [],
      previousPage,
      handleSubmit,
      kafkaTopic,
      kafkaSource,
      sinkType,
      kafkaVolume,
    } = this.props;

    const { kafkaTopicLists, loading } = this.state;

    console.log('kafkaTopic:', kafkaTopic);
    return (
      <form className="mm-top" onSubmit={handleSubmit}>
        <Label2>Heatpipe Kafka Topic</Label2>
        <div>
          Choose heatpipe avro or protobuf topic to ingest. If you do not have
          one yet, create one.
        </div>
        <Field
          name="kafkaTopic"
          component={({ meta: { touched, error, warning } }) => (
            <div>
              <Select
                ariaLabel="kafkaTopic"
                options={kafkaTopicLists}
                value={kafkaTopicLists.find(o => o.id === kafkaTopic)}
                placeholder="Select..."
                labelKey="id"
                valueKey="label"
                onChange={this.handleSelectChange}
                onInputChange={this.handleInputChange}
                searchable
                overrides={{
                  Dropdown: {
                    style: {
                      maxHeight: '300px',
                    },
                  },
                }}
              />
              {loading && <Spinner size={24} />}
              {touched &&
                ((error && <span>{error}</span>) ||
                  (warning && <span>{warning}</span>))}
            </div>
          )}
        />

2

Answers


  1. That behaviour that you are getting—the closing of the dropdown and clearing of the input value when updating the state—is because of the way that React works with state and component re-renders.

    On calling setState, a re-render of the component is triggered, where this dropdown could now close and the input reset. To avoid closing the dropdown and resetting the input using setState, debounce the handleInputChange method. This reduces re-renders and maintains the input field’s value.

    Here’s a quick implementation:

    Install the lodash.debounce package:
    npm install lodash.debounce

    Import and use debounce in your handleInputChange method:

    import debounce from 'lodash.debounce';
    ...
    this.handleInputChange = debounce(this.handleInputChange.bind(this), 300);
    ...
    
    Login or Signup to reply.
  2. I don’t have any experience with Base UI, but the input probably resets because of what you set as value property of the Select component:

    <Select
      //...
      value={kafkaTopicLists.find(o => o.id === kafkaTopic)}
    

    According to the docs, the value should always be an array. If I were to guess, there is probably a warning in your Console about this.

    Unfortunately, I don’t know why the dropdown would close.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search