10

I know some react but I am stuck in a weird situation.

I have two inputs and a button, the button should be enabled when both inputs are not empty. So, I used a state property for each input value and also one property telling me if both inputs have value:

this.state = {
  title: '',
  time :'', 
  enabled : false
}

also I have a onChange for each input to set State accordingly:

<input type="text" id="time" name="time" onChange={this.onChange.bind(this)} value={this.state.time}></input>
<input type="text" id="title" name="title" onChange={this.onChange.bind(this)} value={this.state.title}></input>

and the onChange is like this

onChange(e){
    this.setState({
        [e.target.id] : e.target.value,
        enabled : ( (this.state.title==='' || this.state.time==='' ) ? false : true)
      });
  }

the problem is that the setState looks at previous state and the enabled is always one step behind, so if I type X in first and Y on second, still the enabled would be false.

I managed to solve it by using a setTimeout and taking the second line in it but it looks wrong to me.

  onChange(e){

    this.setState({
        [e.target.id] : e.target.value,
    });

    setTimeout(() => {
      this.setState({
        enabled : ( (this.state.title==='' || this.state.time==='' ) ? false : true)
    });
    }, 0);

  }

Any better solutions?

Amir Shahbabaie
  • 1,352
  • 2
  • 14
  • 33

4 Answers4

15

Any better solutions?

There are 3 solutions for you to achieve that, just pick one


Solution 1: Using the componentDidUpdate()

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: "",
      time: "",
      enabled: false
    };
  }
  onChange(e) {
    this.setState({
      [e.target.id]: e.target.value
    });
  }
   componentDidUpdate(prevProps, prevState) {
     if (
       prevState.time !== this.state.time ||
       prevState.title !== this.state.title
     ) {
       if (this.state.title && this.state.time) {
         this.setState({ enabled: true });
       } else {
         this.setState({ enabled: false });
       }
     }
   }
  render() {
    return (
      <React.Fragment>
        <input
          type="text"
          id="time"
          name="time"
          onChange={this.onChange.bind(this)}
          value={this.state.time}
        />
        <input
          type="text"
          id="title"
          name="title"
          onChange={this.onChange.bind(this)}
          value={this.state.title}
        />
        <button disabled={!this.state.enabled}>Button</button>
      </React.Fragment>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>

Solution 2: Using the setState callback

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: "",
      time: "",
      enabled: false
    };
  }
  onChange(e) {
     this.setState({[e.target.id]: e.target.value},
       () => {
         if (this.state.title && this.state.time) {
           this.setState({ enabled: true });
         } else {
           this.setState({ enabled: false });
         }
       }
     );
  }
  render() {
    return (
      <React.Fragment>
        <input
          type="text"
          id="time"
          name="time"
          onChange={this.onChange.bind(this)}
          value={this.state.time}
        />
        <input
          type="text"
          id="title"
          name="title"
          onChange={this.onChange.bind(this)}
          value={this.state.title}
        />
        <button disabled={!this.state.enabled}>Button</button>
      </React.Fragment>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>

Solution 3: Calculating the enabled based on title and time

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: "",
      time: ""
    };
  }
  onChange(e) {
    this.setState({
      [e.target.id]: e.target.value
    });
  }
  render() {
    const enabled = this.state.time && this.state.title;
    return (
      <React.Fragment>
        <input
          type="text"
          id="time"
          name="time"
          onChange={this.onChange.bind(this)}
          value={this.state.time}
        />
        <input
          type="text"
          id="title"
          name="title"
          onChange={this.onChange.bind(this)}
          value={this.state.title}
        />
        <button disabled={!enabled}>Button</button>
      </React.Fragment>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
You Nguyen
  • 9,961
  • 4
  • 26
  • 52
6

First of all, you do not need to maintain a state for enabled, since it can be derived from other state values and can be directly done in render

onChange(e){
    this.setState({
        [e.target.id] : e.target.value,
    });
}
render() {
   const enabled = this.state.title !== "" && this.state.time !== "";
   return (
       <div>
          <input type="text" id="time" name="time" onChange={this.onChange.bind(this)} value={this.state.time}></input>
          <input type="text" id="title" name="title" onChange={this.onChange.bind(this)} value={this.state.title}></input>
          <button disabled={!enabled}>Button</button>
       </div>
   )
}
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
1

If you want set state after current changes took place, you can pass a second parameter (callback) to the setState, which will happen after state is already set and set you state there again. It will cause additional rerender, but it seems that's what you're asking. This is just so you know how to do something after current state changes took place...

But, I would go with Shubham Khatri' answer. That would be far more logical.

Javid Asgarov
  • 1,511
  • 1
  • 18
  • 32
1

@Shubham Khatri has the correct answer (you don't really need the enabled property in the state) but it's important to understand what is going on with the state and why your hack of using setTimeout worked (luckily in my opinion).

From the official docs

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied.

Also, state updates may be asynchronous:

React may batch multiple setState() calls into a single update for performance.

Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.

I hope this clarifies your issue.

c-chavez
  • 7,237
  • 5
  • 35
  • 49