21 April, 20237 minute read

Stop using `defaultProps`!

I’ve been writing web apps with React for a really long time now1, and I’ve seen a number of its APIs come and go. One API that has long overstayed its welcome is defaultProps–the old school way of declaratively specifying default values for a component’s props. It’s ancient, covered in cobwebs, and no longer relevant within the context of today’s JavaScript ecosystem. At one point it was incredibly valuable to have, but now it’s time to give it up.

The React team are aware that this API is outstayed its welcome. Sebastian Markbåge put up an RFC in February 2019 which indicated defaultProps would eventually be deprecated, and Dan Abramov tweeted about it that same year in May. Between then and now there have been plenty of blog posts, interviews, and GitHub threads where this plan has been reiterated.

The React team have a strong commitment to maintaining backwards compatibility, which is why this API has survived for so long. At long last, however, there's movement: starting in React 18.3.0 you’ll get a warning if you try to use defaultProps and eventually the feature will be removed entirely. If you're still using this API in 2023, it's time to drop it.

If you're not convinced by that, then the rest of this article goes into the weeds of why this API was introduced in the first place, and why it's no longer relevant or recommended. The following section is a bit of a history lesson on web app development and explores the context behind the addition of defaultProps to the React API. If you aren't interested in that then you can freely skip to the following section which breaks down the reasons why this API shouldn't be used in modern codebases.

The history of defaultProps

React was first released in May 2013 which makes it utterly ancient by web dev standards. Back in 2013 JavaScript was a handicapped language with not much going for it. It lacked basic features other languages had had for years, the broader ecosystem was a mess, and there were numerous gotchas in the language that made web development a miserable experience. Had there been any other language available in the browser, no one would be writing JavaScript today. It won purely through being the only option for web developers.

Some work was being done to modernize the language, but it was happening extremely slowly. The ES6 specification was eventually finalized in 2015 and added a whole slew of new features like object destructuring, classes, and arrow functions, but for two entire years of React’s existence these features were nonexistent.

A tool called 6to5 (which eventually turned into Babel) launched in October 2014 and supported transpiling “new” JavaScript syntax into “old” JavaScript syntax, but it took time to see mass adoption because web developers were still pretty skeptical of transpilers at the time.

How crippled was JavaScript, really? Here’s how you default a function parameter now compared to back then, to get a taste for how much more difficult it was to get things done back in the day:

Click to copy
// New JavaScriptfunction sayHello(name = '<unknown>') {  console.log(`Hello, ${name}.`);} // --- // Old and terrible Javascriptfunction sayHello(name) {  name = typeof name === 'undefined' ? '<unknown>' : name;  console.log('Hello, ' + name + '.');}

One of those code samples is a heck of a lot better than the other. Multiply that nastiness across your entire codebase and it's easy to see why JavaScript had such a bad reputation. Something interesting to note here is that the nullish coalescing operator entered the standard after default function parameters! It was standardized in 2020 which means at the time you couldn’t shorten that typeof check down to name ?? '<unknown>'. That snippet of old JavaScript above was the state of the art—you simply couldn’t do any better than that.

The syntax was horrible, but in a free standing function such as sayHello it's possible to grin and bear it. Classes on the other hand? Downright horrible. React’s function components weren’t a thing until October 2015, which meant the only option you had was to write class components. In a class component you access props through a property on this which means that you’d need to duplicate your defaulting logic in every single method of the class.

Click to copy
// Note: Classes didn't exist yet.// React shipped with its own class system.var Greeter = React.createClass({  showAlert: function () {    alert(typeof this.props.name === 'undefined' ? '<unknown>' : this.props.name);  },   render: function () {    var name = typeof this.props.name === 'undefined' ? '<unknown>' : this.props.name;    return <div>Hello, {name}. <a onClick={this.showAlert}>Click here</a>.</div>;  }});

The example here is of course contrived—in this very simple case you could easily pass your defaulted name variable directly to showAlert–but the broader point is that there are many cases where you couldn’t do that and actually would have had to default your prop in multiple places across your class instance. This strategy for default props simply doesn’t scale up to a codebase containing hundreds or thousands of components.

Fortunately the React team recognized this wart, and came up with the idea of a getDefaultProps lifecycle method. This function would be executed by React, and allowed you to specify your default prop values in one centralized location:

Click to copy
var Greeter = React.createClass({  getDefaultProps: function () {    return {      name: '<unknown>'    };  },   showAlert: function () {    alert(this.props.name);  },   render: function () {    return <div>Hello, {this.props.name}. <a onClick={this.showAlert}>Click here</a>.</div>;  }});

Much better! This getDefaultProps method eventually evolved into the defaultProps property which we all (used to) know and love. At the time, getDefaultProps was added to React in order to work around the deficiencies of JavaScript in the same way the createClass method was. Since that time JavaScript has come a long way, and we now have facilities built right into the language to handle default prop values. There’s no longer any reason to be using the defaultProps API.

Why remove defaultProps from React?

The API might seem harmless, but there are plenty of good reasons for removing it from React. The simplest reason is taste: having your initial prop access collocated with the default values is more elegant and readable than having your default prop values hidden all the way at the bottom of your component’s file.

