Typechecking With PropTypes

Linked Projects:

Calreact
 
Lesson code - https://github.com/learnetto/calreact/tree/m6l8-proptypes

In this lesson, we’re going to look at how to add typechecking with propTypes. PropTypes are a way to specify what the type of props in React components should be.

The React documentation gives a good example of propTypes. Here we have a Greeting component which takes a prop called name.

import PropTypes from 'prop-types';

class Greeting extends React.Component {  
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}

And here the propType of name is specified as a string:

Greeting.propTypes = {
  name: PropTypes.string
};

This means any time someone uses this Greeting component, they need to pass a string as the value of name. If it’s not a string, then React will throw a warning. 

Typechecking with propTypes can help you catch a lot of bugs before you ship code to production. But it's not a substitute for user-facing validation. When a propType fails, the warning is for the developer. 

PropTypes also make your code easier to understand when sharing with others. Other developers don't have to guess what value any prop can or cannot accept.

In React, you specify propTypes by using the propTypes property on the component.

MyComponent.propTypes = {
  optionalString: React.PropTypes.string
}

React comes with a number of basic propTypes, like array, boolean, function, number, object, or string. You can find a full list in the React documentation at https://facebook.github.io/react/docs/typechecking-with-proptypes.html.

One thing to note is that some of the propType values are spelt as truncated strings, so for example, bool instead of boolean and func instead of function.

You can define a range of propTypes with oneOfType.

optionalUnion: PropTypes.oneOfType([
  PropTypes.string,
  PropTypes.number,
  PropTypes.instanceOf(Message)
]),

You can also define your own custom propTypes. For example, we could even define our minimum title length requirement as a custom propType function, like below:

customProp: function(props, propName, componentName) {
  if (!/matchme/.test(props[propName])) {
    return new Error(
      'Invalid prop `' + propName + '` supplied to' +
      ' `' + componentName + '`. Validation failed.'
    );
  }
},

You can specify that a prop is required by adding isRequired to the propType. This will produce a warning if you don't pass a value for that prop.

requiredFunc: React.PropTypes.func.isRequired,

Let's see how we can use propTypes in our app.

In our Appointments component, let’s change the initial state value of formErrors from an object to an empty string.

this.state = {
  appointments: this.props.appointments,
  title: {value: '', valid: false},
  appt_time: {value: '', valid: false},
  formErrors: {},
  formValid: false
}

this.state = {
  appointments: this.props.appointments,
  title: {value: '', valid: false},
  appt_time: {value: '', valid: false},
  formErrors: '',
  formValid: false
}

Then we pass that down to the formErrors component. Open that component and specify a propType for that.

FormErrors.propTypes = {
  formErrors: React.PropTypes.object
}

If we refresh now, React will throw a failed propType warning.

[screenshot: 2:40-41]

Warning: Failed prop type: Invalid prop `formErrors` of type `string` supplied to `FormErrors`, expected `object`.

If we fix that by changing this.state.formErrors back to an object, the warning disappears.

this.state = {
  appointments: this.props.appointments,
  title: {value: '', valid: false},
  appt_time: {value: '', valid: false},
  formErrors: {},
  formValid: false
}

We can refactor this by importing PropTypes as a named import:

import React, { PropTypes } from 'react';

So we can remove React from the FormErrors component and make it required by adding isRequired.

FormErrors.propTypes = {
  formErrors: PropTypes.object.isRequired
}

Let’s look at our other components and see what propTypes we can specify.

For our Appointments component, we pass an appointments prop in the index view file.

= react_component('Appointments', props: {appointments: @appointments})

That’s the only prop it has, so define a propType for that.

First, remember to import PropTypes in the Appointments component.

import React, { PropTypes } from 'react';

In the FormErrors component, we defined the propTypes at the bottom of the file because it’s a stateless functional component. But in a class, we can also define propTypes at the top of the class as a static property.

I prefer defining them at the top of the class because then the propTypes are available right at the top of the component code, so it makes the code easier to read.

