How to make input field reactive in React?


#1

I’m a little perplexed as to why an input field in a particular component of mine isn’t reactively updating. My app has a contests collection and each contest has it’s own page. I wrap the ContestPage component in a ContestPageContainer component that fetches data and passes it to the ContestPage via props. Here are the two components:

ContestPageContainer

C.ContestPageContainer = React.createClass({
    propTypes: {
        contest_id: React.PropTypes.string
    },
    mixins: [
         ReactMeteorData
    ],

    getInitialState() {
        return {}
    },
    getMeteorData() {
        var contestId = this.props.contest_id;
        var handle = Meteor.subscribe("contest", contestId);

        return {
            contest: Contests.findOne(contestId),
            contestLoading: ! handle.ready(),
            currentUser: Meteor.user()
        };
    },

    componentWillMount() {
    },
    componentDidMount() {

    },
    componentWillReceiveProps() {
    },
    shouldComponentUpdate() {
        return true;
    },
    componentWillUnmount() {
    },

    render() {
        if (this.data.contestLoading) {
            return (
                <C.Loading />
            );
        } else {
            return (
                <C.ContestPage contest={this.data.contest} contestLoading={this.data.contestLoading} currentUser={this.data.currentUser}/>
            )
        }

    }
});

ContestPage

C.ContestPage = React.createClass({
    propTypes: {
        contest: React.PropTypes.object,
        contestLoading: React.PropTypes.bool,
        currentUser: React.PropTypes.object
    },
    mixins: [],

    getInitialState() {
        return {
            errors: {}
        }
    },
    getMeteorData() {
        // return {
        //     currentUser: Meteor.user()
        // };
    },

    componentWillMount() {
    },
    componentDidMount() {
    },
    componentWillReceiveProps() {
    },
    shouldComponentUpdate() {
        return true;
    },
    componentWillUnmount() {
    },

    handleSideToggle(e) {
        e.preventDefault();

        $("#wrapper").toggleClass("toggled");
    },
    handleOnBlur(e) {
        var contestId = this.props.contest._id;
        var property = $(e.target).context.name;
        var value = $(e.target).val();
        var self = this;

        var modifier = {
            '$set': {

            }
        };
        modifier['$set'][property] = value;

        Meteor.call("contests/update", modifier, contestId, function(err) {
            if (err) {
                alert("Contests Update Error: " + err.reason);
            }
        });

    },
    render() {
        return (
            <div classNameName="contest-page-wrapper">
                <div id="wrapper">

                    <C.ContestPageSidebar />

                    <div id="page-content-wrapper">
                        <div className="container-fluid">
                            <div className="row">
                                <div className="col-lg-12">
                                    <div>
                                        <div className="page-title">
                                            <h1>{this.props.contest.title}</h1>
                                        </div>
                                        <div className="col-sm-12 visible-xs-block top-right-button text-left">
                                            <a href="#" className="btn btn-primary" id="menu-toggle" onClick={this.handleSideToggle}>Toggle Menu</a>
                                        </div>


                                    </div>
                                </div>
                            </div>
                            <div className="row">
                                <div className="col-sm-8">
                                    <form id="contest-page">
                                        <C.FormInput hasError={!!this.state.errors.description} name="Description" type="textarea" label="Description" value={this.props.contest.description} onBlur={ this.handleOnBlur }/>
                                        <C.FormInput hasError={!!this.state.errors.prize} name="Prize" type="text" label="Prize" value={this.props.contest.prize} onBlur={ this.handleOnBlur }/>
                                    </form>
                                </div>
                            </div>
                        </div>
                    </div>

                </div>
            </div>
        )

    }
});

If I open two browsers next to each other and update a field (which calls a method and updates the collection) the same field on the other screen doesn’t update. I’m guessing that a form input value property is a special case and I’m going to have to do something special to reactively update it… Is that correct? Or am I doing something wrong?

Also, I am specifically referring to the <C.FormInput /> component in the ContestPage component.


#2

Hey @ryanswapp,

That code looks alright. My hunch is that your Meteor method that’s being called has a bug in it, or your ‘contest’ subscription has a bug in it.

