Uncontrolled components

Let's start with a basic example—displaying a form with an input field and a Submit button.

The code is pretty straightforward:

  const Uncontrolled = () => ( 
<form>
<input type="text" />
<button>Submit</button>
</form>
);

If we run the preceding snippet in the browser, we can see exactly what we expect—an input field in which we can write something and a clickable button. This is an example of an uncontrolled component, where we do not set the value of the input field, but we let the component manage its own internal state.

Most likely, we want to do something with the value of the element when the Submit button is clicked. For example, we may want to send the data to an API endpoint.

We can do this easily by adding an onChange listener (we will talk more about event listeners later in this chapter). Let's look at what it means to add a listener.

First, we have to change the component from stateless to a class because we need to define some functions and a state:

  class Uncontrolled extends Component

The class has a constructor where we bind the event listener:

  constructor(props) { 
super(props);

this.handleChange = this.handleChange.bind(this);
}

Then, we define the event listener itself:

  handleChange({ target: { value } }) { 
console.log(value);
}

The event listener is receiving an event object, where the target represents the field that generated the event, and we are interested in its value. We start by just logging it because it is important to proceed with small steps, but we will store the value into the state soon.

Finally, we render the form:

  render() { 
return (
<form>
<input type="text" onChange={this.handleChange} />
<button>Submit</button>
</form>
);
}

If we render the component inside the browser and type the word React into the form field, we will see something like the following inside the console:

  R
Re
Rea
Reac
React

The handleChange listener is fired every time the value of the input changes. Therefore, our function is called once for each typed character. The next step is to store the value that's entered by the user and make it available when the user clicks the Submit button.

We just have to change the implementation of the handler to store it in the state instead of logging it, as follows:

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

Getting notified of when the form is submitted is very similar to listening to the change event of the input field; they are both events that are called by the browser when something happens.

So, let's say we add a second event handler inside the constructor, as follows:

  constructor(props) { 
super(props);

this.state = {
value: ''
};

this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}

We may also want to initialize the value property of the state as an empty string, in case the button gets clicked before any change event is triggered.

Let's define the handleSubmit function, where we just log the value. In a real-world scenario, you could send the data to an API endpoint or pass it to another component:

  handleSubmit(e) { 
e.preventDefault();

console.log(this.state.value);
}

This handler is pretty straightforward—we just log the value currently stored in the state. We also want to overcome the default behavior of the browser when the form is submitted, to perform a custom action.

This seems reasonable, and it works very well for a single field. The question now is, what if we have multiple fields? Suppose we have tens of different fields?

Let's start with a basic example, where we create each field and handler manually and look at how we can improve it by applying different levels of optimization.

Let's create a new form with first and last name fields. We can reuse the Uncontrolled class we just created and change the constructor, as follows:

  constructor(props) { 
super(props);

this.state = {
firstName: '',
lastName: ''
};

this.handleChangeFirstName = this.handleChangeFirstName.bind(this);
this.handleChangeLastName = this.handleChangeLastName.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}

We initialize the two fields inside the state and we define an event handler for each one of the fields as well. As you may have noticed, this does not scale very well when there are lots of fields, but it is important to understand the problem clearly before moving to a more flexible solution.

Now, we implement the new handlers:

  handleChangeFirstName({ target: { value } }) { 
this.setState({
firstName: value
})
}

handleChangeLastName({ target: { value } }) {
this.setState({
lastName: value
})
}

We also have to change the submit handler a little bit so that it displays the first and the last name when it gets clicked:

  handleSubmit(e) { 
e.preventDefault();

console.log(`${this.state.firstName} ${this.state.lastName}`);
}

Finally, we describe our elements, structure inside the render method:

  render() { 
return (
<form onSubmit={this.handleSubmit}>
<input type="text" onChange={this.handleChangeFirstName} />
<input type="text" onChange={this.handleChangeLastName} />
<button>Submit</button>
</form>
);
}

We are ready to go—if we run the preceding component in the browser, we will see two fields, and if we type Dan into the first one and Abramov into the second one, we will see the full name displayed in the browser console when the form is submitted.

Again, this works fine, and we can do some interesting things in this way, but it does not handle complex scenarios without requiring us to write a lot of boilerplate code.

Let's look at how we can optimize it a little bit. Our goal is to use a single change handler so that we can add an arbitrary number of fields without creating new listeners.

Let's go back to the constructor and define a single change handler:

  constructor(props) { 
super(props);

this.state = {
firstName: '',
lastName: ''
}

this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}

We may still want to initialize the values, and later in this section, we will look at how to provide prefilled values for the form.

Now, the interesting bit is the way in which we can modify the onChange handler implementation to make it work in different fields:

  handleChange({ target: { name, value } }) { 
this.setState({
[name]: value
});
}

As we have seen previously, the target property of the event we receive represents the input field that has fired the event, so we can use the name of the field and its value as variables.

We then have to set the name for each field, which we are going to do using the render method:

  render() { 
return (
<form onSubmit={this.handleSubmit}>
<input
type="text"
name="firstName"
onChange={this.handleChange}
/>
<input
type="text"
name="lastName"
onChange={this.handleChange}
/>
<button>Submit</button>
</form>
);
}

That's it; we can now add as many fields as we want without creating additional handlers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.200.180