export default class Appointments extends React.Component {
  static propTypes = {
    appointments: PropTypes.array.isRequired
  }

Now if you refresh the app, you shouldn't get any errors or warnings.

Next, let’s look at the AppointmentForm component. We're sending five props from the Appointments component to the AppointmentForm component - title, appt_time, formValid, onUserInput, and onFormSubmit. 

<AppointmentForm title={this.state.title}
  appt_time={this.state.appt_time}
  form_valid = {this.state.formValid}
  onUserInput={this.handleUserInput}
  onFormSubmit={this.handleFormSubmit} />

Let's define propTypes for all of these.

Once again, we’ll import PropTypes into AppointmentForm first.

import React, { PropTypes } from 'react';

At the time of publishing this lesson, propTypes have been moved out of React into a separate library. So if you’re using the latest version of React, you may get warnings about using propTypes from that library instead.

export default class AppointmentForm extends React.Component {

  static propTypes = {
    title: PropTypes.object.isRequired,
    appt_time: PropTypes.object.isRequired,
    formValid: PropTypes.bool.isRequired,
    onUserInput: PropTypes.func.isRequired,
    onFormSubmit: PropTypes.func.isRequired
  }

Remember, when we refactored our form validations code, we changed the state values for title and appt_time to an object with two keys. Here we are only specifying that the prop value is an object, but we are not checking if the keys and values inside the object are valid.

We can also specify that by using the shape type. Inside shape, we can specify the propTypes for the values inside the object.

static PropTypes = {
  title: PropTypes.shape({
    value: PropTypes.string.isRequired,
    valid: PropTypes.bool.isRequired
  }).isRequired,
  appt_time: PropTypes.object.isRequired,
  formValid: PropTypes.object.isRequired,
  onUserInput: PropTypes.func.isRequired,
  onFormSubmit: PropTypes.func.isRequired
}

Let’s do the same for appt_time.

static PropTypes = {
  title: PropTypes.shape({
    value: PropTypes.string.isRequired,
    valid: PropTypes.bool.isRequired
  }).isRequired,
  appt_time: PropTypes.shape({
    value: PropTypes.string.isRequired,
    valid: PropTypes.bool.isRequired
  }).isRequired,
  formValid: PropTypes.object.isRequired,
  onUserInput: PropTypes.func.isRequired,
  onFormSubmit: PropTypes.func.isRequired
}

Of course, the value for appt_time should really be a date but we are actually passing a string and wrapping it in the moment method call before using it. We’ll change this in a minute.

First, test if our typechecking is working by passing in a string to this.state.title.valid instead of a boolean.

constructor (prop, railsContext) {
  super(props)
  this.state = {
    appointments: this.props.appoinments,
    title: {value: '', valid: ''},
    appt_time: {value: '', valid: false},
    formErrors: {},
    formValid: false
  }
}

Then, when we refresh, we should get a failed propType warning.

[screenshot: 6:02-03] 

Let’s try another one. Instead of passing a function to onUserSubmit, submit a number.

<AppointmentForm title={this.state.title}
  appt_time={this.state.appt_time}
  formValid = {this.state.formValid}
  onUserInput={1}
  onFormSubmit={this.handleFormSubmit} />
<AppointmentsList appointments={this.state.appointments} />

When we do this, we get an invalid prop error. (Don't forget to change onUserInput back to a function.)

[screenshot 6:15-16]

Now let’s add propTypes to AppointmentsList.

import React, { PropTypes } from 'react';

AppointmentsList.propTypes = {
  appointments: PropTypes.array.isRequired
}

And finally, we have the Appointment component with one prop, appointment of type object.

Appointment.propTypes = {
  appointment: PropTypes.object.isRequired
}

Don't forget to import PropTypes at the top of this file too.

Now refresh and verify that everything looks fine.

Then try to make an appointment in our app. When we choose a date, we get a failed propType warning because the component is expecting a string, but we're now passing it a date.

To fix that, change the appt_time propType in AppointmentForm.

appt_time: PropTypes.shape({
  value: PropTypes.instanceOf(Date).isRequired,
  valid: PropTypes.bool.isRequired
}).isRequired,

This allows us to choose a date in our app but gives us another warning if we refresh the page because a string is being supplied to AppointmentForm when it's expecting a date. 

[screenshot 7:39]

To fix this, we can change the initial value to a date.

constructor (prop, railsContext) {
  super(props)
  this.state = {
    appointments: this.props.appoinments,
    title: {value: '', valid: ''},
    appt_time: {value: new Date(), valid: false},
    formErrors: {},
    formValid: false
  }
}

That’s propTypes.

Remember, typechecking with propTypes is a form of validation for the developer, it's not a replacement for user-facing validations.

Liked this tutorial? Get more like this in your inbox