Here’s a couple of unsolicited, opinionated pointers on your code :sweat_smile:

  • Use an indentation of 2 spaces, not 4
  • Don’t bother defining React component methods that aren’t actually used
  • Use const or let instead of var to indicate how that variable will be used. There’s no need to ever use var anymore.
  • You have an excessive amount of <div>s in your markup, a lot of single element containers. Use flexbox.
  • There’s no need to use jQuery’s imperative API (toggleClass) with React’s declarative one
  • Having a Meteor.call inside of an event handler is a sign that your model is bleeding into your view. Put that logic behind a class function and call that.
  • Underscore prefix private component methods
  • For components that you’re passing a lot of props to, do one prop per line.
  • Use object destructuring at the top of your render function to make it clear what state, props, and data are being used

I’m a bit of a formatting fanatic :smiling_imp:

Here’s what your code looks like with some of those pointers taken into account:

C.ContestPageContainer = React.createClass({
  propTypes: {
    contest_id: React.PropTypes.string
  },
  
  mixins: [ReactMeteorData],

  getMeteorData() {
    const contestId = this.props.contest_id;
    const handle = Meteor.subscribe("contest", contestId);

    return {
      contest: Contests.findOne(contestId),
      contestLoading: ! handle.ready(),
      currentUser: Meteor.user()
    };
  },

  render() {  
    const { contest, contestLoading, currentUser} = this.data;
    if ( contestLoading ) {
      return <C.Loading />;
    }
    return (
      <C.ContestPage 
        contest={contest} 
        contestLoading={contestLoading} 
        currentUser={currentUser}/>
    );
  }
});

