React PropTypes best practices

React PropTypes best practices

React components are great at allowing us to express all of the things a part of our application may need to care about. This includes the external information our component may need to function as a consumer of our component may need. Here are a number of considerations I have found helpful when writing out and managing prop-types.

Let the implementation fail

This concept speaks to why prop-types provides a warning, to begin with. Often times as authors of an API we want to provide good recovery when our client fails to provide us with the data we need most. If we do too much to recover we lose an opportunity to provide education to our consumer as to what we expect them to do in order to use our API appropriately.

For instance,

class Title extends React.Component {
  static propTypes = { title: PropTypes.string };

  render() {
    if (!this.props.title) { return null; }

    return (
      <h1>{title}</h1>
    );
  }
}

// or written as a pure function

const Title = ({ title }) => (
  {title && <h1>{title}</h1>}
);

Title.propTypes = { title: PropTypes.string }

In the previous examples, the Title component is "guarding" against whether or not a title is provided, and making the determination as to whether or not to return some null artefact as the render method return type. Though seemingly harmless, we are missing an opportunity to do a few things here.

Reduce work by preventing any of the construction or invocation to occur.

The pattern described earlier says that it is OKAY to run or construct our Title component in an incorrect fashion since Title simply returns null or undefined. As an extension of a component that means that all of the lifecycle methods will be ran and be considered for reconciliation by React during future cycles.

As a function, this means a lot of the same in regards to reconciliation, always returning some artefact when we probably should not.

Consider the following refactor
class Title extends React.Component {
  static propTypes = { title: PropTypes.string.isRequired };

  render() {
    return (
      <h1>{title}</h1>
    );
  }
}

// or written as a pure function

const Title = ({ title }) => (
  <h1>{title}</h1>
);

Title.propTypes = { title: PropTypes.string.isRequired }

To highlight the key differences, we applied the isRequired property to our title field. React will examine this and warn our consumers that our Title component did not receive the props it requires to behave correctly. In this case, we expect our Title component to return to us just the title tagged as an H1.

The warning should hint to the implementer that they should not draw the Title component when there is no title to be drawn. This is typically handled in a simple evaluation of the component containing our <Title />.

class ContainerComponent extends React.Component {
  static propTypes = { title: PropTypes.string };
  static defaultProps = { title: '' };

  render (
    <div>
      {
        this.props.title &&
          <Title title={this.props.title} />
      }
    </div>
  );
}

What is accomplished is that the presentation component needs to care less about how to deal with its implementation and more about what it is designed to do. Furthermore, by relying on prop-types to provide feedback to the implementer we are encouraging improved code management through the setting of defaults by the consumer while reducing work the application is doing by instantiating components at the wrong time.

Managing redundant prop-types

PropTypes are great, but a pain to enforce through container components. Let's look again at the Title example:

// Title.js
const Title = ({ title }) => (
  <h1>{title}</h1>
);

Title.propTypes = { title: PropTypes.string.isRequired }
export Title;

// App.js
class App extends React.Component {
  static propTypes = { title: PropTypes.string };
  static defaultProps = { title: '' };

  render() {
    return (
      <div>
        {
          this.props.title &&
            <Title title={this.props.title} />
        }
      </div>
    );
  }
}

If you have been writing a lot of React code, this may look familiar. The problem we are looking at here is how many times we are defining the same prop-type validation from child components in containing components. Often times this leads us to write a lot of duplicate code, and adds, even more, work when it comes to removing components from containers.

There are a couple of ways this can be managed:

Export/import propTypes

We are using modules after all and some folks are already doing this:

// Title.js
export const propTypes = { title: PropTypes.string.isRequired };

const Title = ({ title }) => (
  <h1>{title}</h1>
);

Title.propTypes = propTypes;
export Title;

// App.js
import { Title, propTypes as titlePropTypes } from './Title';

class App extends React.Component {
  static propTypes = Object.assign(titlePropTypes, {});
  static defaultProps = { title: '' };

  render() {
    return (
      <div>
        {
          this.props.title &&
            <Title title={this.props.title} />
        }
      </div>
    );
  }
}

This is a great start but it requires that every composite component manages their prop-types in the same fashion. But if we take a closer look at how prop-types are defined and applied to a React component we should recognize that whenever we import a component, to begin with, we get it's associated prop-types for free. prop-types are considered static properties of the React component, meaning you are not required to instantiate the component to understand what it's prop-types are.

Compositing prop-types from multiple components
const propTypes = { title: PropTypes.string.isRequired };

const Title = ({ title }) => (
  <h1>{title}</h1>
);

Title.propTypes = propTypes;

export Title;

// App.js
import { Title } from './Title';

class App extends React.Component {
  static propTypes = Object.assign(Title.propTypes, {});
  static defaultProps = { title: '' };

  render() {
    return (
      <div>
        {
          this.props.title &&
            <Title title={this.props.title} />
        }
      </div>
    );
  }
}

Here we made a small adjustment to how we inherit prop-types from a child or presentational component to force our container component.

cons?

When we think about prop-type management one thing that is nice for us to know is "What prop-types does my container need to care about?". Without writing out each prop-type in the container we lose the legibility of the composite of fields right there in our code. You could say this method reduces optics and requires that the implementer digs into the component code to understand what props the container needs to be concerned about providing.

Duplicate validation could also be a complaint. This practice leads to fewer default props in presentational components and more default props in our containers making for more rigid API's in presentational components making them simpler to understand how to implement appropriately. Fewer default props defined in presentational components also leads to simpler debugging of default props. Default props may be troublesome while implementing some API and we are not sure where or what is setting a property.

Should containers even care?

The final observation I will share here is, should a container even care to validate it's child API's? For this case, I could really go either way, but if a container component cares about some outside content to be provided, typically from an API, then yes I would vote in favour of including an API contract defined by prop-types to be defined.

However, in the case where our container is the one providing the content for a presentational component it makes no sense to also include child prop-type validation.

Clearly there are a few options here to help us with the management of prop-types in a DRY and responsible manner. I would encourage you to explore, try these patterns on for size.

Subscribe to Leadership Redefined: Master Adaptation & Conscious Strategies

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe