When I first learned about spreading props in JSX, I was thrilled! It is just so convenient to pass props with <MyComponent {...this.props} />
, and override props defined after the spread, like <MyComponent {...this.props} text='override text prop' />
. I knew it made for a great developer experience, but I always wondered if it came with a cost. How does React handle it? Does it affect performance?
Well, it took me far too long, but I've finally answered my questions. You're welcome to play around with this code using Babel's online repl.
Let's start with our control.
const Comp = (props) => (
<div hi='bye' yes='no' />
)
// transpiles to:
var Comp = function Comp(props) {
return React.createElement('div', { hi: 'bye', yes: 'no' });
};
Cool, this shows how JSX gets converted to React.createElement()
and made into valid javascript. Also note that props are converted and passed as an inline object literal.
const someProps = {
one: 1,
two: 2,
}
const OnlySpread = (props) => (
<div {...someProps} />
)
Given the above, how do you expect OnlySpread
to be transpiled?
var someProps = {
one: 1,
two: 2
};
var OnlySpread = function OnlySpread(props) {
return React.createElement('div', someProps);
};
Oooh, nice! It just uses the already created object as its props. No cloning, just passing by reference.
const someProps = {
one: 1,
two: 2,
}
const SpreadAndExplicit = (props) => (
<div {...someProps} hi='bye' />
)
Given the above, how do you expect SpreadAndExplicit
to be transpiled?
var someProps = {
one: 1,
two: 2
};
var SpreadAndExplicit = function SpreadAndExplicit(props) {
return React.createElement('div', Object.assign({}, someProps, { hi: 'bye' }));
};
Bummer! No magic here. It just converts the explicit props to an object literal, then uses Object.assign()
to merge the two.
Object.assign()
is a micro-perf hit compared to creating an object literal, as this esbench shows. So, it is better to explicitly pass all props and not use a spread at all.
const AllExplicit = (props) => (
<div one={1} two={2} hi='bye' />
)
// transpiles to:
var AllExplicit = function AllExplicit(props) {
return React.createElement('div', { one: 1, two: 2, hi: 'bye' });
};
We're back to a single object literal. Micro-perf win!
To be pedantic, lets see what happens when we spread two objects.
const someProps = {
one: 1,
two: 2,
}
const TwoSpread = (props) => (
<div {...someProps} {...props} />
)
// transpiles to:
var TwoSpread = function TwoSpread(props) {
return React.createElement('div', Object.assign({}, someProps, props));
};
Again, no magic. Not surprised that it still uses Object.assign()
.
From our exploration, we could conclude that JSX spreads are good if and only if they are not accompanied with other props.
// GOOD
<div {...this.props} />
// BAD
<div {...this.props} two={2} />
// BAD
<div {...this.props} {...otherProps} />
But there is more to consider.
You see, spread is a deoptimization for two babel transforms used on production bundles: transform-react-inline-elements and transform-react-constant-elements. I want to say this can be fixed by ordering Babel's plugins properly, but this thread explains that an inline object literal (not an object reference) is required for the optimization. Even if transform-react-inline-elements
runs after <div {...someProps} />
gets converted to React.createElement('div', someProps)
, it will not inline it. Why? Because someProps
can contain a ref
, which this transform can not optimize for. Even if the referenced object does not currently contain a ref
, there is no way to guarantee that it won't have one in the future.
If you prioritize developer experience over performance, then go for it. Otherwise, avoid it where you can.
// BAD :(
<div {...this.props} />
Maybe Prepack will enable performant spreads in the future?
Note: I publish these to learn from your responses! Please let me know if you have any thoughts on the subject.