Another reason is that the React team likely doesn’t want to support functionality that duplicates native language features. The code implementing defaultProps is simple, but it's still code that needs to be maintained and tested for as long as it's included in the React codebase. Any new component type (such as lazy components) need to be designed in a way that supports defaultProps in order to not break community expectations.

On the consumer side there are three more practical reasons: defaultProps aren’t good for performance, they don’t play nicely with modern tooling, and they’re less functional than object destructuring in the first place.

defaultProps is bad for performance

Performance is easy to discuss: in order to support defaultProps in React, the createElement and jsx functions (these are what your JSX syntax gets transpiled into) need to include a property check for defaultProps. You can see the implementation here if you are curious. CPUs are fastest when they can run instructions in sequence, and if statements are anything but sequential—depending on what the condition inside the if evaluates to, the CPU will need to jump to a different set of instructions.

A single if statement will execute pretty quickly, React element creation is a hot path. Even a moderately sized React app is going to create thousands of elements every second, and so optimizations to that bit of code are disproportionately valuable. React doing its job quicker means that your app feels snappier in the hands of your customers, and the correlation between performance and revenue is well known. Removing the defaultProps check will make the web slightly faster for everyone, and that's a good thing.

defaultProps don’t work well with modern tools

This is a bit more complicated and necessitates code samples. Consider the following Greeter component—how would you type GreeterProps?

Click to copy
interface GreeterProps {  name: unknown; // ???} const Greeter = ({ name }: GreeterProps) => (  <div>Hello, {name}.</div>); Greeter.defaultProps = {  name: '<unknown>',};

There are two possible answers here: string and string | undefined. Both of these answers are wrong.

Click to copy
// Option 1: stringinterface GreeterProps {  name: string;} const Greeter = ({ name }: GreeterProps) => (  <div>Hello, {name}.</div>); Greeter.defaultProps = {  name: '<unknown>',}; <Greeter />// ^ Error: Property 'name' is missing in type '{}'//          but required in type 'GreeterProps'. // --- // Option 2: string | undefinedinterface GreeterProps {  name: string | undefined;} const Greeter = ({ name }: GreeterProps) => (  <div>Hello, {name}.</div>  //           ^ `name: string | undefined`, not string!  //             Even though we default it!);

With defaultProps it is impossible to type the name prop in a way that makes sense. The problem is that the Greeter component has two stakeholders: people who are using the component, and people who are developing the Greeter component itself. Both of these stakeholders have different expectations of how the name prop should be typed.

From the perspective of a consumer the best type is string | undefined. Advertising a type of string is too strict, and actually prevents us from making use of the default value in the first place. On the other hand, the component developer thinks the best type is string because the entire point of defaulting the prop is to get rid of the undefined value in the first place. For the developer a type of string | undefined is both inaccurate and too loose.

If you’re using object destructuring, then it’s actually possible to have your cake and it eat it too:

Click to copy
interface GreeterProps {  name: string | undefined;} const Greeter = ({ name = '<unknown>' }: GreeterProps) => (  <div>Hello, {name}.</div>  //           ^ name: string); <Greeter />             // works<Greeter name="John" /> // also works

defaultProps is less functional than destructuring

Imagine you have a component which takes a complicated object prop. Here’s a contrived example:

Click to copy
interface GreeterProps {  user: {    firstName: string | undefined;    lastName: string | undefined;  };} const Greeter = (props: GreeterProps) => {  const { firstName = '<unknown>', lastName = '<unknown>' } = props.user;  return <div>Hello, {firstName} {lastName}.</div>};

In this case our user prop can possibly contain firstName and lastName properties. If values are provided for those properties we want to use them, and any missing values will be defaulted to the string <unknown>. This is simple to do with JavaScript’s object destructuring syntax, and completely impossible to do with defaultProps.

If you inspect the implementation of defaultProps you’ll find that props only get defaulted by React at the topmost level of the props object. This means that defaulting your user prop is all or nothing—you can’t default individual fields of that object using defaultProps.

Stop using defaultProps

Back in the day defaultProps made a lot of sense, but the ecosystem has since grown up and the best way to default your component’s props is to use object destructuring.

If you use defaultProps today you’re losing functionality and reducing the quality of your code. Anything using defaultProps should be refactored on sight and if you’re still using the react/require-default-props ESLint rule then you should turn it off.

And on a longer time horizon, using defaultProps means that in future your code will just stop working. Components which expected to never receive an undefined value because of defaultProps will suddenly start receiving undefineds when you upgrade to the next major version of React and fixing this problem after the fact will be a nightmare because using defaultProps traded off type accuracy in the first place. The type system is—in general—a really powerful tool for making large refactors and dealing with library breakage such as the removal of defaultProps, but in this case you don’t get any help.

Get defaultProps out of your code and start destructuring instead—it’s simply the better way.

  1. Here's some of my street cred. Check out that use of defaultProps!

Don't want to miss out on new posts?

Join 100+ fellow engineers who subscribe for software insights, technical deep-dives, and valuable advice.

Get in touch 👋

If you're working on an innovative web or AI software product, then I'd love to hear about it. If we both see value in working together, we can move forward. And if not—we both had a nice chat and have a new connection.
Send me an email at hello@sophiabits.com