Michael Tontchev
Michael Tontchev

Reputation: 1109

How to pass arguments to callback in React + Typescript without new function creation?

Say you have the following TSX code:

render() {
    const someParameter = 5;

    return (
        <div onClick={() => this.doSomething(someParameter)}></div>
    );
}

Now, we know that we shouldn't pass fat arrow functions like this to the onClick handler because it will create a new function every time, forcing a rerender (this is especially bad when we're passing this callback to a deep component, not just a div).

So then the solution, if there were no parameters, would be to bind doSomething to this in the constructor of our class, like this.doSomething = this.doSomething.bind(this) and then pass the bound function as the callback. However, this doesn't cut it in our case, because we want to pass a parameter to the function - someParameter. Imagine that someParameter wasn't just a stupid constant like it is above, but instead an element in an array that we get in a map() over an array in our render method. How do we handle this situation? That is, how do we pass a function that is not created from scratch every time, thereby breaking our ability to smartly rerender only when necessary?

Thanks!

Upvotes: 4

Views: 5027

Answers (2)

Bartek Fryzowicz
Bartek Fryzowicz

Reputation: 6674

bind allows to bind not only context but also parameters. So you can do something like this in constructor:

this.doSomething = this.doSomething.bind(this, someArgument)

Above solution will work only if someArgument doesn't change during component lifecycle.
If this argument is going to be dynamic the only solution is to use arrow function like in your example - but please note that might casue additional re-rendering only if this function is passed as a prop to child components (not HTML elements). According to react docs:

In most cases, this is fine. However, if this callback is passed as a prop to lower components, those components might do an extra re-rendering.

If you don't pass it to React child component it shouldn't be a problem and will not cause any additional re-rendering. Please note that there is a difference between React components and HTML tags. If you use arrow function as event callback on HTML element (e.g. on div generated in map) it will not trigger additional rendering of this div because it's not a component and doesn't have render method. React Virtual DOM algorithm will decide if actuall DOM element corresponding to this div should be updated and in our case existing DOM element will not be updated (you can check it using dev tools).

class Hello extends React.Component{
  constructor() {
    super();
    this.state = {test: 0};
  }
  componentDidMount() {
    var i =0;
    this.timer = setInterval(() => {
      i++;
    	this.setState({test: i})
    }, 1000)
  }
  componentWillUnmount() {
    clearInterval(this.timer)
  }
  render() {
    return (<div>{this.state.test}
      {
        [1, 4, 6].map((v) => {
          return (<div key={v} onClick={() => { console.log(this.state.test)}}>test</div>)
        })
      }
    </div>);
  }
};

ReactDOM.render(
  <Hello name="World" />,
  document.getElementById('container')
);
<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="container">
    <!-- This element's contents will be replaced with your component. -->
</div>

You can inspect outoput generated by above code snippet to see that div elements are not updated in DOM although they have arrow function defined as event callback and parent component it being re-rendred.

In case you need to pass this function to child component you can separetely pass prop with function and prop with argument value to avoid extre re-rendering (of course you also have to implement shouldComponentUpdate):

<SomeComponent myClick={this.doSomething} myArgument={someArgument} />

and somewhere in SomeComponent:

//JSX in SomeComponent render method
<div onClick={() => this.props.myClick(this.props.myArgument)} >...</div>

You can also create additional method in SomeComponent instead of using an arrow function:

handleMyClick() {
   this.props.myClick(this.props.myArgument);
}

and then use it JSX:

<div onClick={this.handleMyClick} >...</div>

So to sum up: if you use arrow function as event callback on HTML tags it shouldn't cause performance problems and it will not trigger any additional re-rendering - it will not break ability to smartly rerender only when necessary. It may cause additional re-rendering only if arrow function is passed down as prop to child components and in such case you can use soulution suggested by me.

Upvotes: 4

Alexander Rakov
Alexander Rakov

Reputation: 1

Can you please check solution proposed here in "Protips" section? https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md

Looks as if this might work:

    var List = createReactClass({
              render() {
        return (
          <ul>
            {this.props.items.map(item =>
              <ListItem key={item.id} item={item} onItemClick={this.props.onItemClick} />
            )}
          </ul>
        );
      }
    });

    var ListItem = createReactClass({
      render() {
        return (
          <li onClick={this._onClick}>
            ...
          </li>
        );
      },
      _onClick() {
        this.props.onItemClick(this.props.item.id);
      }
    });

Upvotes: 0

Related Questions