React: Changing defaultValue doesn't re-render input, causes hidden state in the db

Created on 12 Jun 2015  路  8Comments  路  Source: facebook/react

I posted this on StackOverflow but the dearth of answers makes me think it might be a bug.

Given the following simple code

const {createFactory, createClass, DOM: { label, input, button }} = React;

const tester = createFactory(createClass({
  render() {
      return label({}
               ,`Name: ${this.props.name}`
               ,input({defaultValue: this.props.name})
               ,button({onClick: this.changeName}, "Change")
             )
  },
  changeName() {
    this.setProps({name: "Wilma"})
  }
}) )

React.render(tester({name: "Fred"}), document.querySelector('body'))

clicking the button doesn't re-render the input and it still says "Fred" even though this.props.name is "Wilma".

Demo

Most helpful comment

I posted this on the SO but I'll include it here as well.

I found what seems to be a pretty good solution to this: Use the key prop to force rendering of an entirely new input.

In my particular case, I don't need the input to be controlled with its own onChange prop, as the form surrounding it ultimately controls the state within some store which populates the defaultValue. But the store's state might be asynchronously initialized/updated, and in which case the defaultValue should be updated. So here is a condensed version of my particular case:

import React, { PropTypes } from 'react';
import { Form } from 'provide-page';

const GatherContact = ({
  classes,
  onSubmit,
  movingContactName,
  movingContactEmail,
  movingContactPhone,
  userName,
  userEmail,
  userPhone
}) => (
  <Form onSubmit={onSubmit}>
    <div className={classes.GatherContact}>
      <h2 className={classes.GatherHeading}>
        How can we contact you?
      </h2>

      <input
        type="text"
        className={classes.GatherContactInput}
        placeholder="Name"
        name="movingContactName"
        key={`movingContactName:${movingContactName || userName}`}
        defaultValue={movingContactName || userName}
        required={true}
      />

      <input
        type="email"
        className={classes.GatherContactInput}
        placeholder="Email"
        name="movingContactEmail"
        key={`movingContactEmail:${movingContactEmail || userEmail}`}
        defaultValue={movingContactEmail || userEmail}
        required={true}
      />

      <input
        type="tel"
        className={classes.GatherContactInput}
        placeholder="Phone"
        name="movingContactPhone"
        key={`movingContactPhone:${movingContactPhone || userPhone}`}
        defaultValue={movingContactPhone || userPhone}
        required={true}
      />

      {userName
        ? undefined
        : (
          <input
            type="password"
            className={classes.GatherContactInput}
            placeholder="Password"
            name="userPassword"
            required={true}
            autoComplete="new-password"
          />
        )
      }
    </div>
  </Form>
);

GatherContact.propTypes = {
  classes: PropTypes.object.isRequired,
  onSubmit: PropTypes.func.isRequired,
  movingContactName: PropTypes.string.isRequired,
  movingContactEmail: PropTypes.string.isRequired,
  movingContactPhone: PropTypes.string.isRequired,
  userName: PropTypes.string.isRequired,
  userEmail: PropTypes.string.isRequired,
  userPhone: PropTypes.string.isRequired
};

export default GatherContact;

All 8 comments

The answer you got on Stack Overflow is correct. defaultValue only specifies the value when the input is initially created; if you want it to remain updated you need to use value and handle onChange events correctly.

We don't update when defaultValue changes because otherwise you have two sources (the user's input and your component props) trying to control the value, and it wouldn't be clear what should happen when they conflict. Instead, we force you to think about and write out the actual behavior you want.

But hold on, in that case I'm going to argue that this _is_ a bug as you can end up with a scenario where the current HTML is not stateless.

The specific situation where I ran into this - a grid, and when double clicking on a row I popped open a dialog form to edit the row items. I noticed that my input values were stuck to the first value I opened even though everything else changed. How can this be correct behaviour? The whole point is that state is not stored in the DOM...

If changing defaultValue doesn't cause the input to re-render then you can pass the exact same model through render twice and get different HTML output.

Also the docs need to be updated then, as they imply that defaultValue is the correct thing to use if you don't want to lock the value

The whole point is that state is not stored in the DOM...

With defaultValue, the state _is_ stored in the DOM. That's why we don't recommend it. We recommend using controlled components instead. The docs say:

If you wanted to update the value in response to user input, you could use the onChange event.

