How to avoid bind or inline arrow functions inside render method
Asked Answered
C

4

37

We should avoid method binding inside render because during re-rendering it will create the new methods instead of using the old one, that will affect the performance.

So for the scenarios like this:

<input onChange = { this._handleChange.bind(this) } ...../>

We can bind _handleChange method either in constructor:

this._handleChange = this._handleChange.bind(this);

Or we can use property initializer syntax:

_handleChange = () => {....}

Now lets consider the case where we want to pass some extra parameter, lets say in a simple todo app, onclick of item i need to delete the item from array, for that i need to pass either the item index or the todo name in each onClick method:

todos.map(el => <div key={el} onClick={this._deleteTodo.bind(this, el)}> {el} </div>)

For now just assume that todo names are unique.

As per DOC:

The problem with this syntax is that a different callback is created each time the component renders.

Question:

How to avoid this way of binding inside render method or what are the alternatives of this?

Kindly provide any reference or example, thanks.

Commingle answered 12/7, 2017 at 9:22 Comment(0)
U
38

First: A simple solution will be to create a component for the content inside a map function and pass the values as props and when you call the function from the child component you can pass the value to the function passed down as props.

Parent

deleteTodo = (val) => {
    console.log(val)
}
todos.map(el => 
    <MyComponent val={el} onClick={this.deleteTodo}/> 

)

MyComponent

class MyComponent extends React.Component {
    deleteTodo = () => {
        this.props.onClick(this.props.val);
    }
    render() {
       return <div  onClick={this.deleteTodo}> {this.props.val} </div>
    }
}

Sample snippet

class Parent extends React.Component {
     _deleteTodo = (val) => {
        console.log(val)
    }
    render() {
        var todos = ['a', 'b', 'c'];
        return (
           <div>{todos.map(el => 
             <MyComponent key={el} val={el} onClick={this._deleteTodo}/> 
        
           )}</div>
        )
    }
    
   
}

class MyComponent extends React.Component {
        _deleteTodo = () => {
                     console.log('here');   this.props.onClick(this.props.val);
        }
        render() {
           return <div onClick={this._deleteTodo}> {this.props.val} </div>
        }
    }
    
ReactDOM.render(<Parent/>, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>

EDIT:

Second: The other approach to it would be to use memoize and return a function

constructor() {
    super();
    this._deleteTodoListener = _.memoize(
                   this._deleteTodo, (element) => {
                        return element.hashCode();
                    }
              )
}

_deleteTodo = (element) => {
   //delete handling here
}

and using it like

todos.map(el => <div key={el} onClick={this._deleteTodoListener(el)}> {el} </div>)

P.S. However this is not a best solution and will still result in multiple functions being created but is still an improvement over the initial case.

Third: However a more appropriate solution to this will be to add an attribute to the topmost div and get the value from event like

_deleteTodo = (e) => {
     console.log(e.currentTarget.getAttribute('data-value'));

 }

 todos.map(el => <div key={el} data-value={el} onClick={this._deleteTodo}> {el} </div>)

However, in this case the attributes are converted to string using toString method and hence and object will be converted to [Object Object] and and array like ["1" , "2", "3"] as "1, 2, 3"

Underpass answered 12/7, 2017 at 9:28 Comment(8)
yes we can do that, but with this we need to create separate components and put the parent child relation, i think this will be not that scalable since in large application we used to do this kind of binding at multiple places.Commingle
I've struggled with this as well, and my conclusion is that if this recreation of functions is slowing your application down (which... I guess... could happen if you have a large enough data set that re-renders a lot), you should follow this approach for those components. Otherwise, it's not really an issue for perf, and therefore can be ignored safely.Chessboard
Yes but that how you can avoid what you want to and scalabilty shouldn't be a problem hereUnderpass
In your first two code blocks when you create the class property, you use deleteTodo, but when you make reference to it you use _deleteTodo. Is this a mistake or does the underscore have some special purpose here?Ation
@Ation Thanks for pointing that out, it was a typo caused due to copying and code from the question and adding my ownUnderpass
Isn't the curried function approach just as bad for performance as using an arrow function? you will create another function on each render.Petes
Hmm, this works if there is a parent along with a child component but my question is what if there is no child meaning the map function would look something like..... itemList.map( (item,index) => <span onclick={"how do I pass index to an onClick function here"} /> )Hardiness
@akshaykishore, you can use the Third approach in such a case instead of passing the index to onClickUnderpass
D
5

How to avoid this way of binding inside render method or what are the alternatives of this?

If you care about re-rendering then shouldComponentUpdate and PureComponent are your friends and they will help you optimize rendering.

You have to extract "Child" component from the "Parent" and pass always the same props and implement shouldComponentUpdate or use PureComponent. What we want is a case when we remove a child, other children shouldn't be re-rendered.

Example

import React, { Component, PureComponent } from 'react';
import { render } from 'react-dom';

class Product extends PureComponent {
  render() {
    const { id, name, onDelete } = this.props;

    console.log(`<Product id=${id} /> render()`);
    return (
      <li>
        {id} - {name}
        <button onClick={() => onDelete(id)}>Delete</button>
      </li>
    );
  }
}

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      products: [
        { id: 1, name: 'Foo' },
        { id: 2, name: 'Bar' },
      ],
    };

    this.handleDelete = this.handleDelete.bind(this);
  }

  handleDelete(productId) {
    this.setState(prevState => ({
      products: prevState.products.filter(product => product.id !== productId),
    }));
  }

  render() {
    console.log(`<App /> render()`);
    return (
      <div>
        <h1>Products</h1>
        <ul>
          {
            this.state.products.map(product => (
              <Product 
                key={product.id}
                onDelete={this.handleDelete}
                {...product}
              />
            ))
          }
        </ul>
      </div>
    ); 
  }
}

