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.
defaultProps
The history of 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:
// 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.
// 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:
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.
defaultProps
from React?
Why remove 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
?
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.
// 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:
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:
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
.
defaultProps
Stop using 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 undefined
s 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.
- Here's some of my street cred. Check out that use of
defaultProps
!↩