(http://facebook.github.io/react/docs/forms.html#controlled-components)

Let me know if that's still confusing.

I posted this on the SO but I'll include it here as well.

I found what seems to be a pretty good solution to this: Use the key prop to force rendering of an entirely new input.

In my particular case, I don't need the input to be controlled with its own onChange prop, as the form surrounding it ultimately controls the state within some store which populates the defaultValue. But the store's state might be asynchronously initialized/updated, and in which case the defaultValue should be updated. So here is a condensed version of my particular case:

import React, { PropTypes } from 'react';
import { Form } from 'provide-page';

const GatherContact = ({
  classes,
  onSubmit,
  movingContactName,
  movingContactEmail,
  movingContactPhone,
  userName,
  userEmail,
  userPhone
}) => (
  <Form onSubmit={onSubmit}>
    <div className={classes.GatherContact}>
      <h2 className={classes.GatherHeading}>
        How can we contact you?
      </h2>

      <input
        type="text"
        className={classes.GatherContactInput}
        placeholder="Name"
        name="movingContactName"
        key={`movingContactName:${movingContactName || userName}`}
        defaultValue={movingContactName || userName}
        required={true}
      />

      <input
        type="email"
        className={classes.GatherContactInput}
        placeholder="Email"
        name="movingContactEmail"
        key={`movingContactEmail:${movingContactEmail || userEmail}`}
        defaultValue={movingContactEmail || userEmail}
        required={true}
      />

      <input
        type="tel"
        className={classes.GatherContactInput}
        placeholder="Phone"
        name="movingContactPhone"
        key={`movingContactPhone:${movingContactPhone || userPhone}`}
        defaultValue={movingContactPhone || userPhone}
        required={true}
      />

      {userName
        ? undefined
        : (
          <input
            type="password"
            className={classes.GatherContactInput}
            placeholder="Password"
            name="userPassword"
            required={true}
            autoComplete="new-password"
          />
        )
      }
    </div>
  </Form>
);

GatherContact.propTypes = {
  classes: PropTypes.object.isRequired,
  onSubmit: PropTypes.func.isRequired,
  movingContactName: PropTypes.string.isRequired,
  movingContactEmail: PropTypes.string.isRequired,
  movingContactPhone: PropTypes.string.isRequired,
  userName: PropTypes.string.isRequired,
  userEmail: PropTypes.string.isRequired,
  userPhone: PropTypes.string.isRequired
};

export default GatherContact;

My solution is to add a wrapper component, which uses local state when the user is editing the value, and only triggers the onChange function once focus is lost (or equivalent).

Here's an example, for the npm "rc-slider" component:

import RCSlider from "rc-slider";

export class Slider extends Component
        <{min: number, max: number, step: number, value: number, delayChangeTillDefocus?: boolean, onChange: (val: number)=>void},
        {editedValue: number}> {
    slider: RCSlider;
    render() {
        let {value, delayChangeTillDefocus, onChange, ...rest} = this.props;
        let {editedValue} = this.state;
        return (
            <RCSlider ref={c=>this.slider = c} {...rest} value={editedValue != null ? editedValue : value} onChange={val=> {
                if (delayChangeTillDefocus) {
                    this.setState({editedValue: val});
                } else {
                    onChange(val);
                    this.setState({editedValue: null});
                }
            }}
            onDefocus={val=> {
                if (delayChangeTillDefocus && onChange) {
                    onChange(val);
                    this.setState({editedValue: null});
                }
            }}/>
        );
    }
}

The whole point is that state is not stored in the DOM...

With defaultValue, the state _is_ stored in the DOM. That's why we don't recommend it. We recommend using controlled components instead. The docs say:

If you wanted to update the value in response to user input, you could use the onChange event.
(http://facebook.github.io/react/docs/forms.html#controlled-components)

Let me know if that's still confusing.```

It is still confusing.
As I remember the value property of input is mutable but defaultValue is not. You can only set defaultValue from outside of the element. So for me it makes more sense to use defaultValue if you want to use controlled component. I can see issue with using value: when user choose something in select and using value prop to populate chosen value through the state(controlled way) value is added twice to the element.
In controlled way you kind of rerender the component, basically initialize it with new options every time they change, which should update component as it was created from the scratch. Then the concept that says that component is updated every time props are changed is not always true.

I posted this on the SO but I'll include it here as well.

I found what seems to be a pretty good solution to this: Use the key prop to force rendering of an entirely new input.

In my particular case, I don't need the input to be controlled with its own onChange prop, as the form surrounding it ultimately controls the state within some store which populates the defaultValue. But the store's state might be asynchronously initialized/updated, and in which case the defaultValue should be updated. So here is a condensed version of my particular case:

import React, { PropTypes } from 'react';
import { Form } from 'provide-page';

const GatherContact = ({
  classes,
  onSubmit,
  movingContactName,
  movingContactEmail,
  movingContactPhone,
  userName,
  userEmail,
  userPhone
}) => (
  <Form onSubmit={onSubmit}>
    <div className={classes.GatherContact}>
      <h2 className={classes.GatherHeading}>
        How can we contact you?
      </h2>

      <input
        type="text"
        className={classes.GatherContactInput}
        placeholder="Name"
        name="movingContactName"
        key={`movingContactName:${movingContactName || userName}`}
        defaultValue={movingContactName || userName}
        required={true}
      />

      <input
        type="email"
        className={classes.GatherContactInput}
        placeholder="Email"
        name="movingContactEmail"
        key={`movingContactEmail:${movingContactEmail || userEmail}`}
        defaultValue={movingContactEmail || userEmail}
        required={true}
      />

      <input
        type="tel"
        className={classes.GatherContactInput}
        placeholder="Phone"
        name="movingContactPhone"
        key={`movingContactPhone:${movingContactPhone || userPhone}`}
        defaultValue={movingContactPhone || userPhone}
        required={true}
      />

      {userName
        ? undefined
        : (
          <input
            type="password"
            className={classes.GatherContactInput}
            placeholder="Password"
            name="userPassword"
            required={true}
            autoComplete="new-password"
          />
        )
      }
    </div>
  </Form>
);

GatherContact.propTypes = {
  classes: PropTypes.object.isRequired,
  onSubmit: PropTypes.func.isRequired,
  movingContactName: PropTypes.string.isRequired,
  movingContactEmail: PropTypes.string.isRequired,
  movingContactPhone: PropTypes.string.isRequired,
  userName: PropTypes.string.isRequired,
  userEmail: PropTypes.string.isRequired,
  userPhone: PropTypes.string.isRequired
};

export default GatherContact;

I cannot say that this is good solution, as it disables some features of select, for example. In case with multiple select if you use this WA, and you need select many items, you will have to click as many times as many element you have to choose. Annoying but still can work. This is good WA but not the solution.

Was this page helpful?
0 / 5 - 0 ratings