Skip to content

Instantly share code, notes, and snippets.

@rgdelato
Last active May 8, 2017 23:29
Show Gist options
  • Save rgdelato/2bc1543a1823344403d18742f6e753c8 to your computer and use it in GitHub Desktop.
Save rgdelato/2bc1543a1823344403d18742f6e753c8 to your computer and use it in GitHub Desktop.
Forms in React.js

Forms in React.js

Controlled Change

Lets say you want to make form input that shows the user an error message if they leave it blank. If you typically write controlled inputs, you might do something like this simplified example:

class ControlledInput extends Component {
  state = { fields: { username: "" }, errors: {} };

  handleChange = event => {
    const { name, value } = event.target;

    this.setState(state => ({
      fields: { ...state.fields, [name]: value },
      errors: { ...state.errors, [name]: this.validate(value) }
    }));
  };

  validate(value) {
    return value === "" ? "Please don't leave this field blank." : undefined;
  }

  render() {
    const { fields, errors } = this.state;

    return (
      <form>
        <label>
          <input
            type="text"
            name="username"
            value={fields.username}
            onChange={this.handleChange}
          />

          {errors.username
            ? <div className="error">{errors.username}</div>
            : null}
        </label>
      </form>
    );
  }
}

This works, but it re-renders the entire form every time the user types into the input, and the user's typed character won't display until the form is done re-rendering. It's not noticeable in this simple example, but as your form gets larger and your validation gets more complicated, this can start to feel slow for fast-typing users.

Uncontrolled Change

Also, for a majority of our use cases, we don't actually need the user's input the moment they type it. For example, if we only wanted to check the user's input on blur, we could get away with:

<input
  name="username"
  type="text"
  {/* Note: no value prop */}
  onBlur={this.handleBlur}
/>
handleBlur = event => {
  const { name, value } = event.target;

  this.setState(state => ({
    errors: { ...state.errors, [name]: this.validate(value) }
  }));
};

This way, no matter how quickly the user types, we only re-render after the user is finished typing, so typing never feels slow. But the original controlled example used onChange, so let's apply this same kind of strategy to an onChange handler:

<input
  name="username"
  type="text"
  onChange={this.handleChange}
/>
handleChange = event => {
  const { name, value } = event.target;
  const err = this.validate(value);

  if (this.state.errors[name] !== err) {
    this.setState(state => ({ errors: { ...state.errors, [name]: err } }));
  }
};

In this version, we still run the validation every time the input is changed, but we only re-render if the error message is different from its current value. Re-rendering is usually much slower than running validation code, so this if check is usually the only optimization we need.

And for the sake of completeness, lets say that we also want to limit how often we call the validation code as well. Optimizing this is usually overkill, but maybe the validation makes a back-end call or something. You can solve that with a debounce function wrapper:

import debounce from "lodash/debounce";
handleChange = event => {
  const { name, value } = event.target;
  this.handleChangeDebounced({ name, value });
};
handleChangeDebounced = debounce(({ name, value }) => {
  const err = this.validate(value);

  if (this.state.errors[name] !== err) {
    this.setState(state => ({ errors: { ...state.errors, [name]: err } }));
  }
}, 400);

Now this input only re-renders when the error is meaningfully different and the validation is only called every 400 milliseconds at most.

Note that I'm only passing through the specific keys I need from the event object rather than the entire event object in handleChange. Due to React's synthetic event pooling, you'll need to call event.persist() if you pass the entire event through.

Uncontrolled Submit (Serializer)

In the controlled example, it's pretty clear how you get the form data when it's time to submit the form: you use this.state.fields. But when you're using an uncontrolled form, you don't have the data yet, so you have to go get it at the time of form submission. Provided that all of your inputs are properly named, you can do that by passing the form element to your favorite form serializer:

import serializeForm from "form-serialize";
<form onSubmit={this.handleSubmit}>
handleSubmit = event => {
  const formElement = event.target;
  const formData = serializeForm(formElement, { hash: true, empty: true });

  // ...then do something with the form data!
};

Uncontrolled Submit (Instance Variable)

If you really don't want to use a form serializer, you can also keep your form data in an instance variable. In this example, we've added this.fields, which is separate from this.state, so we can update it directly in handleChange without re-rendering.

state = { errors: {} };
fields = {};
handleChange = event => {
  const { name, value } = event.target;
  this.fields[name] = value;
  const err = this.validate(value);

  if (this.state.errors[name] !== err) {
    this.setState(state => ({ errors: { ...state.errors, [name]: err } }));
  }
};

I hope this helps! Until next time, stop trying to control everything! :P

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment