Why shouldn't JSX props use arrow functions or bind?


I'm running lint with my React app, and I receive this error:

error    JSX props should not use arrow functions        react/jsx-no-bind

And this is where I'm running the arrow function (inside onClick):

{this.state.photos.map(tile => (
  <span key={tile.img}>
    <Checkbox
      defaultChecked={tile.checked}
      onCheck={() => this.selectPicture(tile)}
      style={{position: 'absolute', zIndex: 99, padding: 5, backgroundColor: 'rgba(255, 255, 255, 0.72)'}}
    />
    <GridTile
      title={tile.title}
      subtitle={<span>by <b>{tile.author}</b></span>}
      actionIcon={<IconButton onClick={() => this.handleDelete(tile)}><Delete color="white"/></IconButton>}
    >
      <img onClick={() => this.handleOpen(tile.img)} src={tile.img} style={{cursor: 'pointer'}}/>
    </GridTile>
  </span>
))}

Is this a bad practice that should be avoided? And what's the best way to do it?

Why you shouldn't use inline arrow functions in JSX props

Using arrow functions or binding in JSX is a bad practice that hurts performance, because the function is recreated on each render.

  1. Whenever a function is created, the previous function is garbage collected. Rerendering many elements might create jank in animations.

  2. Using an inline arrow function will cause PureComponents, and components that use shallowCompare in the shouldComponentUpdate method to rerender anyway. Since the arrow function prop is recreated each time, the shallow compare will identify it as a change to a prop, and the component will rerender.

As you can see in the following 2 examples - when we use inline arrow function, the <Button> component is rerendered each time (the console shows the 'render button' text).

Example 1 - PureComponent without inline handler

class Button extends React.PureComponent {
  render() {
    const { onClick } = this.props;
    
    console.log('render button');
    
    return (
      <button onClick={ onClick }>Click</button>
    );
  }
}

class Parent extends React.Component {
  state = {
    counter: 0
  }
  
  onClick = () => this.setState((prevState) => ({
    counter: prevState.counter + 1
  }));
  
  render() {
    const { counter } = this.state;
    
    return (
      <div>
        <Button onClick={ this.onClick } />
        <div>{ counter }</div>
      </div>
    );
  }
}

ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Example 2 - PureComponent with inline handler

class Button extends React.PureComponent {
  render() {
    const { onClick } = this.props;
    
    console.log('render button');
    
    return (
      <button onClick={ onClick }>Click</button>
    );
  }
}

class Parent extends React.Component {
  state = {
    counter: 0
  }
  
  render() {
    const { counter } = this.state;
    
    return (
      <div>
        <Button onClick={ () => this.setState((prevState) => ({
          counter: prevState.counter + 1
        })) } />
        <div>{ counter }</div>
      </div>
    );
  }
}

ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Binding methods to this without inlining arrow functions

  1. Binding the method manually in the constructor:

    class Button extends React.Component {
      constructor(props, context) {
        super(props, context);
    
        this.cb = this.cb.bind(this);
      }
    
      cb() {
    
      }
    
      render() {
        return (
          <button onClick={ this.cb }>Click</button>
        );
      }
    }
    
  2. Binding a method using the proposal-class-fields with an arrow function. As this is a stage 3 proposal, you'll need to add the Stage 3 preset or the Class properties transform to your babel configuration.

    class Button extends React.Component {
      cb = () => { // the class property is initialized with an arrow function that binds this to the class
    
      }
    
      render() {
        return (
          <button onClick={ this.cb }>Click</button>
        );
      }
    }
    

Function Components with inner callbacks

When we create an inner function (event handler for example) inside a function component, the function will be recreated every time the component is rendered. If the function is passed as props (or via context) to a child component (Button in this case), that child will re-render as well.

Example 1 - Function Component with an inner callback:

const { memo, useState } = React;

const Button = memo(({ onClick }) => console.log('render button') || (
  <button onClick={onClick}>Click</button>
));

const Parent = () => {
  const [counter, setCounter] = useState(0);
  
  const increment = () => setCounter(counter => counter + 1); // the function is recreated all the time
  
  return (
    <div>
      <Button onClick={increment} />
      
      <div>{counter}</div>
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="root"></div>

To solve this problem, we can wrap the callback with the useCallback() hook, and set the dependencies to an empty array.

Note: the useState generated function accepts an updater function, that provides the current state. In this way, we don't need to set the current state a dependency of useCallback.

Example 2 - Function Component with an inner callback wrapped with useCallback:

const { memo, useState, useCallback } = React;

const Button = memo(({ onClick }) => console.log('render button') || (
  <button onClick={onClick}>Click</button>
));

const Parent = () => {
  const [counter, setCounter] = useState(0);
  
  const increment = useCallback(() => setCounter(counter => counter + 1), []);
  
  return (
    <div>
      <Button onClick={increment} />
      
      <div>{counter}</div>
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="root"></div>


This is because an arrow function apparently will create a new instance of the function on each render if used in a JSX property. This might create a huge strain on the garbage collector and will also hinder the browser from optimizing any "hot paths" since functions will be thrown away instead of reused.

You can see the whole explanation and some more info at https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md


To avoid creating new functions with the same arguments, you could memoize the function bind result, here is a simple utility named memobind to do it: https://github.com/supnate/memobind


Using inline functions like this is perfectly fine. The linting rule is outdated.

This rule is from a time when arrow functions were not as common and people used .bind(this), which used to be slow. The performance issue has been fixed in Chrome 49.

Do pay attention that you do not pass inline functions as props to a child component.

Ryan Florence, the author of React Router, has written a great piece about this:

https://cdb.reacttraining.com/react-inline-functions-and-performance-bdff784f5578


You can use arrow functions using react-cached-handler library, no need to be worried about re-rendering performance :

Note : Internally it caches your arrow functions by the specified key, no need to be worried about re-rendering!

render() {

  return <div>
  {
        this.props.photos.map(photo=>
          <Photo key={photo.url}
            onClick={this.handler(photo.url, (url) => { 
                 console.log(url) })}
          />)
   }
 </div>

}

Other features:

  • Named handlers
  • Handle events by arrow functions
  • Access to the key, custom arguments and the original event
  • Component rendering performace
  • Custom context for handlers

Is the react/jsx-no-bind ESLint rule still relevant?

tldr: In most cases React (Fiber) is fast enough, so this rule is neglectable.

A more environment-specific approach: Is your current app performance sufficient? If the answer is "no", then measure before and after a refactoring (e.g. function memoization) - don't rely on premature optimization.

There is also no need to speculate about function creation cost and performance of closures.

Who still might profit?

Memoized components ?

Components wrapped by React.memo or PureComponent use reference equality checks on props to determine re-renders. A memoized function provides a stable object reference and can prevent a re-render. If this component is costly to render, performance might benefit (recap tldr).

DOM nodes ?

There is not much to reconcile for simple DOM nodes.

Non-memoized components ?

A non-memoized component re-renders every time, state or props change in one of its ancestors.

Solutions

1. Disable the rule. Don't bother, until performance suffers (subjective to app).

2. Adjust rule to ignore warnings for DOM nodes as kind of compromise:

rules: {
  "react/jsx-no-bind": [
    "error",
    {
      ignoreDOMComponents: true,
      ignoreRefs: true,
    },
  ],
},

const Comp = () => <span onClick={() => console.log("Hello!")} />; // no warning

3. Use performance optimizations like useCallback for Hooks. Useful FAQ entries: