Supplying state to bind against
The idea behind binding with React is that we have a state that we need to bind to. In the case of creating data that we want to display on the screen, our state can be as simple as an interface describing the properties that we want to use. For a single contact, this translates to our state looking like this:
export interface IPersonState {
FirstName: string,
LastName: string,
Address1: string,
Address2: StringOrNull,
Town: string,
County: string,
PhoneNumber: string;
Postcode: string,
DateOfBirth: StringOrNull,
PersonId : string
}
Note that we have created a union type called StringOrNull as a convenience. We will place this in a file called Types.tsx so that it looks like this:
export type StringOrNull = string | null;
What we want to do now is tell our component what state it is going to use. The first thing to do is update our class definition so that it looks like this:
export default class PersonalDetails extends React.Component<IProps, IPersonState>
This follows the convention where the properties are passed into our class from the parent and the state comes from our local component. This separation of properties and state is important to us because it provides us with a way for the parent to communicate with the component (and for the component to communicate back with the parent), while still being able to manage the data and behaviors that our component wants as the state.
Here, our properties are defined in an interface called IProps. Now that we have told React what the shape of our state is going to be internally, React and TypeScript use this to create a ReadOnly<IPersonState> property. Therefore, it is important to ensure that we are using the right state. If we use the wrong type for our state, TypeScript will inform us of this.
Our app.tsx file is going to create a default for the state and pass this to our component as its property. The default state is the one that will be applied when the user presses clear to clear the currently edited entry, or New Person to start adding a new person. Our IProps interface looks like this:
interface IProps {
DefaultState : IPersonState
}
With this in place, we are ready to change our App.tsx file to create our default state and to pass this into our PersonalDetails component. As we can see in the following code, the property from the IProps interface becomes a parameter in the <PersonalDetails .. line. The more items we add to our properties interface, the more parameters we will have to add to this line:
import * as React from 'react';
import Container from 'reactstrap/lib/Container';
import './App.css';
import PersonalDetails from './PersonalDetails';
import { IPersonState } from "./State";
export default class App extends React.Component {
private defaultPerson : IPersonState = {
Address1: "",
Address2: null,
County: "",
DateOfBirth : new Date().toISOString().substring(0,10),
FirstName: "",
LastName: "",
PersonId : "",
PhoneNumber: "",
Postcode: "",
Town: ""
}
public render() {
return (
<Container>
<PersonalDetails DefaultState={this.defaultPerson} />
</Container>
);
}
}
What was interesting about the changes we made to support passing in properties is that we have already seen binding in action here. Inside the render method, where we set Default={this.defaultPerson}, we are using binding. With the use of { } here, we are telling React that we want to bind to something, whether it's to a property or an event. We will encounter binding a lot in React.
Now we are going to add a constructor to PersonalDetails.tsx to support the property that is being passed in from App.tsx:
private defaultState: Readonly<IPersonState>;
constructor(props: IProps) {
super(props);
this.defaultState = props.DefaultState;
this.state = props.DefaultState;
}
We are doing two things here. First, we are setting up a default state to go back to if we need to, which we received from our parent; second, we are setting up the state for this page. We didn't have to create a state property in our code as this is provided for us by React.Component. This is the final part of learning how we have tied our property from the parent to the state.
Right. Let's set up our first name and last name elements to work with the binding from our state. The idea here is that if we update the state of the first or last names in our code, this will automatically be updated in our UI. So, let's change the entries as required:
<Row>
<Col><input type="text" id="firstName" className="form-control" value={this.state.FirstName} placeholder="First name" /></Col>
<Col><input type="text" id="lastName" className="form-control" value={this.state.LastName} placeholder="Last name" /></Col>
</Row>
Now, if we run our application, we have entries that are bound to the underlying state. There is, however, an issue with this code. If we try to type into either textbox, we will see that nothing happens. The actual text entry is rejected. That does not mean we have done anything wrong, rather we only have part of the overall picture here. What we need to understand is that React provides us with a read-only version of the state. If we want our UI to update our state, we have to explicitly opt into this by reacting to changes and then setting the state as appropriate. First, we are going to write an event handler to handle setting the state when the text changes:
private updateBinding = (event: any) => {
switch (event.target.id) {
case `firstName`:
this.setState({ FirstName: event.target.value });
break;
case `lastName`:
this.setState({ LastName: event.target.value });
break;
}
}
With this in place, we can now update our input to trigger this update using the onChange attribute. Again, we are going to use binding to match the onChange event to the code that is triggered as a result:
<Row>
<Col>
<input type="text" id="firstName" className="form-control" value={this.state.FirstName} onChange={this.updateBinding} placeholder="First name" />
</Col>
<Col><input type="text" id="lastName" className="form-control" value={this.state.LastName} onChange={this.updateBinding} placeholder="Last name" /></Col>
</Row>
From this code, we can clearly see that this.state provides us with access to the underlying state that we set up in our component and that we need to change it using this.setState. The syntax of this.setState should look familiar as it matches the key to the value, which we have encountered many times before in TypeScript. At this stage, we can now update the rest of our entry components to support this two-way binding. First, we expand our updateBinding code as follows:
private updateBinding = (event: any) => {
switch (event.target.id) {
case `firstName`:
this.setState({ FirstName: event.target.value });
break;
case `lastName`:
this.setState({ LastName: event.target.value });
break;
case `addr1`:
this.setState({ Address1: event.target.value });
break;
case `addr2`:
this.setState({ Address2: event.target.value });
break;
case `town`:
this.setState({ Town: event.target.value });
break;
case `county`:
this.setState({ County: event.target.value });
break;
case `postcode`:
this.setState({ Postcode: event.target.value });
break;
case `phoneNumber`:
this.setState({ PhoneNumber: event.target.value });
break;
case `dateOfBirth`:
this.setState({ DateOfBirth: event.target.value });
break;
}
}
We aren't going to code dump all of the changes that we need to make to our actual inputs. We just need to update each input to match the value to the appropriate state element, and then add the same onChange handler in each case.