Skip to content

Instantly share code, notes, and snippets.

@nemoDreamer
Last active November 4, 2023 18:10
Show Gist options
  • Save nemoDreamer/21412b28dc65d51e2c5c8561a8f82ce1 to your computer and use it in GitHub Desktop.
Save nemoDreamer/21412b28dc65d51e2c5c8561a8f82ce1 to your computer and use it in GitHub Desktop.
`React.cloneElement` vs. `Render Prop` pattern benchmarking

React.cloneElement vs. Render Prop pattern benchmarking

Run:

babel clone-element-vs-render-prop.jsx | node

Result:

  • React.cloneElement x 41,818 ops/sec ±0.76% (1079 runs sampled)
  • Render Prop x 47,911 ops/sec ±0.59% (1079 runs sampled)

Fastest is Render Prop

Conclusion:

  • The margin of performance improvement is negligible for "Render Prop". You have to manually spread the props, and for the nested pattern to work, you also have to pass the onus of nesting children to the Container...
  • The "React.cloneElement" solution is more legible, and keeps the nested structure requirement inside the Presentation where they belong.

I propose sticking with "React.cloneElement", unless I've missed something and the "nesting" problem for "Render Prop" can be solved more elegantly?

Correction:

Update: see correction coment below.

import React from 'react';
import PropTypes from 'prop-types';
import TestRenderer from 'react-test-renderer';
const Benchmark = require('benchmark');
Benchmark.options.minSamples = 1000;
const suite = new Benchmark.Suite();
// --------------------------------------------------
// PROPS
// --------------------------------------------------
const outerProps = {
style: {
color: 'black',
},
id: 'outer',
};
const innerProps = {
id: 'inner',
};
// --------------------------------------------------
// PRESENTATIONS
// --------------------------------------------------
const PresentationCloneElement = ({ outerElement, innerElement }) => (
<div>
{React.cloneElement(
outerElement,
outerProps,
React.cloneElement(innerElement, innerProps),
)}
</div>
);
PresentationCloneElement.propTypes = {
outerElement: PropTypes.node.isRequired,
innerElement: PropTypes.node.isRequired,
};
const PresentationRenderProp = ({ outerRender, innerRender }) => (
<div>
{outerRender({
...outerProps,
children: innerRender(innerProps),
})}
</div>
);
PresentationRenderProp.propTypes = {
outerRender: PropTypes.func.isRequired,
innerRender: PropTypes.func.isRequired,
};
// --------------------------------------------------
// CONTAINERS
// --------------------------------------------------
const ContainerCloneElement = ({ isDisabled, label }) => (
<PresentationCloneElement
outerElement={<button disabled={isDisabled} />}
innerElement={<span>{label}</span>}
/>
);
ContainerCloneElement.propTypes = {
isDisabled: PropTypes.bool.isRequired,
label: PropTypes.string.isRequired,
};
const ContainerRenderProp = ({ isDisabled, label }) => (
<PresentationRenderProp
outerRender={({ children, ...props }) => (
<button disabled={isDisabled} {...props}>
{children}
</button>
)}
innerRender={props => <span {...props}>{label}</span>}
/>
);
ContainerRenderProp.propTypes = {
isDisabled: PropTypes.bool.isRequired,
label: PropTypes.string.isRequired,
};
// --------------------------------------------------
// TESTS
// --------------------------------------------------
const testCloneElement = () =>
TestRenderer.create(<ContainerCloneElement isDisabled label="Yoyo" />);
const testRenderProp = () =>
TestRenderer.create(<ContainerRenderProp isDisabled label="Yoyo" />);
// these should have identical output:
console.log(testCloneElement().toJSON());
console.log(testRenderProp().toJSON());
// --------------------------------------------------
// SUITE
// --------------------------------------------------
suite
.add('React.cloneElement', () => {
testCloneElement();
})
.add('Render Prop', () => {
testRenderProp();
})
// add listeners
.on('cycle', event => {
console.log(String(event.target));
})
.on(
'complete',
// eslint-disable-next-line func-names
function() {
console.log(`Fastest is ${this.filter('fastest').map('name')}`);
},
)
// run async
.run({ async: true });
@nemoDreamer
Copy link
Author

Correction:

The "Render Prop"'s "nesting" problem can actually be avoided:

const PresentationRenderProp = ({ outerRender, innerRender }) => (
  <div id="outerWrapper">
    {outerRender({
      ...outerProps,
      children: <div id="innerWrapper">{innerRender(innerProps)}</div>,
    })}
  </div>
);

const ContainerRenderProp = ({ isDisabled, label }) => (
  <PresentationRenderProp
    outerRender={props => <button disabled={isDisabled} {...props} />}
    innerRender={props => <span {...props}>{label}</span>}
  />
);

But needing to manually spread props is still an issue, but it does offer the flexibility of being able to render a deeper structure, and pepper the props at different levels.

So all-in-all, with that strong benefit, and the new legibility of "Render Prop", I'd propose going with that pattern...!

@PatrickChevalierADSK
Copy link

Good stuff! And also it's the fastest! 🎆

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment