Don’t use inline arrow functions as properties.

Mikhail Boutylin
Dec 28, 2017 · 5 min read

ES6 brought a really cool feature to javascript ecosystem, which is arrow functions.

const foo = (x) => x * x;

Arrow function is great, cause it can be easily used to create a closure, for example for react event handler. Let look at following simple app:

// ItemRenderer.jsimport React from 'react';

const ItemRenderer = ({ onClick, value }) => (
<div onClick={onClick}>
{value}
</div>
);

export default ItemRenderer;
// Group.jsimport React from 'react';
import ItemRenderer from './ItemRenderer';
const Group = ({ time, items, log }) => (
<div>
<h1>{time}</h1>
{items.map(
({ id, value }) => (
<ItemRenderer
key={id}
value={value}
onClick={() => { log(`value is: ${value}`); }}
/>
)
)}
</div>
);

export default Group;
// App.jsimport React from 'react';
import Group from './Group';

class App extends React.Component {
constructor() {
super();
this.state = {
time: new Date().toISOString(),
items: [
{ id: 'a', value: 'Some text' },
{ id: 'b', value: 'Some other text' },
{ id: 'c', value: 'Third text' },
],
};
}

componentDidMount() {
this.timeout = setInterval(
() => {
this.setState({
time: new Date().toISOString(),
});
},
100,
);
}

componentWillUnmount() {
clearInterval(this.timeout);
}

log = (v) => {
console.log(v);
};

render() {
return (
<Group
log={this.log}
{...this.state}
/>
);
}
}

export default App;

So that, app should be a component with time header, and list of items. The time has to be displayed once at second. To invoke log property passed from Root with correct parameters, we use closure as param:

onClick={() => { log(`value is: ${value}`); }}

The code has certainly one big performance problem. All items are rendered on each update Group. Let solve it, by using pure enhancer from recompose that does a shallow compare of properties before updating the component:

// ItemRenderer.jsimport React from 'react';
import { compose, pure } from 'recompose';

const enhancer = compose(
pure,
);

const ItemRenderer = ({ onClick, value }) => {
console.log('re-render!'); // just checking
return (
<div onClick={onClick}>
{value}
</div>
);
};

export default enhancer(ItemRenderer);

Now let ensure that that ItemRenderer is not re-rendered. And…

What was that??? Let see, what are the properties, that have actually changed. To do that let plug reprolog lib and change the code.

// ItemRenderer.jsimport React from 'react';
import { compose, pure } from 'recompose';
import { withLogger } from 'reprolog';

const enhancer = compose(
pure,
withLogger(),
);

const ItemRenderer = ({ onClick, value }) => (
<div onClick={onClick}>
{value}
</div>
);

export default enhancer(ItemRenderer);

(You can see full setup of reprolog in the readme).

And now we can see, that what is causing the update is onClick property. That happens cause on each render of Group, the closure is recreated for each item rendered. This closure does the same thing, and has identical shape. But it is a new closure instance. For that reason it will be the same as comparing:

const a = () => {};
const b = () => {};
a === b // false
a == b // false

For that reason passing inline closures as arrow functions is a very dangerous pattern (just imagine ItemRenderer is a heavier component and you have to display 20 or more of them on the screen). We could certainly write this in a following way:

// ItemRenderer.jsimport React from 'react';
import { compose, pure } from 'recompose';
import { withLogger } from 'reprolog';
const enhancer = compose(
pure,
withLogger(),
);
const ItemRenderer = ({ onClick, value }) => (
<div onClick={() => onClick(`value is: ${value}`)}>
{value}
</div>
);

export default enhancer(ItemRenderer);
// Group.jsimport React from 'react';
import ItemRenderer from './ItemRenderer';

const Group = ({ time, items, log }) => (
<div>
<h1>{time}</h1>
{items.map(
({ id, value }) => (
<ItemRenderer
key={id}
value={value}
onClick={log}
/>
)
)}
</div>
);

export default Group;

This will not re-render ItemRenderer on each update of Group (which solves current performance problem). But we didn’t get rid of the pattern itself, and brought more knowledge about callback behavior into ItemRenderer component.

One possible fix for that issue would be to wrap ItemRenderer with an enhancer which would add this knowledge without modifying it code. We can use withHandlers enhancer from recompose. This enhancer creates handlers lazily, when those are invoked, and passes properties :

// ItemRenderer.jsimport React from 'react';
import { compose, pure } from 'recompose';
import { withLogger } from 'reprolog';

const enhancer = compose(
pure,
withLogger(),
);

const ItemRenderer = ({ onClick, value }) => (
<div onClick={onClick}>
{value}
</div>
);

export default enhancer(ItemRenderer);
// Group.jsimport React from 'react';
import { compose, withHandlers } from 'recompose';
import ItemRenderer from './ItemRenderer';

const itemEnhancer = compose(withHandlers({
onClick: ({ clickHandler, value }) => () => {
clickHandler(`value is: ${value}`);
},
}));

const EnhancedItemRendrer = itemEnhancer(ItemRenderer);

const Group = ({ time, items, log }) => (
<div>
<h1>{time}</h1>
{items.map(({ id, value }) => (
<EnhancedItemRendrer
key={id}
value={value}
clickHandler={log}
/>
))}
</div>
);

export default Group;

Now let open debugger, and see, that there no item updates:

Be aware: The same issue happens for inline arrays and inline objects:

{} === {} // false;
[] === [] // false;

In next article we’ll talk about how to solve that problem.

Happy debugging :)

Frontend Weekly

Frontend Weekly

It's really hard to keep up with all the front-end development news out there. Let us help you. We hand-pick interesting articles related to front-end development. You can also subscribe to our weekly newsletter at http://frontendweekly.co

Mikhail Boutylin

Written by

React / typescript entusiast

Frontend Weekly

It's really hard to keep up with all the front-end development news out there. Let us help you. We hand-pick interesting articles related to front-end development. You can also subscribe to our weekly newsletter at http://frontendweekly.co

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store