Advanced TypeScript Programming Projects
上QQ阅读APP看书,第一时间看更新

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.

Note that there is a caveat to that preceding statement. If we have two interfaces of exactly the same shape, then TypeScript treats them as equivalent to each other. So, even though TypeScript is expecting IState, if we supply something called IMyOtherState that has exactly the same properties, then TypeScript will happily let us use that in its place. The question, of course, is why would we want to duplicate the interface in the first place? I cannot think of many cases where we would want to do that, so the idea of using the right state is accurate for almost all the cases we are ever likely to encounter.

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
}
Something that may seem slightly confusing at first is a potential contradiction between my earlier statement the idea that the properties and state are different—with state being something that is local to the component and yet we are passing state down as part of the properties. I deliberately use state as part of the name to reinforce the fact that this represents the state. The values that we are passing in can be called anything at all. They do not have to represent any state; they could simply be functions that the component calls to trigger some response in the parent. Our component will receive this property and it will be its responsibility to convert any part that it needs into state.

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>
);
}
}
Date handling with JavaScript can be off-putting when we want to hook the date into a date picker component. The date picker expects to receive the date in the format of YYYY-MM-DD. So, we use the new Date().toISOString().substring(0,10) syntax to get today's date, which includes a time component, and only retrieve the YYYY-MM-DD portion from this. Even though the date picker expects the date to be in this format, it does not say that this is the format that will be displayed on the screen. The format on your screen should respect the local settings of the user.

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.

Changes to state will not be reflected back in the parent props. If we wanted to explicitly set a value back in the parent component, this would require us to trigger a change to props.DefaultState. I advise against doing this directly if you can possibly avoid it.

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.

As Address2 can be null, we are using the ! operator on our binding so that it looks slightly different:  value={this.state.Address2!} .