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

82,620

Solution 1

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/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/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/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/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/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/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/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

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

Solution 2

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

Solution 3

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

Mostly, because inline functions can break memoization of optimized components:

Traditionally, performance concerns around inline functions in React have been related to how passing new callbacks on each render breaks shouldComponentUpdate optimizations in child components. (docs)

It is less about additional function creation cost:

Performance issues with Function.prototype.bind got fixed here and arrow functions are either a native thing or are transpiled by babel to plain functions; in both cases we can assume it’s not slow. (React Training)

I believe people claiming function creation is expensive have always been misinformed (React team never said this). (Tweet)

When is the react/jsx-no-bind rule useful?

You want to ensure, that memoized components work as intended:

  • React.memo (for function components)
  • PureComponent or custom shouldComponentUpdate (for class components)

By obeying to this rule, stable function object references are passed. So above components can optimize performance by preventing re-renders, when previous props have not changed.

How to solve the ESLint error?

Classes: Define the handler as method, or class property for this binding.
Hooks: Use useCallback.

Middleground

In many cases, inline functions are very convenient to use and absolutely fine in terms of performance requirements. Unfortunately, this rule cannot be limited to only memoized component types. If you still want to use it across-the-board, you could e.g. disable it for simple DOM nodes:

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

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

Solution 4

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

Solution 5

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

Share:
82,620
KadoBOT
Author by

KadoBOT

A strong conceptual thinker with fifteen years of web development experience using HTML/CSS, JavaScript, jQuery, WordPress, and implemented SPA (Single Page Applications) for the current browsers using React, and who has a keen interest in all things related to the Internet with the ability and experience needed to design and execute complex software projects for a diverse variety of users. Committed to having an impact on the future of any company that he works for, and possesses the enthusiasm and commitment to learn and develop his career within a fast paced and growing business. Has a track record of building fluid and dynamic interfaces that are user-friendly, and which positively impact on people’s lives. Right now he is looking for a suitable opportunity that will not only take his career to the next level, but will also allow him to work alongside a very specialised team.

Updated on July 08, 2022

Comments

  • KadoBOT
    KadoBOT almost 2 years

    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?

  • lux
    lux about 8 years
    How do you achieve this on stateless components?
  • Ori Drori
    Ori Drori about 8 years
    Stateless (function) components don't have this, so there's nothing to bind. Usually the methods are supplied by a wrapper smart component.
  • lux
    lux about 8 years
    Makes sense. I have some stateless components that dispatch actions to redux, but via arrow funcs in the component, not via props passed in. Seems like I could refactor to pass in the function instead and gain some perf. Thanks.
  • Ori Drori
    Ori Drori about 8 years
    Welcome. Use react-redux and mapDispatchToProps in the smart component created by connect to create the functions you pass to props.
  • lux
    lux about 8 years
    I do actually, but come to think of it, I never just provide the callback in the onClick, I always bind to an arrow function out of habit, thinking it would be invoked immediately otherwise. Thanks for the tip.
  • adam-beck
    adam-beck about 8 years
    @OriDrori: How does that work when you need to pass data in the callback? onClick={() => { onTodoClick(todo.id) }
  • Ori Drori
    Ori Drori about 8 years
    @adam-beck - add it inside the callback method definition in the class cb() { onTodoClick(this.props.todo.id); }.
  • adam-beck
    adam-beck about 8 years
    This is the lowest level I have and it's just a stateless component. Is this what you meant? gist.github.com/adam-beck/686bd0e4f495abcbbdcd0fab41f68eb6
  • Ori Drori
    Ori Drori about 8 years
    Use a class component, and use one of it's methods as the callback. The CB then will be able to use one of the class properties as id.
  • Fabian Zeindl
    Fabian Zeindl over 7 years
    In my test right now class instance arrow functions are not bound to this.
  • Ori Drori
    Ori Drori over 7 years
    Indeed they are not. You should bind them manually by using one of the methods in the answer.
  • philk
    philk over 7 years
    would constructor(props) { this.onTodoClick = () => this.props.onTodoClick(this.props.id) } do work also?
  • Ori Drori
    Ori Drori over 7 years
    @philk - Indeed. It's equivalent to declaring Class Instance Fields.
  • philk
    philk over 7 years
    @OriDrori but is it considered bad style, or why I never see that in tutorials and examples?
  • Ori Drori
    Ori Drori over 7 years
    No idea, but maybe class properties, and bind in the constructor preserve the class format, ie methods in the class body, while the method you presented moves everything to the constructor. However, I'm just guessing.
  • hippietrail
    hippietrail over 6 years
    Not only that. Creating the new function instances each time means the state is modified and when a component's state is modified it will be re-rendered. Since one of the main reasons to use React is to only render elements that change, using bind or arrow functions here is shooting yourself in the foot. It is not well documented though, especially in the case of working with mapping arrays within Lists, etc.
  • apieceofbart
    apieceofbart almost 6 years
    "Creating the new function instances each time means the state is modified" what do you mean by that? There's no state in at all in the question
  • Devashish
    Devashish over 5 years
    "Rerendering many elements might create jank in animations." Does this apply to animations done using JS only or also to CSS animations?
  • Ori Drori
    Ori Drori over 5 years
    Garbage collection might effect both types of animations as far as I know.
  • krankuba
    krankuba about 5 years
    Can you please show how to write a unit test on components with inline arrow functions? ˆˆ
  • Ori Drori
    Ori Drori almost 5 years
    @adam-beck - I've added a solution using hooks that addresses the passing data in the callback question.
  • sbaechler
    sbaechler almost 5 years
    @krankuba This is not what this question was about. You can still pass in anonymous functions that are not defined inline but are still not testable.
  • Shota Tamura
    Shota Tamura over 4 years
    @adam-beck I think this is how to use useCallback with dynamic value. stackoverflow.com/questions/55006061/…
  • kapil
    kapil about 4 years
    The question was why can't we use it. Not how to use it with some other hack.
  • Kiran
    Kiran over 3 years
    You can check list of items section here: github.com/yannickcr/eslint-plugin-react/blob/master/docs/ru‌​les/…. Gives a nice example of how to proceed when there is a need to pass data
  • Steve
    Steve over 2 years
    This is a major generalization, inline functions as props are not inherently bad as Ryan Florence illustrates in this post medium.com/@ryanflorence/…