C.ContestPage = React.createClass({
  propTypes: {
    contest: React.PropTypes.object,
    contestLoading: React.PropTypes.bool,
    currentUser: React.PropTypes.object
  },

  getInitialState() {
    return {
      errors: {},
      toggled: false,
    }
  },

  _handleSideToggle(e) {
    e.preventDefault();
    let { toggled } = this.state;
    toggled = !toggled;
    this.setState({ toggled });
  },

  _handleOnBlur(e) {
    const contestId = this.props.contest._id;
    const property = e.target.context.name;
    const value = e.target.value;

    const modifier = {
      '$set': {
        [property]: value
      }
    };

    Meteor.call("contests/update", modifier, contestId, function(err) {
      if (err) {
        alert("Contests Update Error: " + err.reason);
      }
    });

  },
  render() {
    const { errors, toggled } = this.state;
    const { contest } = this.props;
    const wrapperClasses = toggled ? 'toggled' : '';
    return ( 
      <div className="contest-page-wrapper" >
        <div id="wrapper" className={wrapperClasses}>
          <C.ContestPageSidebar />
          <div id="page-content-wrapper" >
            <div className="container-fluid" >
              <div className="row" >
                <div className="col-lg-12" >
                  <div>
                    <div className="page-title" >
                      <h1>{contest.title}</h1>
                    </div>
                    <div className="col-sm-12 visible-xs-block top-right-button text-left" >
                      <a href="#"
                        className="btn btn-primary"
                        id="menu-toggle"
                        onClick={this._handleSideToggle}>
                        Toggle Menu
                      </a>
                    </div>
                  </div>
                </div>
              </div>
              <div className="row" >
                <div className="col-sm-8" >
                  <form id="contest-page" >
                    <C.FormInput 
                      hasError={!!errors.description}
                      name="Description"
                      type="textarea"
                      label="Description"
                      value={contest.description}
                      onBlur={this._handleOnBlur} /> 
                    <C.FormInput 
                      hasError={!!errors.prize}
                      name="Prize"
                      type="text"
                      label="Prize"
                      value={contest.prize}
                      onBlur={this._handleOnBlur}/> 
                  </form> 
                </div>
              </div>
            </div>
          </div>

Hope some of that is helpful!


#3

@louis Thanks for the pointers! I like your use of destructuring. Much cleaner! I’m still a React noob so I appreciate all the advice I can get.

I’m not sure that there is a problem with the method because the data is updated and it updates the page accordingly, but the input value is not updated. For example (I have actually tried this), if I had <p>{contest.description}</p> right above the form input for the contest description, the paragraph tag would get updated with the new data on a separate screen after the method runs. However, the value of the form input would not change on the other screen even though the paragraph was updated.


#4

You’re welcome!

I see. Can you post the code for the C.FormInput component?


#5

@louis Sure. Here it is:

C.FormInput = React.createClass({
  propTypes: {
    hasError: React.PropTypes.bool,
    label: React.PropTypes.string,
    type: React.PropTypes.string,
    name: React.PropTypes.string,
    value: React.PropTypes.string,
    onKeyUp: React.PropTypes.func,
    onBlur: React.PropTypes.func
  },
  shouldComponentUpdate() {
    return true;
  },
  render() {
    const { type, name, label, value, onKeyUp, onBlur, hasError } = this.props;
    var className = "form-group";
    if (hasError) {
      className += " has-error";
    }

    if (type === "textarea") {
      return (
          <div className={ className }>
              <label htmlFor={ name.toLowerCase() } className="control-label">{ name }</label>
              <textarea type={ type } className="form-control" name={ name.toLowerCase() } placeholder={ label } defaultValue={ value } onKeyUp={ onKeyUp } onBlur={ onBlur }></textarea>
          </div>
      )
    }

    return (
      <div className={ className }>
          <label htmlFor={ name.toLowerCase() } className="control-label">{ name  }</label>
          <input type={ type } className="form-control" name={ name.toLowerCase() } placeholder={ label } defaultValue={ value } onKeyUp={ onKeyUp } onBlur={ onBlur }/>
      </div>
    )
  }
});

#6

interesting, first thing which came to my mind was that
var property = $(e.target).context.name;
is uppercase “Description” and you are expecting “description” property
but I am noob in React and stuff


#7

@shock I just posted the code for the input field which will make the $(e.target).context.name code make more sense. I call .toLowerCase() on it for the name value so that it is the appropriate case when I grab it with my other code.


#8

Yes, I just realized.
But now we can see that you are setting there defaultValue where I would not be 100% sure if React should really update it. I would expect it to keep value, change defaultValue, but that is not affecting value which is shown or returned on .property of that DOM element. Mby ?


#9

@shock ya I was thinking that may be an issue as well. I’m not sure what the best way to approach it is though… I just changed the component to try and work with a regular value property and it still doesn’t work. I’m not really sure how I should be accomplishing this task though… I just added a onChange property.

C.FormInput = React.createClass({
  propTypes: {
    hasError: React.PropTypes.bool,
    label: React.PropTypes.string,
    type: React.PropTypes.string,
    name: React.PropTypes.string,
    value: React.PropTypes.string,
    onKeyUp: React.PropTypes.func,
    onBlur: React.PropTypes.func
  },
  getInitialState() {
    return {
      value: this.props.value
    }
  },
  shouldComponentUpdate() {
    return true;
  },

  handleChange(event) {
    this.setState({
      value: $(event.target).val()
    });
  },

  render() {
    const { type, name, label, onKeyUp, onBlur, hasError } = this.props;
    const { value } = this.state;
    var className = "form-group";
    if (hasError) {
      className += " has-error";
    }

    if (type === "textarea") {
      return (
          <div className={ className }>
              <label htmlFor={ name.toLowerCase() } className="control-label">{ name }</label>
              <textarea type={ type } className="form-control" name={ name.toLowerCase() } placeholder={ label } value={ value } onChange={ this.handleChange } onKeyUp={ onKeyUp } onBlur={ onBlur }></textarea>
          </div>
      )
    }

    return (
      <div className={ className }>
          <label htmlFor={ name.toLowerCase() } className="control-label">{ name  }</label>
          <input type={ type } className="form-control" name={ name.toLowerCase() } placeholder={ label } value={ value } onChange={ this.handleChange } onKeyUp={ onKeyUp } onBlur={ onBlur }/>
      </div>
    )
  }
});

#10

The first set of code you posted didn’t include a handler for onChange and you weren’t passing a function for onKeyUp so the component wouldn’t be updated. The second set does though, it looks like that should work.

React Developer Tools are really handy to debug components if it’s still not updating, you could also try some console logs in the render to see if value is getting passed down correctly.

You also don’t need to use jQuery on the event in handleChange. React has a synthetic event system that standardizes them all, you can just use event.target.value