You now have some experience working with props, state and refs. However, there are several major flaws with our current implementation:
- We are holding states in multiple places. This is hard to manage because we have to remember where each state is stored.
- We are duplicating the same states in multiple places. We are holding the valid state in both the RegistrationForm element as well as the Input elements. The RegistrationForm's valid state can be derived from the states of the Input elements.
To prevent both of these flaws, we should lift the state store to the closest common ancestor of the components that need it; for us, this will be the RegistrationForm component.
Here's how it would work. First, we turn the Input components back into stateless, dumb component, whose output depends solely on the props passed in. We are going to be passing down one new prop, name, which is a name that is used to identify the input. It is similar to the name attribute on a normal input HTML element. We will also change the signature of our RegistrationForm.handleInputChange() method to accept the name of the input as its first parameter.
function Input (props) {
return (
<label>
{props.label}
<input type={props.type} value={props.value} onChange={(event) => props.onChange(props.name, event)} />
<div className="indicator" style={{ ... }}></div>
</label>
)
}
Our Input components are no longer holding any state, nor carrying out any validation. Instead, these tasks have been delegated to the component's closest common ancestor, which is the RegistrationForm. So, inside RegistrationForm, we can:
- Remove any references to these Input components - because they no longer hold any state, we have no reasons to hold on to these references
- Update this.state to hold the values and validity information for the Input components.
class RegistrationForm extends React.Component {
constructor(props) {
super(props);
this.state = {
email: {
value: "",
valid: null
},
password: {
value: "",
valid: null
}
};
}
...
}
Next, update our JSX components to pass down states like value and valid to the Input components. We are also passing down a name prop that helps identify the element:
render() {
return (
<form onSubmit={this.handleRegistration}>
<Input label="Email" type="email" name="email" value={this.state.email.value} valid={this.state.email.valid} onChange={this.handleInputChange} />
<Input label="Password" type="password" name="password" value={this.state.password.value} valid={this.state.password.valid} onChange={this.handleInputChange} />
<Button title="Register" disabled={!(this.state.email.valid && this.state.password.valid)} />
</form>
)
}
Next, we will completely rewrite our handleInputChange method of RegistrationForm to validate the input and store both the value and its validity into the state. It will use the name and event parameters passed by the onChange event handler of Input.
handleInputChange = (name, event) => {
const value = event.target.value;
const valid = validator[name](value);
this.setState({
[name]: { value, valid }
});
}
Lastly, we no longer need to use refs to get the values of the Input components and validate them, since they are already in the state. So, remove those lines from our handleRegistration method:
handleRegistration = (event) => {
...
const hasValidParams = this.state.email.valid && this.state.password.valid;
if (!hasValidParams) { ... }
const email = this.state.email.value;
const password = this.state.password.value;
...
}
Now, refresh the page and everything should work as it did before.
In this section, we have lifted the state of our components and consolidated it into a single place. This makes our state easier to manage. However, the way we are changing the state is by passing down onChange props. Whilst this is fine for simple components like this, it gets much less performant once the components are heavily nested. A single change may invoke tens of functions and this is not sustainable. Therefore, as we continue to develop our application, we will use a state management tool, such as Redux or MobX.