render(<App />, document.getElementById('root'));

Demo: https://codesandbox.io/s/99nZGlyZ

Expected behaviour

  • <App /> render()
  • <Product id=1... render()
  • <Product id=2... render()

When we remove <Product id=2 ... only <App /> is re-rendered.

  • render()

To see those messages in demo, open the dev tools console.

The same technique is used and described in article: React is Slow, React is Fast: Optimizing React Apps in Practice by François Zaninotto.

Danuloff answered 12/7, 2017 at 9:52 Comment(2)
thanks for the suggestion, but i think using unique key will solve this one What we want is a case when we remove a child, other children shouldn't be re-rendered since just i want to render a single div with text. this approach will play a big role when component is large and we want to avoid re-rendering of it.Commingle
Using key property doesn't solve this issue, look at: codesandbox.io/s/xVZ7pL6E even if you use key property, render() of other <Product /> are also called. The only change between demo and that link is Product extends Component instead of PureComponent.Danuloff
C
3

This answer https://mcmap.net/q/226327/-how-to-avoid-bind-or-inline-arrow-functions-inside-render-method is definitely exhaustive, but I'd say fighting excessive re-renders instead of just re-creating the tiny callback would bring you more performance improvements. That's normally achieved by implementing a proper shouldComponentUpdate in the child component.

Even if the props are exactly the same, the following code will still re-render children unless they prevent it in their own shouldComponentUpdate (they might inherit it from PureComponent):

handleChildClick = itemId => {}

render() {
    return this.props.array.map(itemData => <Child onClick={this.handleChildClick} data={itemData})
}

Proof: https://jsfiddle.net/69z2wepo/92281/.

So, in order to avoid re-renders, the child component has to implement shouldComponentUpdate anyway. Now, the only reasonable implementation is completely ignoring onClick regardless of whether it has changed:

shouldComponentUpdate(nextProps) {
    return this.props.array !== nextProps.array;
}
Chronicle answered 22/11, 2017 at 14:59 Comment(2)
In your proof, you're calling ReactDom.render twice. This would force all the components to render from top to bottom, so I'm not sure how that counts as proof for re-render based on the approach to using onClick. In fact, you seem to suggest what the official docs suggest for avoiding re-render due to event handler wiring is wrong.Mucoviscidosis
Thanks for checking out my code! Well, although I realise that my advice actually answers a different question, namely how to avoid unnecessary re-renders instead of how to avoid creating excess functions, but that same quoted doc says in the same paragraph that excess functions are hardly a big deal, unlike unnecessary re-renders. Regarding my calling ReactDOM.render twice, I strongly believe it behaves the same way, here's a similar example where I've changed explicit re-rendering to the one which is caused by some parent state update: jsfiddle.net/7a9enxsb/1.Chronicle
R
3

Documentation encourages to use data-attributes and access them from within evt.target.dataset:

_deleteTodo = (evt) => {
  const elementToDelete = evt.target.dataset.el;
  this.setState(prevState => ({
    todos: prevState.todos.filter(el => el !== elementToDelete)
  }))
}

// and from render:

todos.map(
  el => <div key={el} data-el={el} onClick={this._deleteTodo}> {el} </div>
)

Also note that this makes sense only when you have performance issues:

Is it OK to use arrow functions in render methods?

Generally speaking, yes, it is OK, and it is often the easiest way to pass parameters to callback functions.

If you do have performance issues, by all means, optimize!

Randeerandel answered 13/11, 2018 at 9:24 Comment(2)
Since your answer is now in 2018, just to share nowadays you can use "React Hook"Inhalator
Are you talking about useCallback?Randeerandel

© 2022 - 2024 — McMap. All rights reserved.