Making dynamic form inputs with React

18 Jan 2017

Forms seem very simple so far. Indeed, when we only have a fixed set of fields, it’s pretty easy to make them into code.

In real apps, however, forms are often a bit more lively and dynamic. A common example is having a set of similar inputs that are backed by an array.

For example, say we have an incorporation form. Among other fields, a company has a variable number of shareholders.

Typically when dealing with array inputs, here are the things we can do with them:

How do we make a form that allows editing such an array?

Data model

It always helps to think about the data first.

It might seem simple at first.

['Shareholder 1', 'Shareholder 2']

but… in life there’s always more, to everything.

Avoid primitive types. While it might be tempting to make an array of strings to represent shareholders (or anything else that seems to only require one input), there are a couple of potential pitfalls:

A better default would be to always start with an array of objects.

{ name: '' }

It gives you a chance to give a meaningful name to the field, even if you only have one; as well as makes future additions of fields easier.

But what if I really need an array of primitives? This storage format is convenient within the form, but it’s important to note that you can send to, and receive from, the server the data in any format. Even an array of strings. You will just have to transform between an array of strings and an array of objects, and vice versa, when interacting with the server.

Operations on data

Recall that we need to do the following with the array:

Adding an item is easy.

For deleting an item, we’re going to need to identify it somehow. An index will do for our case.

To change a field, besides an identifier, we need to know which field to change. There is more than one valid way to implement this use case:

The former could serve well to reduce boilerplate if there are several fields. The latter could be more flexible, as it will allow to execute different logic depending on the field.

For the purpose of this post, I only have one field and will make a specialized function.

Don’t mutate the array

While it could be tempting to push to the array or do something like this.state.shareholders[idx].name = newName, this is not a sound approach.

First, React will not know that something has changed if you do this. This is because React is only re-rendering when either the props or the state change. Mutating the state object, or any nested object, keeps the identity of the object, and React thinks nothing has changed.

We have to call setState with a new value to let React know it should re-render.

Second, mutations are prone to unexpected bugs. Using non-mutating methods for changing arrays is not that hard.

To add a new item, you can use the .concat method on array, and set the resulting array with setState:

this.setState({
  shareholders: this.state.shareholders.concat([{ name: '' }]),
});

To remove an item, using .filter is the easiest non-mutating alternative:

// assuming `idx` is defined and is an index of an item to remove
this.setState({
  shareholders: this.state.shareholders.filter((s, _idx) => _idx !== idx),
});

And finally, to change an existing item, we can make use of .map and Object.assign/object spread notation:

this.setState({
  shareholders: this.state.shareholders.map((s, _idx) => {
    if (_idx !== idx) return s;
    // this is gonna create a new object, that has the fields from
    // `s`, and `name` set to `newName`
    return { ...s, name: newName };
  }),
});

Piecing it all together

Rendering the input for each shareholder is trivial: we just loop over this.state.shareholders.

class IncorporationForm extends React.Component {
  constructor() {
    super();
    this.state = {
      name: '',
      shareholders: [{ name: '' }],
    };
  }

  // ...

  handleShareholderNameChange = (idx) => (evt) => {
    const newShareholders = this.state.shareholders.map((shareholder, sidx) => {
      if (idx !== sidx) return shareholder;
      return { ...shareholder, name: evt.target.value };
    });

    this.setState({ shareholders: newShareholders });
  }

  handleSubmit = (evt) => {
    const { name, shareholders } = this.state;
    alert(`Incorporated: ${name} with ${shareholders.length} shareholders`);
  }

  handleAddShareholder = () => {
    this.setState({
      shareholders: this.state.shareholders.concat([{ name: '' }])
    });
  }

  handleRemoveShareholder = (idx) => () => {
    this.setState({
      shareholders: this.state.shareholders.filter((s, sidx) => idx !== sidx)
    });
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        {/* ... */}
        <h4>Shareholders</h4>

        {this.state.shareholders.map((shareholder, idx) => (
          <div className="shareholder">
            <input
              type="text"
              placeholder={`Shareholder #${idx + 1} name`}
              value={shareholder.name}
              onChange={this.handleShareholderNameChange(idx)}
            />
            <button type="button" onClick={this.handleRemoveShareholder(idx)} className="small">-</button>
          </div>
        ))}
        <button type="button" onClick={this.handleAddShareholder} className="small">Add Shareholder</button>
        <button>Incorporate</button>
      </form>
    )
  }
}

JS Bin on jsbin.com

The code is not perfect, and it doesn’t have to be. There are many ways to make it prettier, but this is not a post on refactoring. The code is not the point. Thinking about forms in terms of data is.


BTW. I blog about forms in React specifically, and other React-related things. If you like what you see here, subscribe below to make sure you don’t miss out on my next post.

Think your friends would dig this article, too?

Google+
Tumblr
Preview

Know how you tear your hair out when your designer asks to make a nice form?

But what if you could implement the form experience your users deserve?

The Missing Forms Handbook of React can help you breeze through your React forms. You will learn how forms fit into React and see how to implement common form patterns.

The excerpt contains a table of contents and two chapters: on building an intuition for forms and handling various form controls.

No spam. I will occasionally send you my posts on JavaScript and React.
Preview

Know how you tear your hair out when your designer asks to make a nice form?

But what if you could implement the form experience your users deserve?

The Missing Forms Handbook of React can help you breeze through your React forms. You will learn how forms fit into React and see how to implement common form patterns.

The excerpt contains a table of contents and two chapters: on building an intuition for forms and handling various form controls.