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.