Atte
Atte

Reputation: 308

React render is not called on supposed setState call

I am trying to implement a button which switches between two displayed forms. However, this is not working, as no change occurs on button click. I have the following code:

import React, {Component} from "react";
import ShortenForm from "./ShortenForm";
import UnshortenForm from "./UnshortenForm";

class FormSelector extends Component {
    constructor(props) {
        super(props);
        this.state = {
            shorten: this.props.shorten // Pass true or false
        };
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(event) {
        this.setState({shorten: event.target.value});
    }

    render() {
        let form;
        let button;
        if (this.state.shorten) {
            form = <ShortenForm placeholder='Enter URL to shorten'/>;
            button = <button onClick={this.handleChange} value={false}>Change</button>;
        } else {
            form = <UnshortenForm placeholder='Enter URL to unshorten'/>;
            button = <button onClick={this.handleChange} value={true}>Change</button>;
        }

        return (
            <React.Fragment>
            {form}
            {button}
            </React.Fragment>

        );
    }
}

export default FormSelector;

Upvotes: 0

Views: 54

Answers (3)

Raghav
Raghav

Reputation: 480

I see a few answers here, but I think your key question could still be addressed. So I'm going to try and do that here, and maybe consolidate all this information just a bit.

The value prop that you're passing is actually a string, not a boolean value. Hence, you're assigning the string "true" when you mean to be assigning the boolean true - this is what messes up your if condition, as in Javascript (as in other languages) a non-empty string is what we call a truthy value, and hence your if condition will always evaluate to true. This is your key issue here, above all else. Replacing this with a boolean value and negating it (as the other answers show) most definitely works, but this won't work when you have multiple forms. If this is the case, I would use string identifiers, embracing the fact that value passes a string, and render different forms accordingly.

I've modified your class, this should give you a good idea of what I mean. You can also find a CodeSandbox here, which should give you a slightly more interactive idea

import React, { Component } from "react";

class FormSelector extends Component {
  constructor(props) {
    super(props);
    this.state = {
      shorten: this.props.shorten
    };
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(event) {
    console.log("Setting: " + event.target.value);
    this.setState({ shorten: event.target.value });
  }

  render() {
    // This bit is just for illustrative purposes
    console.log(this.state.shorten);
    console.log(typeof this.state.shorten);
    if (this.state.shorten === "short") {
      return (
        <React.Fragment>
          <span>Shortened</span>
          <button onClick={this.handleChange} value={"unshort"}>
            Change
          </button>
        </React.Fragment>
      );
    } else {
      return (
        <React.Fragment>
          <span>Unshortened</span>
          <button onClick={this.handleChange} value={"short"}>
            Change
          </button>
        </React.Fragment>
      );
    }
  }
}

export default FormSelector;

Upvotes: 1

Ajeet Shah
Ajeet Shah

Reputation: 19863

You should use updater argument of setState in this case:

this.setState((state, props) => {
  return {shorten: !state.shorten};
});

As, from docs:

updater argument: (state, props) => stateChange

Both state and props received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged with state.

Upvotes: 1

JMadelaine
JMadelaine

Reputation: 2964

In your handleChange function, you're setting state to the value of event.target.value, but the value is a string, so both 'true' and 'false' will return true.

This means that this.state.shorten will always be true, so the ShortenForm will always render.

You shouldn't be using the value of a button to determine state, as the value attribute is treated differently across browsers.

You also don't need to create two different buttons and choose which one to render based on state. Just render the same button that calls the same function every time. All the function does is invert the current state value:

handleChange = () => this.seState(prev => ({ shorten: !prev.shorten })

You don't need to provide a value to the button:

return (
  <>
    {form}
    <button onClick={this.handleChange}>Change</button>
  </>
)

Upvotes: 1

Related Questions